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
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 you 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 ruin 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:
attitudeManager::attitudeManager() { }
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:
attitudeManager::attitudeManager() { currentState = &fetchInstructionsMode::getInstance(); }
An now, when we run the unit tests, we will see everything passes! This new test will run everytime you run all the other tests, and eventually when things get merged, Everytime 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.
Add Comment