Unit 09 · lesson

WPILib Unit Tests: Test Robot Code on the Desktop

So far, Week 9 has tested small Java classes that do not depend on a robot framework. That is a good place to begin because pure logic is fast to test and easy to reason about.

WPILib also supports unit testing inside Java robot projects. The current WPILib documentation uses JUnit 5 and runs robot tests in desktop simulation, which lets teams exercise code without deploying every change to a roboRIO or moving a physical mechanism.

That does not make simulation identical to hardware. It gives you another controlled software evidence source.

A WPILib subsystem is still Java code

Recall the command-based architecture from Week 6.

A subsystem commonly extends SubsystemBase:

import edu.wpi.first.wpilibj2.command.SubsystemBase;

public class RangeSubsystem extends SubsystemBase {
  private double rangeMeters;

  public RangeSubsystem(double initialRangeMeters) {
    rangeMeters = initialRangeMeters;
  }

  public double getRangeMeters() {
    return rangeMeters;
  }

  public boolean obstacleTooClose(
      double cautionDistanceMeters
  ) {
    return rangeMeters < cautionDistanceMeters;
  }
}

This is a simplified teaching subsystem. It stores a modeled value rather than reading real sensor hardware.

The class participates in WPILib's command-based subsystem architecture because it extends SubsystemBase.

The decision method:

obstacleTooClose(...)

is still ordinary Java logic that can be tested with known values.

Put the test in the test source set

A WPILib Java project may contain a production source file such as:

src/main/java/frc/robot/subsystems/RangeSubsystem.java

and a test such as:

src/test/java/frc/robot/subsystems/RangeSubsystemTest.java

The test belongs under src/test/java because WPILib's Java test tooling uses the test source set.

A test class might look like:

package frc.robot.subsystems;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;

class RangeSubsystemTest {

  @Test
  void reportsObstacleInsideCautionDistance() {
    RangeSubsystem subsystem =
        new RangeSubsystem(0.25);

    assertTrue(
        subsystem.obstacleTooClose(0.50)
    );
  }

  @Test
  void reportsNoObstacleAtExactBoundary() {
    RangeSubsystem subsystem =
        new RangeSubsystem(0.50);

    assertFalse(
        subsystem.obstacleTooClose(0.50)
    );
  }
}

The testing concepts did not change because the class now extends SubsystemBase.

You still have:

  • controlled setup;
  • a behavior call;
  • an expected result;
  • an assertion.

Why test the smallest useful layer first

Suppose your robot's full obstacle response eventually involves:

  • a sensor;
  • a subsystem;
  • a command;
  • the command scheduler;
  • motor output;
  • physical movement.

Testing all of those at once makes failures harder to isolate.

If the threshold rule is wrong, a small unit test should expose that before you involve the rest of the system.

This is one reason testable design matters. A focused method with clear inputs and outputs gives the test a clean target.

The trace below makes that boundary visible. It follows one static teaching case from observation to decision to commanded response, then separates software verification from physical-hardware verification.

Read the evidence left to right. The trace is not claiming that a sensor, motor, roboRIO, or ROS 2 graph is live. It is showing which claim each software-stage observation can support.

WPILib can simulate hardware-facing code too

Real subsystems often wrap WPILib hardware classes rather than storing a plain double.

The official WPILib unit-testing example demonstrates this by creating simulation objects such as simulated motor and solenoid state, then using JUnit assertions to inspect what the subsystem did.

That adds another layer:

  • production subsystem code uses WPILib hardware abstractions;
  • simulation objects expose the modeled hardware state on the desktop;
  • JUnit asserts expected behavior against that simulated state.

For example, if an intake subsystem should not run its motor while retracted, a simulation test can verify the modeled motor output remains zero.

That is more meaningful than checking whether a method printed the word stopped.

Some robot tests need setup and cleanup

WPILib's documentation uses JUnit lifecycle annotations such as:

@BeforeEach

and:

@AfterEach

when a fresh robot component and simulation state are needed for every test.

Conceptually:

@BeforeEach
void setup() {
  // create fresh subsystem and simulation state
}

@AfterEach
void shutdown() throws Exception {
  // release resources used by the test
}

A clean test should not accidentally depend on state left behind by a previous test.

This matters especially when WPILib objects allocate hardware-resource identifiers or simulation resources.

Why clean test state matters

Imagine two tests share one mutable subsystem.

Test A changes the modeled range from 0.42 to 0.20.

Test B assumes the subsystem still starts at 0.42.

If the object is reused, Test B may fail depending on which test ran first.

That is a test isolation problem.

A good unit test should normally be able to run by itself and still produce the same result.

Creating fresh state for each test makes the result easier to trust.

Run tests through WPILib's test workflow

The stable WPILib documentation provides Test Robot Code in the VS Code Command Palette.

When tests run, the terminal reports which tests passed or failed. JUnit also writes a detailed HTML report under:

build/reports/tests/test/index.html

WPILib's documentation also notes that robot-code tests run in simulation on the desktop.

That workflow gives you an authentic engineering cycle:

  1. edit robot code;
  2. run tests;
  3. inspect a failure;
  4. repair code or a mistaken expectation;
  5. rerun the same tests.

You still need hardware testing later when the claim concerns physical behavior.

Simulation is not a physical robot

If a WPILib simulation test passes, you can say something like:

The tested subsystem produced the expected simulated state for the supplied test case.

You cannot automatically say:

The physical robot mechanism will behave correctly under every real condition.

Simulation may not reproduce:

  • wiring defects;
  • damaged hardware;
  • mechanical friction;
  • sensor noise;
  • battery sag;
  • CAN faults;
  • vendor-specific behavior not modeled by the simulation;
  • timing or environmental effects outside the test.

Tests reduce uncertainty. They do not erase the boundary between software and the physical machine.

WPILib test evidence is also not ROS graph evidence

The same boundary applies to ROS 2.

A JUnit test of RangeSubsystem does not prove that:

ros2 node list

contains any particular node.

It does not prove that a ROS topic exists or that a ROS message was published.

Those are separate runtime observations.

This course keeps those evidence layers separate because a real robot architecture may eventually combine them.

Worked review: what does each result prove?

Suppose you have:

RangeSubsystemTest
  reportsObstacleInsideCautionDistance PASSED
  reportsNoObstacleAtExactBoundary PASSED

and separately:

ros2 node list
/sensor_bridge
/motor_guard

The JUnit result supports a claim about the tested Java/WPILib code path under its supplied inputs.

The ROS result supports a claim about node names visible in the inspected graph state.

Neither result proves the other.

A disciplined report keeps both records instead of combining them into "robot works."

Practice: design three subsystem tests

A simplified elevator subsystem contains:

public boolean aboveSafeHeight(
    double heightMeters
) {
  return heightMeters > 1.20;
}

The requirement says the elevator is considered above the safe height only when the value is strictly greater than 1.20 m.

Design three JUnit tests for:

1.19 m
1.20 m
1.21 m

For each test:

  • choose a descriptive method name;
  • write the expected boolean assertion;
  • explain which boundary behavior the test protects.

Then state one physical elevator failure that these desktop tests could not detect.

The next lesson will turn your growing collection of test ideas into a test matrix, including normal values, boundaries, invalid inputs, exceptions, and state-preservation checks.