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.
- 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 thetest
directory that mirrors what is present in thesrc
directory whose name matches that of the file you are writing tests for with the suffixTests
(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 filesrc/tap/foo/Bar.cpp
, create the filetest/tap/foo/BarTests.cpp
(ortest/tap/foo/bar_tests.cpp
if the source file is namedsrc/tap/foo/bar.cpp
). - In the source test file in the
test
directory, create a new test by#include
ing<gtest/gtest.h>
and then creating aTEST
. ATEST
looks like this:
The first argument toTEST(TestSuiteName, functionName_description_of_expected_behavior) { ...test body... }
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. - 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 followingTEST
as an example:
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.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(); }
- Keep writing new
TEST
s. As long as the name (test suite name + test name) are unique, during compilation all the newTEST
s 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.