Testing
A test is a check you can repeat. Some need a person standing at the robot, and some the computer runs by itself every time the code changes.
- Your project from Coroutines, building clean, or the
mech-6-Testingbranch. - A charged battery and a clear bench for the on-robot half.
The two halves catch different mistakes. A person at the robot catches a motor wired backwards or a loose CANcoder. An automated test catches a teammate's edit that quietly changed a setpoint three weeks before an event.
On the robot
Run this list before the first enable after any change, and before every match. It takes two minutes.
- Battery above 12.5 volts on the driver station. A low battery makes a tuned mechanism look untuned.
- Every CAN device in Tuner X with the ID the code expects: 31 and 32 for the arm, 21 for the flywheel.
- The code on the robot came from main, built after the last pull.
- Clear the mechanism's path. One person holds Disable and says "enabling" out loud before clicking Enable.
- Press one binding at a time, briefly, and watch the mechanism, not the screen. Check it moves the right way, stops when it should, and does what
whileFalsesays on release.
Disable first
A mechanism heading the wrong way, a grinding noise, or a smell means Disable, now. Work out why with the robot disabled.
Write what you pressed and what happened in the pull request description. That line is how a reviewer knows the change ran on a robot.
A test file
Automated tests live in src/test/java, in a folder that mirrors the class they test. ArmTest.java goes in src/test/java/first/robot/mechanisms/, so it shares Arm's package. The generated build.gradle already includes JUnit 5, the library that runs them, so there is nothing to install.
class ArmTest { private final Scheduler scheduler = Scheduler.getDefault(); private Arm arm; @BeforeEach void setUp() { // Start the simulated hardware layer, the same one Simulate Robot Code uses. assertTrue(HAL.initialize()); arm = new Arm(); } @AfterEach void tearDown() { // The scheduler outlives each test. Clear it so one test's commands never leak into the next. scheduler.cancelAll(); } @Test void verticalAsksForAQuarterTurn() { scheduler.schedule(arm.vertical()); scheduler.run(); assertEquals(0.25, arm.getTargetPosition().in(Rotations), 1e-9); }}Every method marked @Test is one test, and JUnit runs each against a fresh Arm built in setUp. The test schedules a command and calls scheduler.run() once, which is one robot loop. Then it asks the arm where it is headed. assertEquals fails the test unless the answer is 0.25 rotations, give or take 1e-9.
The second test on the branch schedules vertical(), then horizontal(), and checks that the newer command took the arm and the older one stopped running.
Fake a sensor
isAtTarget() compares a measured speed to the target. A test cannot spin a real wheel, so it sets the speed the simulated motor reports.
// A second handle on CAN ID 21 reaches the same simulated motor as the one inside Flywheel,// so the mechanism's hardware can stay private.motorSim = new TalonFX(21, new CANBus("canivore")).getSimState();// Flywheel sets Clockwise_Positive. Tell the sim, or every speed it reports comes back// negative.motorSim.Orientation = ChassisReference.Clockwise_Positive; // ...then, in the test, with runFast() scheduled and run:motorSim.setRotorVelocity(74.8);// Simulated sensors update on their own schedule. Give the new speed time to arrive.Timer.delay(0.1);assertTrue(flywheel.isAtTarget()); motorSim.setRotorVelocity(70.0);Timer.delay(0.1);assertFalse(flywheel.isAtTarget());Both halves matter. 74.8 is inside the 0.5 rotations per second tolerance and must count. 70 is outside it and must not. A test with only the first half still passes when somebody widens the tolerance to 10.
The arm's motor is the other way round, CounterClockwise_Positive, which is also the sim's default. Match the orientation to the mechanism every time.
Testable code
These tests work because the mechanisms from Workshop 3 already answer questions. getTargetPosition(), getVelocity() and isAtTarget() each return a value, and a test can only check a value it can read. A command that sets a motor and reports nothing can only be checked by watching it.
So when you add a decision, put it in a method that returns the answer: true or false, a speed, an angle. Keep the hardware private. A test reaches it through the simulated device on the same CAN ID, as above.
When a test fails for no reason you can see, check these first.
- The target is still 0. The test scheduled a command and never called
scheduler.run(). Scheduling only queues it. - A sensor reads the wrong value. The
Timer.delayis missing, or the sim orientation does not match the motor's inversion. - A test passes alone and fails with the others. A command from an earlier test is still scheduled. Keep
cancelAll()intearDown.
Check your work
- Open the command palette and run WPILib: Test Robot Code. From a terminal,
.\gradlew testdoes the same. - In
Flywheel.java, change the tolerance fromRotationsPerSecond.of(0.5)toRotationsPerSecond.of(10.0)and run the tests again. - Change it back and run them once more.
You should see
- Four lines ending in
PASSED, one per test, thenBUILD SUCCESSFUL. - With the wide tolerance:
atTargetOnlyWithinHalfARotationPerSecond() FAILED, pointing at line 72 ofFlywheelTest.java. - All four passing again once the tolerance is back.
Check yourself
Your test schedules arm.horizontal() and asserts the target is 0.5. It fails, and the target reads 0. What is missing?
You set the simulated flywheel to 74.8 rps and isAtTarget() is false. The wait is there. What do you check?
A teammate wants to test a new isJammed() check on the flywheel. Which version can a test check?
All four tests pass. Which is still unchecked until someone stands at the robot?