Unit 09 · lab

Lab: Build, Break, and Repair a Java Robot Test Suite

This lab turns Week 9 into an actual testing workflow.

You will create JUnit tests for robot-domain Java code, introduce one deliberate boundary defect, preserve the failing result, repair the implementation, rerun the same test, and then compare that software evidence with a separate ROS 2 graph observation.

Your final artifact is a Component Test Record.

Choose your execution lane

Local WPILib lane

Use this lane if you have a WPILib Java project available in VS Code.

Place production code under the project's src/main/java source tree and test code under:

src/test/java/

Run the tests using WPILib: Test Robot Code from the VS Code Command Palette. Preserve the terminal result or the JUnit HTML report generated under:

build/reports/tests/test/index.html

This is the preferred lane because the tests actually execute in the WPILib desktop test environment.

Reader evidence lane

If you do not have a local WPILib project, complete the same code and reasoning tasks using the supplied result records in this lab.

Label them supplied JUnit evidence. Do not claim you executed them locally.

Both lanes assess the same concepts. Neither requires physical robot hardware.

Guided example: turn one rule into a test

Production rule:

class ObstacleGuard {
  private final double cautionDistanceMeters;

  ObstacleGuard(double cautionDistanceMeters) {
    this.cautionDistanceMeters =
        cautionDistanceMeters;
  }

  boolean shouldStop(RangeSource source) {
    return source.getRangeMeters()
        < cautionDistanceMeters;
  }
}

Known input:

RangeSource near =
    new FixedRangeSource("test-near", 0.25);

Requirement:

0.25 m is below 0.50 m, so shouldStop must return true.

JUnit test:

@Test
void stopsForKnownNearRange() {
  ObstacleGuard guard =
      new ObstacleGuard(0.50);

  RangeSource near =
      new FixedRangeSource("test-near", 0.25);

  assertTrue(guard.shouldStop(near));
}

The test is defensible because the expectation comes from the written rule and the controlled input.

You will now build a broader suite.

Part 1: Create the test files

Use these production classes from the course:

RangeSource
FixedRangeSource
ObstacleGuard
ValidatedRangeSensor

In a real WPILib project, organize the files inside your package structure. One reasonable layout is:

src/main/java/frc/robot/logic/RangeSource.java
src/main/java/frc/robot/logic/FixedRangeSource.java
src/main/java/frc/robot/logic/ObstacleGuard.java
src/main/java/frc/robot/logic/ValidatedRangeSensor.java

src/test/java/frc/robot/logic/ObstacleGuardTest.java
src/test/java/frc/robot/logic/ValidatedRangeSensorTest.java

Your package names may differ. Keep the source and test package paths consistent.

Part 2: Test the caution boundary

Create three tests for a guard configured at 0.50 m:

InputExpected
0.49 mtrue
0.50 mfalse
0.51 mfalse

Your class should contain tests equivalent to:

@Test
void stopsImmediatelyBelowBoundary() {
  ObstacleGuard guard =
      new ObstacleGuard(0.50);

  RangeSource source =
      new FixedRangeSource("below", 0.49);

  assertTrue(guard.shouldStop(source));
}

@Test
void continuesExactlyAtBoundary() {
  ObstacleGuard guard =
      new ObstacleGuard(0.50);

  RangeSource source =
      new FixedRangeSource("at", 0.50);

  assertFalse(guard.shouldStop(source));
}

@Test
void continuesImmediatelyAboveBoundary() {
  ObstacleGuard guard =
      new ObstacleGuard(0.50);

  RangeSource source =
      new FixedRangeSource("above", 0.51);

  assertFalse(guard.shouldStop(source));
}

Record the requirement these three tests protect:

The caution rule is strict < 0.50 m.

Part 3: Run the correct version once

If you are using the local WPILib lane, run WPILib: Test Robot Code.

Preserve the result for the three boundary tests.

If you are using the reader lane, use this supplied baseline record:

ObstacleGuardTest
  stopsImmediatelyBelowBoundary        PASSED
  continuesExactlyAtBoundary           PASSED
  continuesImmediatelyAboveBoundary    PASSED

Label it supplied evidence.

This baseline matters because you are about to break the code deliberately.

Part 4: Introduce the boundary defect

Change the implementation from:

return source.getRangeMeters()
    < cautionDistanceMeters;

to:

return source.getRangeMeters()
    <= cautionDistanceMeters;

Do not change the tests.

Predict which test should fail before running or reading the result.

The correct prediction is:

continuesExactlyAtBoundary

because the new implementation now classifies 0.50 m as true.

Part 5: Preserve the failing result

Local lane: rerun the same suite and preserve the failed test name plus expected/actual information.

Reader lane: use this supplied defect record:

