Unit Testing¶
High-fidelity simulation with CTR Electronics devices can be used for unit testing robot applications.
When writing unit tests, the regular device APIs should be used to control devices and read status signals. Just like in simulation, the device SimState
API can be used to update the simulated state of the device.
Additionally, users must ensure the robot is enabled prior to controlling actuators. This can be accomplished in WPILib by calling DriverStationSim.setEnabled(true)
(Java, C++), followed by DriverStation.notifyNewData()
to apply the change (Java, C++).
Important
There may be a short delay between enabling the robot and the simulated actuators being enabled. Unit tests should delay for ~100ms after constructing all devices and enabling the robot to account for this delay.
In unit tests, users should utilize the StatusSignal.waitForUpdate()
and BaseStatusSignal.waitForAll()
APIs to wait for fresh data after sending a control request or modifying the simulated device state.
Important
There may be a short delay between sending a control request and the simulated device applying the control. Unit tests should delay for ~20ms after sending a control request to account for this delay.
Below is an example unit test that verifies the robot is enabled and verifies that the device responds to a control request.
public class TalonFXTest implements AutoCloseable {
static final double DELTA = 1e-3; // acceptable deviation range
TalonFX m_fx;
TalonFXSimState m_fxSim;
@Override
public void close() {
/* destroy our TalonFX object */
m_fx.close();
}
@BeforeEach
public void constructDevices() {
assert HAL.initialize(500, 0);
/* create the TalonFX */
m_fx = new TalonFX(0);
m_fxSim = m_fx.getSimState();
/* enable the robot */
DriverStationSim.setEnabled(true);
DriverStationSim.notifyNewData();
/* delay ~100ms so the devices can start up and enable */
Timer.delay(0.100);
}
@AfterEach
void shutdown() {
close();
}
@Test
public void robotIsEnabled() {
/* verify that the robot is enabled */
assertTrue(DriverStation.isEnabled());
}
@Test
public void motorDrives() {
/* set the voltage supplied by the battery */
m_fxSim.setSupplyVoltage(RobotController.getBatteryVoltage());
var dutyCycle = m_fx.getDutyCycle();
/* wait for a fresh duty cycle signal */
dutyCycle.waitForUpdate(0.100);
/* verify that the motor output is zero */
assertEquals(dutyCycle.getValue(), 0.0, DELTA);
/* request 100% output */
m_fx.setControl(new DutyCycleOut(1.0));
/* wait for the control to apply */
Timer.delay(0.020);
/* wait for a new duty cycle signal */
dutyCycle.waitForUpdate(0.100);
/* verify that the motor output is 1.0 */
assertEquals(dutyCycle.getValue(), 1.0, DELTA);
}
}
class TalonFXTest : public testing::Test {
protected:
/* create the TalonFX */
hardware::TalonFX m_fx{0};
sim::TalonFXSimState& m_fxSim{m_fx.GetSimState()};
void SetUp() override
{
/* enable the robot */
frc::sim::DriverStationSim::SetEnabled(true);
frc::sim::DriverStationSim::NotifyNewData();
/* delay ~100ms so the devices can start up and enable */
std::this_thread::sleep_for(std::chrono::milliseconds{100});
}
};
TEST_F(TalonFXTest, RobotIsEnabled)
{
/* verify that the robot is enabled */
EXPECT_TRUE(frc::DriverStation::IsEnabled());
}
TEST_F(TalonFXTest, MotorDrives)
{
/* set the voltage supplied by the battery */
m_fxSim.SetSupplyVoltage(frc::RobotController::GetBatteryVoltage());
auto& dutyCycle = m_fx.GetDutyCycle();
/* wait for a fresh duty cycle signal */
dutyCycle.WaitForUpdate(100_ms);
/* verify that the motor output is zero */
EXPECT_DOUBLE_EQ(dutyCycle.GetValue(), 0.0);
/* request 100% output */
m_fx.SetControl(controls::DutyCycleOut{1.0});
/* wait for the control to apply */
std::this_thread::sleep_for(std::chrono::milliseconds{20});
/* wait for a new duty cycle signal */
dutyCycle.WaitForUpdate(100_ms);
/* verify that the motor output is 1.0 */
EXPECT_DOUBLE_EQ(dutyCycle.GetValue(), 1.0);
}