Tutorial
Unit testing is an essential part of all code development, and is even more important when developing safety critical applications such as ZeroPilot. While other docs on our Confluence explain how to set up Unit Testing, this doc will explain how to actually write a unit test using Google Test through an example.
The following sections will be covered:
Creating and setting up the unit test file
Updating the
CMakeLists.txt
fileAdding fake functions
Writing a test
The Example
Let’s say we created a new directory in ~/ZeroPilot-SW/Autopilot
called MathStuff
. In this folder, we have the following files:
~/MathStuff/Src/addition.cpp
- the file contains a function that adds three numbers~/MathStuff/Src/numbers.cpp
- the file contains two functions that return integers~/MathStuff/Inc/addition.hpp
~/MathStuff/Inc/numbers.hpp
addition.hpp
#ifndef ADDITION_HPP
#define ADDITION_HPP
#include "SomeHardwareFunctions.hpp"
int addNumbers(int num1);
#endif
addition.cpp
int addNumbers(int num1) {
hardwareFunction(); // Calls a function that depends on hardware (added for the fake functions section :). It returns void
return num1 + getSecondNum() + getThirdNum();
}
numbers.hpp
#ifndef NUMBERS_HPP
#define NUMBERS_HPP
int getSecondNum(void);
int getThirdNum(void);
#endif
numbers.cpp
You want to test the addNumbers()
function in addition.cpp
to see if the returned value is num1 + 3
.
Creating and Setting up the Unit Test File
The first step is to create the file that will run the unit tests.
All of our test files are stored in directory ~/ZeroPilot-SW/Autopilot/Test/Src
. At the time of writing, this directory contains subdirectories such as ~/PathManager
and ~/AttitudeManager
to organize unit testing files.
Here are the steps you will follow:
Pick the subdirectory in
~/ZeroPilot-SW/Autopilot/Test/Src
that best matches the code you are testing (ex. if the code you are testing lies in the Path Manager, put the unit test file in the~/PathManager
directory). If the code you have written does not fit into any of the subdirectories, feel free to make a new one . For this example, we will make a new directory called~/ZeroPilot-SW/Autopilot/Test/Src/MathStuff
since our files are in a brand new directory.Within your chosen/created subdirectory, create a file with the following naming scheme:
Test_{Module_you_are_testing}_{optional_additional_details}.cpp
. Using our naming scheme, we will name our test fileTest_Addition.cpp
The
optional_additional_details
are short acronyms that better describe what the file is testing. An example isfsm
(finite state machine) that tests whether the states in our state machines transition as expected. Tbh, you don’t really need to worry about this.
Within the newly created C++ file, copy the following template and paste it in:
The dependencies are files and libraries that you include to help your code run. (ex.
#include "pathStateClasses.hpp"
and#include <string>
The definitions contains any file-scope constants you need in your code. If you are going to be using numeral literals, it’s better to declare them as a constant using
constexpr
or#define
to improve readability.The test fixtures are where we will declare fake functions (more on this later)
The custom fakes will almost never be used, but it allows us to create custom fake functions (more on this later)
The unit tests section will compose of all of our unit tests
<gtest/gtest.h>
allows us to use the Google Test API"fff.h"
is an open source program that allows us to make fake functions (more on this later)
Updating the CMakeLists.txt File
With our unit test file Test_Addition.cpp
created, we must make sure when building unit tests using ~/ZeroPilot-SW/Autopilot/Tools/build.bash -t
that the file is compiled and executed. To do this, we will modify the ~/ZeroPilot-SW/Autopilot/CMakeLists.txt
file.
For context, the CMakeLists.txt
is responsible for compiling and packaging our code into a set of executables that a computer can run. In the case of unit tests, we ask the CMakeLists.txt
file to create a set of executables (we decide how the executables are made – read more to learn ) and run them.
There are two ways you can really go about adding your unit tests to the CMakeLists.txt
file:
Ask it to create a brand new executable file for your unit test
Append your unit test to an existing executable
We will discuss these methods later in this section!
Letting CMake know your files exist
NOTE: This sub-section only applies if you have created a brand new directory in ~/ZeroPilot-SW/Autopilot
. Since we created a new directory called MathStuffs
, we will need to implement these steps.
Locate the section of the
CMakeLists.txt
file responsible for building unit tests. It starts at the following line (at the time of writing, this was around Line 100):Add your
Inc
folder to theinclude_directories
property (read more about this property here).Note that this may look different depending on when you are modifying the
CMakeLists.txt
file, theinclude_directories
property may look different than what is shown belowThe
${CMAKE_CURRENT_SOURCE_DIR}
is substituted with the path between your root directory (~/
) and the ZeroPilot Autopilot folder (~/ZeroPilot-SW/Autopilot
). This allows you to compile the ZeroPilot-SW repository anywhere in your computer's directory
And now we are done. CMake knows our folder exists and can find it when we build our unit tests.
Creating a new unit test executable
If you want to create a new unit test executable, you will need to follow the following template:
NAME_OF_YOUR_TEST
is just a comment that summarizes what the executable tests. Examples we have used in the past include “ATTITUDE_MANAGER_FSM”, “ATTITUDE_MANAGER_MODULES”, etc. For our example, we will use “Math Stuff Modules”.NAME_OF_YOUR_TEST_SOURCES
is the name referring to the set of .cpp files that should be compiled into your executable. We will name this “MATH_STUFF_MODULES_SOURCES”NAME_OF_YOUR_TEST_UNIT_TEST_SOURCES
is the name referring to the set of .cpp unit test files that will test your code. We will name this “MATH_STUFF_MODULES_UNIT_TEST_SOURCES”NAME_OF_EXECUTABLE
is the name of the executable that will run your unit tests. We will call it “mathStuffModules” for our example.
For our example, our end result should be the following:
Adding onto an existing unit test executable
Say we have this existing config in our CMakeLists.txt
file:
Now, say we want to include one more test file to check a multiplication function that is done in ~/MathStuff/Src/multiplication.cpp
. We will test this file in a new unit test file called ~/Test/Src/MathStuff/Test_Multiplication.cpp
. We can add it to the existing CMake code using the following two steps:
Add the new relevant source .cpp files to the
MATH_STUFF_MODULES_SOURCES
section. This would include ourmultiplication.cpp
file.Add the new relevant unit test .cpp files to the
MATH_STUFF_MODULES_UNIT_TEST_SOURCES
section. This would include ourTest_Multiplication.cpp
file.
The end result is the following
Fake Functions
When you test your code, there are times when you simply cannot run certain functions because of their dependence on hardware, or because you don’t have access to the files in your NAME_OF_YOUR_TEST_SOURCES
section in the CMakeLists.txt
file. When these situations arise, we use fake functions (this link explains fake functions).
In simple terms, a fake function simplifies the implementation of some of our incompatible/incomplete/inaccessible functions to one that is usable by our unit tests.
To make fake functions, we will use the following syntax in our test file under Test Fixtures:
TEST_GROUP_NAME
the name of the group of tests unit testing our module. For our case, it will be called “MathStuffFunctions”. You’ll see this come up more in the next section.
In our implementation of addition.cpp
, we called a function called hardwareFuncion()
that is inaccessible to us in the unit tests (because hardware-level functions cannot be tested on a computer). So, we need to make a fake for it. Here is the end result:
Error Codes you may Encounter
Undefined Symbol Error
As you run your unit tests, chances are you will run into an error like this:
What this error is telling you is you forgot to make a fake function for AutoSteer_Init()
Of course, this is a specific case, but the name of the function you forgot to fake will be in the place of AutoSteer_Init()
.
Duplicate Symbol Error
A weird/useful feature of fake functions are you only need to declare them once for an executable. So, if in your CMakeLists.txt
you have the code pasted below, if the fake functions are declared in testFile1.cpp
, you don’t need to redeclare them in any of the other test files.
If you do redeclare fake functions in another file (say testFile3.cpp
) within the same executable, you will come across this error:
If you come across this error, the fix is simple: delete your redeclaration of the fake function.
Writing a Test
The Structure
And voila, now we are at the stage where we can write our tests! You may write multiple tests in your testing file and each one will follow the same template:
TEST_GROUP_NAME
→ This is what we saw in the Fake Functions sectionTEST_NAME
→ Name of our unit testSETUP → This is where we initialize some key variables
DEPENDENCIES → Include any dependencies that are needed. You will almost never use this section and you are free to delete it if needed.
STEPTHROUGH → This is where we call the functions to execute the program we want to test
ASSERTS → This is where we verify that the state of our program is what we expect it to be
Checking Program State
At the end of each test, we run a few checks to see if the final state of our program/returned value is what we expect it to be. To do this, we use EXPECT_EQ
and EXPECT_NEAR
in the Google Test API.
The syntax for both are the following:
If the check fails, the test fails.
Example
For our example, we will test the addition with three tests to see if our returned value is num1 + 3
. Here is our test file: