Test Driven Development (TDD)
What is TDD ?
Tdd is a style of software development in which unit tests are written at the same time that the code is written. The basic workflow is to decide on some functionality, write a unit test that tests that functionality, watch that test fail (because you have not implemented the functionality yet), write the functionality, watch the test pass and move on to the next piece of functionality.
Why do we use TDD ?
In short, because we don't want our planes to crash in fiery explosions. Don’t get us wrong, that will happen, but hopefully you can understand why we’d want to limit that. Unit testing provides a really good way to catch bugs before the code makes it on to the aircraft.
Ok, but why TDD instead of writing the tests after I write the code ?
At the end of the day, it’s all the same to us, as long as the unit tests exist. That being said, TDD is a proven model in the software world -It turns out that many more bugs get caught when tests are written alongside code. So we recommend you use the TDD workflow.
Do I have to write unit tests?
Short answer is yes. Though if your’e writing a driver for a sensor, where you need to interact with the HAL layer, unit testing can be low value, so we won’t require them for anything that low level. That being said, for anything higher level, we won’t merge your code unless it’s been unit tested.
Setting up
To rig your module for unit testing, you’ll have to do a couple things.
Create a test file
Inside AutoPilot/Test/Src/**Some_Directory_for_classification**, create a Test_YOURMODULENAME.cpp file.
To get started, at the minimum, you’ll need
#include <gtest/gtest.h> #include “YOURMODULENAME.h” using namespace std; using ::testing::Test;
Feel free to take a look at the other Test files to see what they consist of.
Tell Cmake about your file.
Open AutoPilot/CmakeLists.txt and scroll down to the unit testing section.
In the include_directories section, make sure the path to the directory containing YOURMODULENAME.hpp is there. If it isn’t, add it.
Keep scrolling down, you’ll see a series of subsections, (######### Attitude manager fsm, for example). Each of those subsections produces a single exe. Figure out whether your module fits within one of those sections or whether you need to create a new subsection (do this by feel, you’ll probably be right).
If it fits within an existing subsection, add your MYMODULE.cpp file to the ***SOURCES variable and add your Test_MYMODULE.cpp file to the ***UNIT_TEST_SOURCES variable
If you make your own subsection, just copy one of the existing subsections, gut all of the existing files and add your stuff the same way as in i, making sure to name the section, the variables and the exe something meaningful.
Your’e done! if you run the build script with -c -t, ( -c only for the first time), it should pull in and execute your tests.
Getting started
I’m going to do this by example. Take the development of the attitude Manager state machine. This piece of software is responsible for controlling the aircraft’s roll, pitch and yaw to get us to the the commanded attitude,
The first thing your’e going to want to do, (after you’ve designed a general architecture for your module), is figure out how a user would interact with your module. That means deciding on what api to expose and writing out the interface. In the case of the attitude manager state machine, we’ve decided to go with an architecture where the user needs some way of getting the state machine to “run”, they need some way to read the current state of the state machine, and they needs some way of setting the state machine to a certain state. That’s it! Notice how simple that is ? Although the state machine internals will eventually be fairly complicated, it is your responsibility as a designer to make life as easy as possible for the person interacting with it. So here is what the interface looks like right now (attitudeManager.hpp):
#pragma once
#include "attitudeStateManager.hpp"
class attitudeManager
{
public:
attitudeManager();
inline attitudeState* getCurrentState() const {return currentState;}
void execute();
void setState(attitudeState& newState);
private:
attitudeState* currentState;
};
These are just prototypes and there are even undefined types, that’s OK. Let’s write a test.
Writing your first unit test
Ok, so where do we start ? At the beginning. We know from our architecture that the state machine needs to be in the “fetchInstructionsMode” state. So the first unit test should just be creating an attitudeManager object and checking to see wether it’s in this state right after it’s created. That looks like this:
TEST(AttitudeManagerFSM, InitialStateIsFetchInstructions) {
/***********************SETUP***********************/
attitudeManager attMng;
/********************DEPENDENCIES*******************/
/********************STEPTHROUGH********************/
/**********************ASSERTS**********************/
ASSERT_EQ(*(attMng.getCurrentState()), fetchInstructionsMode::getInstance());
}
(Don’t be bothered of pointer de-referencing or the calling of the getInstance method, these are just the way we chose to implement things as part of our architecture. How you choose to do design module is up to you)
So there’s a very simple test. If you run the unit tests (with ./Tools/build.bash -t), you’l notice things won’t compile. Makes sense, since we don’t actually have any code implemented. So let’s write the constructor of this class and the getCurrentState methods:
In attitudeManager.cpp:
This time, when you run the unit tests, you’l notice it fails. So now let’s write the code to allow it to pass:
An now, when we run the unit tests, we will see everything passes! This new test will run every time you run all the other tests, and eventually when things get merged, every time someone pushes to our repo. What it’s done is give us insurance forever that whatever happens, attitudeManager objects always start in the “fetchInstructionsmode” state.
Writing your second test
Ok so we’re starting in the correct state, great. The next piece of functionality we’d like to have is for the state machine to flow between states correctly. That is, when the state machine is in the “sensorFusionMode” state, running execute once should take it to the “PidLoopMode” state. Here is the test that expresses that:
You should be able to see what’s going on: we just set the state machine to the sensorFusionMode state, run execute once and check to see that we in fact reached the PID state.