ObstacleGuardTest
  stopsImmediatelyBelowBoundary        PASSED
  continuesExactlyAtBoundary           FAILED
      expected: false
      actual:   true
  continuesImmediatelyAboveBoundary    PASSED

Your Component Test Record must include:

  • defect introduced;
  • failed test;
  • input;
  • expected result;
  • actual result;
  • written requirement.

Do not repair the record. Preserve the failure exactly as evidence.

Part 6: Repair the implementation, not the expectation

Restore:

<

instead of:

<=

Rerun the test suite or use the original passing baseline in the reader lane.

Record:

repair: <= changed back to <
regression test retained: continuesExactlyAtBoundary

Explain why the test stays in the suite after the bug is fixed.

Part 7: Test the validated range boundaries

Create a sensor with:

minimum = 0.10 m
maximum = 2.00 m
starting reading = 0.42 m

Add tests for:

0.10 m accepted
2.00 m accepted
0.09 m rejected
2.01 m rejected

Use assertThrows(...) for rejected updates.

Example:

@Test
void rejectsReadingAboveMaximum() {
  ValidatedRangeSensor sensor =
      makeSensor();

  assertThrows(
      IllegalArgumentException.class,
      () -> sensor.updateRange(2.01)
  );
}

Your helper method may create a fresh sensor for each test:

private ValidatedRangeSensor makeSensor() {
  return new ValidatedRangeSensor(
      "range-front",
      0.10,
      2.00,
      0.42
  );
}

Part 8: Test state preservation after failure

Add:

@Test
void rejectedUpdatePreservesLastValidReading() {
  ValidatedRangeSensor sensor =
      makeSensor();

  assertThrows(
      IllegalArgumentException.class,
      () -> sensor.updateRange(3.70)
  );

  assertEquals(
      0.42,
      sensor.getRangeMeters(),
      0.001
  );
}

This test matters because an exception alone does not prove the object stayed valid.

Record both behaviors:

3.70 m rejected
stored reading remains 0.42 m

Part 9: Add one specification question

Consider the input:

Double.NaN

Do not automatically change the production class.

Instead, write this in your record:

Open requirement question:
Should non-finite range values such as NaN be rejected explicitly?

Testing is allowed to expose an unanswered requirement.

A good engineer does not quietly invent policy just because a test idea revealed a gap.

Part 10: Inspect a separate ROS 2 evidence source

The terminal below is deterministic and does not know anything about your JUnit test run.

xterm.js terminal simulation

Inspect a Java application beside a ROS 2 Jazzy graph

Source a simulated Jazzy shell, verify the active distribution, inspect nodes and their graph relationships, and keep runtime observations separate from Java and hardware claims.

This is a controlled command simulator. It does not execute Java, ROS 2, shell commands, or network requests on your device.

Commands worth trying
  • echo $ROS_DISTRO
  • source /opt/ros/jazzy/setup.bash
  • echo $ROS_DISTRO
  • ros2 --help
  • ros2 node list
  • ros2 node info /sensor_bridge
  • ros2 node info /motor_guard
  • ros2 topic list
  • ros2 param get /motor_guard caution_distance_m

Run:

source /opt/ros/jazzy/setup.bash
ros2 node list

Record the node names shown.

Now add two evidence headings to your artifact:

Java/WPILib test evidence
ROS 2 graph evidence

Do not merge them.

Part 11: Repair an overclaim

A teammate writes:

All JUnit tests passed, and /sensor_bridge is visible, so the robot's front sensor and stopping system are working correctly.

Rewrite the claim.

Your repaired version should say what the tests established, what the graph snapshot established, and what physical evidence remains missing.

Final Component Test Record

Your artifact must contain:

Rule under test

Write the strict caution-distance requirement.

Test matrix

Include at least:

  • below boundary;
  • exact boundary;
  • above boundary;
  • exact sensor minimum;
  • exact sensor maximum;
  • below minimum;
  • above maximum;
  • rejected-update state preservation.

Deliberate regression

Preserve the <= defect, failed test, expected/actual result, repair, and successful rerun.

Exception tests

Show at least one assertThrows(...) case.

Execution label

State whether your JUnit results came from:

local WPILib execution

or:

supplied JUnit evidence

ROS graph evidence

Record the controlled ros2 node list result separately.

Limitations

Name at least three facts these tests and graph observations do not establish about physical hardware.

Success criteria

Your lab is complete when another student can identify:

  • the exact software requirement;
  • the boundary values that protect it;
  • the intentionally introduced defect;
  • the test that exposed that defect;
  • why the expectation was not changed to hide the failure;
  • how invalid range updates are tested;
  • what state remains after a rejected update;
  • which evidence came from JUnit and which came from ROS 2;
  • what still requires physical-system evidence.

A strong test record does not try to make every result green. It preserves the disagreement that taught you something and shows exactly how the repair was verified.