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:

  1. Creating and setting up the unit test file

  2. Updating the CMakeLists.txt file

  3. Adding fake functions

  4. 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:

  1. 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.

  2. 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 file Test_Addition.cpp

    1. The optional_additional_details are short acronyms that better describe what the file is testing. An example is fsm (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.

  3. Within the newly created C++ file, copy the following template and paste it in:

    1. The dependencies are files and libraries that you include to help your code run. (ex. #include "pathStateClasses.hpp" and #include <string>

    2. 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.

    3. The test fixtures are where we will declare fake functions (more on this later)

    4. The custom fakes will almost never be used, but it allows us to create custom fake functions (more on this later)

    5. The unit tests section will compose of all of our unit tests

    6. <gtest/gtest.h> allows us to use the Google Test API

    7. "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:

  1. Ask it to create a brand new executable file for your unit test

  2. 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.

  1. 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):

  2. Add your Inc folder to the include_directories property (read more about this property here).

    1. Note that this may look different depending on when you are modifying the CMakeLists.txt file, the include_directories property may look different than what is shown below

    2. The ${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:

  1. Add the new relevant source .cpp files to the MATH_STUFF_MODULES_SOURCES section. This would include our multiplication.cpp file.

  2. Add the new relevant unit test .cpp files to the MATH_STUFF_MODULES_UNIT_TEST_SOURCES section. This would include our Test_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_SOURCESsection in the CMakeLists.txtfile. 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:

Ew… Linker error

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 section

  • TEST_NAME → Name of our unit test

  • SETUP → 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: