Unit Test Guide - uw-advanced-robotics/taproot GitHub Wiki

This guide is intended to walk through the process of writing unit tests in our repository.

Recommended Prerequisite Reading

Before reading this guide, be sure to check out the section on the drivers architecture. For many unit tests you write you will have to interact with the drivers class, so it is important that you generally understand how the drivers are designed before writing unit tests. In case you are unfamiliar with the different build targets present in our codebase, refer to the section on build targets overview. We have a special build target for unit tests that you can read more in the build targets overview. Also, I highly recommend reading through Embedded C/C++ Unit Testing Basics. This is an interesting read that covers some challenges with writing unit tests for embedded systems. It also goes over some basic unit test related definitions that you should be familiar with.

What is a Unit Test?

A simple unit test is a test designed to check the functionality of a single, isolated component. Typically it is self contained and requires few inputs and outputs.

What is the Purpose of Writing Unit Tests

The goal of writing unit tests is to ensure code operates as expected in all conceivable scenarios. As an example, suppose you want to write some unit tests for the function int limitval(int val, int min, int max). This function will do different things based on the arguments passed in. Each unit test you write to test limitval would be responsible for verifying that a particular combination of arguments passed in to the function results in expected behavior. So for your limitval function, a number of unit tests may verify that the same value passed in is returned if val >= min && val <= max, while one may test that an error condition is set and the function doesn't limit val if min > max, while yet another test may ensure that if min == max, val is set equal to min. It is important to test normal cases as well as edge cases (scenarios that happen rarely or should never happen) to ensure your code works as expected in every possible scenario.

Our Unit Test Framework: GoogleTest and GoogleMock

Our unit test framework is built using the GoogleTest and GoogleMock libraries. These libraries provide us with ways to handle simple assertions of equality up to more complex testing with mock and fake classes. We statically link these libraries with our code when running on the the hosted target.

Disclaimer: This is not a complete guide on "how to use GoogleTest/GoogleMock"

While there will be some example code given and short explanations of particularly important features of GoogleTest and GoogleMock, for a complete guide on how the unit test library we use works check out the GoogleTest primer and the Googlemock cookbook.

How to Write Unit Tests?

Lets now walk through typical workflow of creating some unit tests.

  1. Assuming you are creating a new suite of tests for some class in the main repository (code located in the src folder), create a .cpp file in the test directory that mirrors what is present in the src directory whose name matches that of the file you are writing tests for with the suffix Tests (or _tests if the code you are writing tests for is in a file that uses snake case). For example, if you are writing tests for code in the file src/tap/foo/Bar.cpp, create the file test/tap/foo/BarTests.cpp (or test/tap/foo/bar_tests.cpp if the source file is named src/tap/foo/bar.cpp).
  2. In the source test file in the test directory, create a new test by #includeing <gtest/gtest.h> and then creating a TEST. A TEST looks like this:
    TEST(TestSuiteName, functionName_description_of_expected_behavior)
    {
        ...test body...
    }
    
    The first argument to TEST is the test suite name, typically the name of whatever class or collection of code you are testing, and the second argument is a description of the test. If you are testing a particular function, it is often helpful to start the second argument with the name. Both arguments must be syntactically valid C++ class names because these arguments are combined to create a class name.
  3. A single TEST should validate a particular program state (as described above). When writing the body of the test, it is suggested that you first set up the test completely and then "run" it. Consider the following TEST as an example:
    TEST(FrictionWheelRotateCommand, execute_zero_desired_rpm_always_zero)
    {
        // Define any objects necessary to the unit test.
        Drivers d;
        FrictionWheelSubsystemMock fs(&d);
        // Define the object you are testing.
        FrictionWheelRotateCommand fc(&fs, 0);
        // Set up GoogleMock expectations (Wondering what these are? Keep reading).
        EXPECT_CALL(drivers->canRxHandler, removeReceiveHandler).Times(2);
        EXPECT_CALL(drivers->djiMotorTxHandler, removeFromMotorManager).Times(2);
        EXPECT_CALL(fs, setDesiredRpm(0));
    
        // "Run" the test.
        fc.execute();
    }
    
    The above shows the distinction between setting up a unit test and running it. In this example, running it only consists of a single function call, but when your test requires more complex interaction to run it, it pays off to define all your necessary objects and expectations at the top of a test for ease of readability.
  4. Keep writing new TESTs. As long as the name (test suite name + test name) are unique, during compilation all the new TESTs that you created will be added to the GoogleTest test runner. To run the tests, you can do so via the command line or using the VSCode interface. See the bottom of the readme for information about building code.

Some Important GoogleTest and GoogleMock Semantics

Now that you have a general understanding of how to write tests, you may still be wondering about how to use GoogleTest and GoogleMock. For definitive information about how to use these libraries I urge you to get familiar with the documentation provided by the developers of these test environments. Below you will find a small portion of the GoogleTest/GoogleMock API that was used in the above example and is worth mentioning. Note that there is a lot more to these libraries that you should eventually be aware of.

EXPECT_*

You can use the EXPECT_* macros for asserting if something is true, false, or equals something else. See here for further information.

EXPECT_CALL

The EXPECT_CALL macro different than EXPECT_*. It is a GoogleMock macro that allows you to say "I expect that some function in some object will be called some number of times with some arguments passed in to it." If your test following this macro doesn't meet the set expectation, the test will fail. A more complete explanation of when to set expectations is located in this section of the GoogleMock cookbook. You cannot just use this macro with any object or function. Instead, you can only use this macro with a mock class. If you are not familiar with what a mock is at a more general level, I highly recommend reading Embedded C/C++ Unit Testing Basics (the same post I linked above). To create a mock class, refer to this section in the GoogleTest cookbook.

As an example, consider this line of code from the example TEST above:

EXPECT_CALL(fs, setDesiredRpm(0));

Here, you are saying "I expect that in this test, fs's setDesirecRpm function will be called once with the argument of 0. In the example above, fs is an instance of a FrictionWheelSubsystemMock, which has a well defined setDesiredRpm function according to GoogleMock specification. Since the procedure is pretty straight forward for creating a mock, this guide will not cover it. Instead, you may look in test/tap/mock for some example mocks.

ON_CALL

The ON_CALL macro is another useful tool used to interact with mock classes while writing unit tests. Per the GoogleMock cookbook, "ON_CALL defines what happens when a mock method is called, but doesn't imply any expectation on the method being called". I recommend making sure you understand what EXPECT_CALL and ON_CALL do so they will be fully at your disposal when you need them.