Tutorial

Simple Unit Tests

Every programmer has at one time deployed code without having tested it.  Simple changes go out with the assumption that they can not possibly fail. And then they betray us.  We learn the lesson:  all code must be tested, thoroughly and repeatedly.

On robots we often need the hardware to do some of the testing, but there are still a lot of tests that can be executed offline.  Ideally, you should build up suites of tests that execute automatically; just start one test program and all tests execute.  There are many categories of automated tests, but the most common is called unit testing, because they test small units of functionality, including the functions you assumed can’t fail.

Well crafted unit tests will improve the quality of your software and insure its quality down the road.  You may choose to organize your development around those tests, a practice called Test Driven Development.  Unit tests are also essential to refactoring, which is a systematic technique for improving your code;  you’ll need automated test to verify that your refactored code still works correctly.

Unit testing with WPILib

GradleRIO is already set up for the unit testing frameworks JUnit and GoogleTest.   If you define unit test classes in your project, they will automatically execute every time you build the code.

Let’s define a simple function and create unit tests for it.  Don’t worry that this code looks too simple to merit testing.   Remember that no code is so trivial that it won’t fail.

A simple function

Suppose you’ve got a Gyro installed on your robot.  When you first start it up, the gyro will show 0 degrees.  Rotate the robot a little to the right and it might read 30 degrees.  However, the gyro’s scale is continuous, so after a lot of driving around it might read 1537 degrees or -2781 degrees.  This might mess up the math in some of your autonomous commands, since 1537 degrees is really the same as 97 degrees.  We need a function that simplifies angles into the range -180 to 180.  Here are some test cases:

  • 270 degree is the same as -90 degrees
  • -315 degrees is the same as 45 degrees
  • 30 degrees is still 30 degrees
  • -60 degrees is still -60 degrees

Here’s a simple conversion function.  It definitely isn’t perfect, but we’ll fix that in a minute:

public int simplifyAngle(int angle) {
    if (angle > 180) {
        angle = angle - 360;
    }
    if (angle < -180) {
        angle = angle + 360;
    }
    return angle;
}

For this example, this function is in your Robot class which is stored with the other java main classes in your “src” directory:

unit_test_func1

A simple unit test

Add a “test” directory under “src” for your java unit tests.  Right-click on “src”, select “New Folder” and enter “test/java/frc/robot”.  Right-click on “robot” and select create an empty class named “RobotTest.java”

unit_test_test1.png

Consider the test method:

@Test
public void testSimplifyAngle() {
    Robot robot = newRobot();
    assertEquals(-90, robot.simplifyAngle(270));
    assertEquals(-45, robot.simplifyAngle(315));
    assertEquals(-60, robot.simplifyAngle(-60));
    assertEquals(30, robot.simplifyAngle(30));
}

The @Test annotation on top means that this method will be executed by the GradleRIO test task.  We create a Robot object and then test our method for each of the test cases.

This test class will execute every time you build robot code.  If any of the assertions fail, the whole build will be rejected. To see what happens on a failure, temporarily change the 30 degree test so it expects -30 degrees. The build will fail and tell you to check line 15:

unit_test_fail1

Improving the function

How many test cases should you use?  Usually more than you would expect, even for simple functions.

Always include a trivial test case, sometimes called the “happy path” case. The 30 degree and -60 degree test might be considered happy path tests, but we could also test 0 degrees.  Add some test scenarios where there are logical transitions; these are called “corner cases”.  For this example, corner tests might be at 180 degrees and -180 degrees.  Also test a couple extreme cases, such as 1537 degrees and -2781 degrees.  Extreme tests at absolute maximums or minimums are called “edge cases”.

Now our test looks like this:

@Test
public void testSimplifyAngle() {
    Robot robot=new Robot();
    assertEquals(-90, robot.simplifyAngle(270));
    assertEquals(-45, robot.simplifyAngle(315));
    assertEquals(-60, robot.simplifyAngle(-60));
    assertEquals(30, robot.simplifyAngle(30));
    assertEquals(0, robot.simplifyAngle(0));
    assertEquals(180, robot.simplifyAngle(180));
    assertEquals(-180, robot.simplifyAngle(-180));
    assertEquals(97, robot.simplifyAngle(1537));
    assertEquals(99, robot.simplifyAngle(-2781));
}

Executing this test reveals that our function fails for the extreme cases.  Our function can’t handle 1537 degrees.  We’ve found a bug in our logic.   We go back to the original function and, after a little thought,  change it to the following:

public int simplifyAngle(int angle) {
    while (angle > 180) {
        angle = angle - 360;
    }
    while (angle < -180) {
        angle = angle + 360;
    }
    return angle;
}
Now our test passes.  The bug is fixed.

Refactoring

At some point, you or one of your teammates will rewrite parts of the robot code, at which point you must retest and verify that the new code is at least as good as the old.  For instance, someone might refactor the angle simplification like this:

public int simplifyAngle(int angle) {
    return angle > 180 
    ? simplifyAngle(angle - 360) 
    : angle < -180 ? simplifyAngle(angle + 360) : angle;
}

Does this function do the same job?  It turns out that it does. Is this function better? Well, it is shorter, but you should decide if it’s really more readable.

Eventually, you might stumble on logic like this:

public int simplifyAngle(int angle) {
    return  (int)Math.round(Math.IEEEremainder(angle,360.0));
}

This is even shorter.  It’s much more cryptic, but it does pass the tests.  You could use any of these functions in your robot.  Unit tests have verified that they all do the same thing.

Writing good tests

Now that you know how to create unit tests, start adding them to your robot projects. You will find that writing good tests is as difficult and subtle a skill as programming the robot code.  You should start watching for opportunities to test.  Break up your big methods into smaller methods and structure them so they are more amenable to testing.  Test the simple things, but especially watch for code that is tricky.

It’s probably possible to write too many tests, but don’t worry about that.  On professional projects the test suites are often larger than the baseline code.

Good unit tests should have the following qualities:

  1. Test the requirements and nothing but requirements.  In the above example we require that 270 degrees is simplified down to -90 degrees.  However, don’t try to craft tests that verify the number of times the “while” loop executes to achieve this.
  2. Tests should be deterministic and always succeed or fail based on the requirements.  Take care around code that depends on hardware or file systems or random functions or timers or large memory usage.  Structure your code so you can manage any randomness.
  3. Unit tests should be fast.  They execute before every build and you don’t want to start regretting how slow they are.
  4. Tests should be easy to read, understand, and maintain later

The above example is intentionally simple.  Once you’ve mastered the concepts you can start to think about automated testing of larger classes, non-trivial state machines,  subsystems and commands.

Further Reading

Leave a comment