Unit Testing for FRC Robot Code
What is Unit Testing?
Unit testing is the practice of testing individual units of source code to determine if they are fit for use. In the context of FRC, a 'unit' is typically a class, a method, or a subsystem. Unlike integration tests or full robot tests, unit tests run on your development machine (not the roboRIO) and execute in milliseconds.
Unit testing allows you to verify logic, math, and state transitions without needing a physical robot. This is crucial for catching bugs early, documenting expected behavior, and allowing for safe refactoring. For FRC, it enables testing complex logic like kinematics, state machines, and autonomous sequences before you even have a drivetrain built.
Learn more: WPILib: Unit Testing
Unit testing allows you to verify logic, math, and state transitions without needing a physical robot. This is crucial for catching bugs early, documenting expected behavior, and allowing for safe refactoring. For FRC, it enables testing complex logic like kinematics, state machines, and autonomous sequences before you even have a drivetrain built.
Learn more: WPILib: Unit Testing
Why Unit Testing is Important
1. Early Bug Detection: Catch logic errors, off-by-one errors, and edge cases before deploying code to the robot. This saves valuable battery/practice time.
2. Safe Refactoring: When you clean up or optimize code, unit tests act as a safety net, ensuring you haven't broken existing functionality.
3. Documentation: Tests serve as live documentation. A new member can look at "DrivetrainTest" to understand how the "Drivetrain" class is supposed to behave.
4. Faster Feedback Loop: deploying code takes minutes; running tests takes seconds. This encourages more frequent testing and iteration.
5. Testing Hard-to-Reach States: You can simulate rare error conditions (like a sensor disconnect or max current draw) that are dangerous or difficult to reproduce on a real robot.
2. Safe Refactoring: When you clean up or optimize code, unit tests act as a safety net, ensuring you haven't broken existing functionality.
3. Documentation: Tests serve as live documentation. A new member can look at "DrivetrainTest" to understand how the "Drivetrain" class is supposed to behave.
4. Faster Feedback Loop: deploying code takes minutes; running tests takes seconds. This encourages more frequent testing and iteration.
5. Testing Hard-to-Reach States: You can simulate rare error conditions (like a sensor disconnect or max current draw) that are dangerous or difficult to reproduce on a real robot.
Testing Frameworks
WPILib projects use JUnit 5 as the standard testing framework. It provides the structure for writing tests (assertions, test lifecycle methods).
Mockito is typically used alongside JUnit. It allows you to create 'mock' objects—fake versions of complex dependencies like motor controllers or sensors. This lets you test your logic in isolation without needing the actual hardware libraries to be active or connected.
Mockito is typically used alongside JUnit. It allows you to create 'mock' objects—fake versions of complex dependencies like motor controllers or sensors. This lets you test your logic in isolation without needing the actual hardware libraries to be active or connected.
Basic Unit Test Example
package frc.robot.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class MathUtilsTest {
@Test
public void testClamp() {
// Assert that clamp(val, min, max) returns expected results
assertEquals(5.0, MathUtils.clamp(10.0, 0.0, 5.0), 0.001, "Should clamp to max");
assertEquals(0.0, MathUtils.clamp(-5.0, 0.0, 5.0), 0.001, "Should clamp to min");
assertEquals(3.0, MathUtils.clamp(3.0, 0.0, 5.0), 0.001, "Should return value within range");
}
@Test
public void testDeadband() {
assertEquals(0.0, MathUtils.applyDeadband(0.05, 0.1), 0.001, "Should be zero inside deadband");
assertEquals(0.5, MathUtils.applyDeadband(0.5, 0.1), 0.001, "Should return value outside deadband");
}
}Testing Subsystems with Mocks
Subsystems often depend on hardware (TalonFX, SparkMax, etc.). To test subsystem logic without hardware, use Mockito to mock these dependencies.
By mocking, you can verify that your subsystem calls the correct methods on the hardware (e.g., "did we tell the motor to set voltage to 12V?") without actually having a motor connected.
By mocking, you can verify that your subsystem calls the correct methods on the hardware (e.g., "did we tell the motor to set voltage to 12V?") without actually having a motor connected.
Subsystem Test with Mockito
package frc.robot.subsystems;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.revrobotics.spark.SparkMax;
import frc.robot.Constants;
public class IntakeTest {
private SparkMax m_mockMotor;
private Intake m_intake;
@BeforeEach
void setup() {
// Create a mock SparkMax
m_mockMotor = mock(SparkMax.class);
// Inject the mock into the subsystem
m_intake = new Intake(m_mockMotor);
}
@Test
void testDeploy() {
m_intake.deploy();
// Verify the motor was set to the deploy speed
verify(m_mockMotor).set(Constants.Intake.kDeploySpeed);
}
@Test
void testStop() {
m_intake.stop();
// Verify the motor was stopped
verify(m_mockMotor).set(0.0);
}
}Testing Commands and Time
Commands often involve timing (e.g., "WaitCommand", running for a duration). WPILib's "SimHooks" allows you to control the robot's simulated time, letting you test time-dependent logic instantly.
You can also simulate the "CommandScheduler" to verify that commands finish when expected or are interrupted correctly.
You can also simulate the "CommandScheduler" to verify that commands finish when expected or are interrupted correctly.
Command and Time Simulation
package frc.robot.commands;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
import edu.wpi.first.wpilibj.simulation.SimHooks;
import edu.wpi.first.wpilibj2.command.CommandScheduler;
import edu.wpi.first.wpilibj2.command.WaitCommand;
public class CommandTest {
@Test
void testWaitCommand() {
CommandScheduler scheduler = CommandScheduler.getInstance();
WaitCommand waitCmd = new WaitCommand(2.0);
scheduler.schedule(waitCmd);
scheduler.run();
assertTrue(scheduler.isScheduled(waitCmd), "Command should be scheduled");
// Advance time by 1 second
SimHooks.stepInTime(1.0);
scheduler.run();
assertTrue(scheduler.isScheduled(waitCmd), "Command should still be running after 1s");
// Advance time by another 1.1 seconds (total 2.1)
SimHooks.stepInTime(1.1);
scheduler.run();
assertFalse(scheduler.isScheduled(waitCmd), "Command should finish after 2s");
}
}Best Practices for FRC Unit Testing
Follow these guidelines for effective tests:
- Isolate tests: Each test should run independently and not depend on the state of others.
- Mock hardware: Don't rely on real hardware classes; mock them or use simulation classes.
- Test edge cases: Test boundary values (0, max, min) and invalid inputs.
- Keep it fast: Unit tests should run instantly. If it takes seconds, it's too slow.
- Use dependency injection: Pass hardware objects into subsystem constructors to make mocking easier.
- Run often: Run tests before every commit and push.