Update (September 9, 2017): The Ceedling interface has changed a bit. This article should still work for you, but wherever you see rake
used as a command below, you'll want to use ceedling
instead. [See this other post for more details on the change from rake to ceedling][0].
Maybe you've heard of Test-Driven Development (TDD), and maybe you've even thought it seemed like a reasonable idea. If you haven't tried TDD yet though, you really should. Here's some help to get you started right now.
In this article we'll set up a unit test framework for a C application and then start using it for TDD.
TDD requires that tests can be created and run easily. You need a unit test framework to do this. The way to get up and running right now is to use a test framework called Ceedling.
To review, the premise of TDD is that we use the creation of unit tests to incrementally drive the development of the software. The steps look like:
- Write a test, run it and watch it fail.
- Implement the code to make the test pass.
- Re-run the test and watch it pass.
- Refactor.
- Repeat.
This allows us to be very clear about what the code is to do, because we've defined every behavior in a test.
Since we're creating and running tests so frequently, we need these operations to be easy and fast. Ceedling will allow us to quickly add new modules and tests.
Ceedling also builds and runs tests on the host PC, so when working on an embedded project we don't have to waste time downloading to the target. In fact we might not even need hardware at all to test most of our application, which is another benefit entirely.
1. Install Ceedling
Ceedling requires Ruby to run and uses GCC to build each test.
- Install Ruby (Windows Installer, Other Instructions). Be sure that the Ruby bin folder is in your path, e.g.
C:\Ruby21\bin
- Install Ceedling with the Ruby "gem" tool with the command:
gem install ceedling
- If you're on Windows, you'll likely need to install GCC. I recommend installing with Cygwin. When installing be sure to select the "Devel" packages to have GCC installed. Then put the Cygwin bin folder in your path, e.g.
C:\cygwin64\bin
2. Create a Project
Use the ceedling new <projectName>
command to create a new project:
$ ceedling new MyProject
create MyProject/vendor/ceedling/docs/CeedlingPacket.pdf
create MyProject/vendor/ceedling/docs/CExceptionSummary.pdf
...
create MyProject/vendor/ceedling/vendor/unity/src/unity.h
create MyProject/vendor/ceedling/vendor/unity/src/unity_internals.h
create MyProject/project.yml
create MyProject/rakefile.rb
Project 'MyProject' created!
- Tool documentation is located in vendor/ceedling/docs
- Execute 'rake -T' to view available test & build tasks
This generates a project tree and the configuration files needed to use Ceedling. Project creation only needs to be done once when starting a project. Important among the created folders are:
- src: Where all of our source files will go.
- build: Contains anything generated by Ceedling during the build.
- test: Where our unit test files will go.
Now we have a project in the MyProject
folder. Note the instructions from the Ceedling output when we created the project -- we can use rake -T
to show us how to use it:
$ cd MyProject
$ rake -T
rake clean # Delete all build artifacts and temporar...
rake clobber # Delete all generated files (and build a...
rake environment # List all configured environment variables
rake files:header # List all collected header files
rake files:source # List all collected source files
rake files:test # List all collected test files
rake logging # Enable logging
rake module:create[module_path] # Generate module (source, header and tes...
rake module:destroy[module_path] # Destroy module (source, header and test...
rake paths:source # List all collected source paths
rake paths:support # List all collected support paths
rake paths:test # List all collected test paths
rake summary # Execute plugin result summaries (no bui...
rake test:* # Run single test ([*] real test or sourc...
rake test:all # Run all unit tests
rake test:delta # Run tests for changed files
rake test:path[dir] # Run tests whose test path contains [dir...
rake test:pattern[regex] # Run tests by matching regular expressio...
rake verbosity[level] # Set verbose output (silent:[0] - obnoxi...
rake version # Display build environment version info
Ceedling is built on Rake, which is Ruby's dependency-based build tool. Ceedling and its unit test operations are implemented as rake tasks. These are the commands we will use to build and run our tests.
Note: Since we're using Rake we can extend our build capabilities by adding our own rake tasks to do whatever we would like, e.g. generating documentation or downloading to the target.
3. Create a Module
Now it's time to write some code. Imagine we're building a car and we want to build a module to implement the lighting system. We create a module like this:
$ rake module:create[lights]
Generating 'lights'...
mkdir -p ./test/.
mkdir -p ./src/.
File ./test/./test_lights.c created
File ./src/./lights.c created
File ./src/./lights.h created
This creates three files: lights.c
to implement our module, lights.h
to define the public interface and a test file where we can put the unit tests for it. These files are automatically created in the correct folders of our tree.
Note: We could also have provided a deeper path in which to create the module, e.g. rake module:create[electrical/lights]
.
At this point, we can try running our unit tests:
$ rake test:all
Test 'test_lights.c'
--------------------
Generating runner for test_lights.c...
Compiling test_lights_runner.c...
Compiling test_lights.c...
Compiling unity.c...
Compiling lights.c...
Compiling cmock.c...
Linking test_lights.out...
Running test_lights.out...
-----------
TEST OUTPUT
-----------
[test_lights.c]
* ""
--------------------
IGNORED TEST SUMMARY
--------------------
[test_lights.c]
Test: test_module_generator_needs_to_be_implemented
At line (14): "Implement me!"
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 1
PASSED: 0
FAILED: 0
IGNORED: 1
This tells us that a single test was run and it was ignored.
The module:create
task has used a template to create the test file. Inside the test file test_lights.c
is a single test named test_module_generator_needs_to_be_implemented
which uses a special ignore directive to tell Ceedling to ignore this test. The function looks like this:
void test_module_generator_needs_to_be_implemented(void)
{
TEST_IGNORE_MESSAGE("Implement me!");
}
This is the convention for unit tests which Ceedling. Test files have names that start with test_
and they go in the test
folder. Within each of these files, unit tests are functions whose names start with test_
.
Also in the test file are the setUp()
and tearDown()
functions. These functions are run before and after each of the test functions in the test file. These functions are yours to use if you need them.
4. Implement a Feature
Now that we have a module for the lights, it's time to add some functionality. In the test-driven way, we'll first add a test that describes some desired behavior. Say we want this behavior:
When the headlight switch is off, then the headlights are off.
In this case we're going to replace test_module_generator_needs_to_be_implemented()
with a new test function:
void test_WhenTheHeadlightSwitchIsOff_ThenTheHeadLightsAreOff(void)
{
// When the headlight switch is off...
lights_SetHeadlightSwitchOff();
// then the headlights are off.
TEST_ASSERT_EQUAL(false, lights_AreHeadlightsOn());
}
What we've done here is define two new functions to implement in the lights
module, lights_SetHeadlightSwitchOff()
and lights_AreHeadlightsOn()
. We call the first function to turn the lights off, and then call the second to confirm the state of the headlights.
The TEST_ASSERT_EQUAL()
macro is what we use to verify that the value returned from lights_AreHeadlightsOn()
is the expected value (false
). This is one of the many macros available for comparing various types, all of which are explained in the Unity documentation.
Now we can run our tests, but obviously this is going to fail with all kinds of compilation errors, because these functions don't even exist yet.
$ rake test:all
Test 'test_lights.c'
--------------------
Generating runner for test_lights.c...
Compiling test_lights_runner.c...
Compiling test_lights.c...
...
> Shell executed command:
'gcc.exe -I"test" -I"test/support" -I"src" -I"MyProject/vendor/ceedling/vendor/unity/src
" -I"MyP
roject/vendor/ceedling/vendor/cmock/src" -I"build/test/mocks" -DTEST -DGNU_COMPI
LER -g -c "test/test_lights.c" -o "build/test/out/test_lights.o"'
> And exited with status: [1].
rake aborted!
...
Tasks: TOP => build/test/results/test_lights.pass => build/test/out/test_lights.
out => build/test/out/test_lights.o
(See full trace by running task with --trace)
--------------------
OVERALL TEST SUMMARY
--------------------
No tests executed.
The next step is to implement the minimum functionality to pass our test. Here is the interface defined in lights.h:
#ifndef lights_H
#define lights_H
#include <stdbool.h>
void lights_SetHeadlightSwitchOff(void);
bool lights_AreHeadlightsOn(void);
#endif // lights_H
And the implementation in lights.c:
#include "lights.h"
#include <stdbool.h>
void lights_SetHeadlightSwitchOff(void)
{
}
bool lights_AreHeadlightsOn(void)
{
return false;
}
We can then run our test and see that it passes:
$ rake test:all
Test 'test_lights.c'
--------------------
Compiling test_lights.c...
Compiling lights.c...
Linking test_lights.out...
Running test_lights.out...
-----------
TEST OUTPUT
-----------
[test_lights.c]
* ""
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 1
PASSED: 1
FAILED: 0
IGNORED: 0
This may sort of feel like we're cheating, since lights_SetHeadlightSwitchOff()
doesn't actually do anything and lights_AreHeadlightsOn()
simply returns false
, but as we add more tests we'll continue to add functionality.
5. Repeat
We can now continue adding features to the module until it does every thing that we need it to -- by adding tests and then writing the code to make them pass. For example might want to implement this behavior:
When the headlight switch is on, then the headlights are on.
So, we create an additional test:
void test_WhenTheHeadlightSwitchIsOn_ThenTheHeadLightsAreOn(void)
{
// When the headlight switch is on...
lights_SetHeadlightSwitchOn();
// then the headlights are on.
TEST_ASSERT_EQUAL(true, lights_AreHeadlightsOn());
}
If we run this test it will fail because lights_SetHeadlightSwitchOn()
doesn't exist yet, but when we update lights.h:
#ifndef lights_H
#define lights_H
#include <stdbool.h>
void lights_SetHeadlightSwitchOff(void);
void lights_SetHeadlightSwitchOff(void);
bool lights_AreHeadlightsOn(void);
#endif // lights_H
And add the implementation in lights.c:
#include "lights.h"
#include <stdbool.h>
static bool areLightsOn = false;
void lights_SetHeadlightSwitchOff(void)
{
areLightsOn = false;
}
void lights_SetHeadlightSwitchOn(void)
{
areLightsOn = true;
}
bool lights_AreHeadlightsOn(void)
{
return areLightsOn;
}
Then we can run our tests and watch them both pass:
$ rake test:all
Test 'test_lights.c'
--------------------
Generating runner for test_lights.c...
Compiling test_lights_runner.c...
Compiling test_lights.c...
Linking test_lights.out...
Running test_lights.out...
-----------
TEST OUTPUT
-----------
[test_lights.c]
+ ""
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 2
PASSED: 2
FAILED: 0
IGNORED: 0
Conclusion
If you haven't tried TDD in a C project yet, I recommend using Ceedling to give it a shot. This has been a trivial example, but it demonstrates how to get started.
In order to do TDD, you need to be able to create and run tests easily since you'll be doing this all the time. Ceedling is a good way to get this functionality in C so that TDD can be used for embedded development.
Additionally, Ceedling offers the ability to create mock module interfaces using CMock. This is going to be a valuable tool for writing more substantial unit tests, and especially for mocking out embedded hardware components.
The source used in this example is available on GitHub.
Save this article for later
The article is pretty long and technical. If you're not ready to use all of it right now, get everything in this article (and more) in my downloadable "how to" guide.Sign up to get it here.