Add unit tests to your current project with Ceedling

Hey -- this article is pretty long and technical. Get everything in here (and more) in my downloadable how-to guide.

Updated (September 9, 2017): Updated this post to use ceedling commands instead of rake commands based on recent changes to Ceedling. See this post for more details.

You want to try unit testing your embedded software but there's a problem -- you've got an existing project and a whole lot of code already written. Maybe it's even embedded legacy code.

You can build, load and run your application just fine from your IDE. But where do the tests go and how do you run them? And what does it mean for your existing project?

Well, it turns out that you can add Ceedling to your project and run it independently from your IDE and release build.

The test code (and framework) will be isolated from your production code and won't interfere with your release builds. This allows you to experiment with unit testing... without messing with the rest of your team.

Using Ceedling like this is the quickest way to get Unity and CMock set up to test your code. Don't worry about integrating with Eclipse (or whatever IDE you're using) yet -- just get your tests running from the command line first.

Typically, you have some source files that your IDE compiles into a release build that can be run on the target:

To add unit testing support to this project you can set up Ceedling to run in parallel from the same source files. You write the tests in separate test files, and then Ceedling uses GCC (instead of your target compiler) to build tests that you run on your host PC.

These test binaries are built in their own build folder so they don't interfere with your existing release configuration. The tests are executed independently from your IDE by running just a few simple commands from the command prompt.

Start with an existing project

In this exercise I'm using an example project for the TI Tiva C Series LaunchPad development board. It's a simple little board with an ARM Cortex-M4.

The example is based on TI's "blinky" project, which just blinks an LED. We'll be modifying this code as we progress, but you can find the starting point for this exercise here on GitHub.

The project source consists of a main loop in blinky.c and an LED driver in led.c. The main() loop just calls into the LED driver with led_turn_on() and led_turn_off() to do the blinking:

This project also has all of the "junk" in it that you'd expect when using an IDE. In fact it has project settings and build folders for Keil, IAR or Code Composer Studio (TI's own free Eclipse-based IDE). Here's a simplfied view of what this mess looks like (some files are omitted for brevity):

Notice here that the source files are all mixed-in with other types of files here. I am not a fan of this nonsense... but this is pretty common especially with IDEs from embedded vendors. I much prefer the convention of putting all the source in it's own folder. This is cleaner, makes the project tree easier to understand, and also makes it easier to configure Ceedling. We'll revisit this a little later.

For now though -- since we're just getting started -- we'll leave everything as it is here and install Ceedling along side of it. That way we won't break anything in this existing project.

Install and configure Ceedling

Before you can add Ceedling to your project you'll need to install Ceedling on your system. This also requires installing Ruby and GCC. If you need help with this, you can find some more details about installing Ceedling in my free guide.

Once Ruby, GCC and Ceedling are installed, the first step is to install Ceedling into the existing project. This is done from the command line.

WARNING: This is going to dump a bunch files into your project. As with any project changes make sure you've got a backup somewhere -- preferably in source control.

Ceedling has a new command for creating "new" projects. It's not obvious, but you can also use new to install Ceedling into an existing project. Let's check it out.

So from the command line, go in to the parent folder of your project. In this case, it's the folder above our blinky project. From there you'll run ceedling new blinky (since our project is in a folder named "blinky"). This will install Ceedling into your existing project folder by creating some new files and folders:

projects> ceedling new blinky
Welcome to Ceedling!
      create  blinky/vendor/ceedling/docs/CeedlingPacket.pdf
      create  blinky/vendor/ceedling/docs/CExceptionSummary.pdf
      ...
      create  blinky/vendor/ceedling/vendor/unity/src/unity_internals.h
      create  blinky/project.yml

Project 'blinky' created!
- Tool documentation is located in vendor/ceedling/docs
- Execute 'ceedling help' to view available test & build tasks

Now you can drop into the blinky project folder and run Ceedling with ceedling test:all. We've haven't created any tests yet though, so no tests are actually going to execute:

projects> cd blinky

projects\blinky> ceedling test:all

--------------------
OVERALL TEST SUMMARY
--------------------

No tests executed.

Ceedling just added these files and folders to the project:

  • build: This is where the tests are built.
  • src: This is where Ceedling expects to find your source code.
  • test: This is where your unit tests will go.
  • vendor: This is where the Ceedling source files are.
  • project.yml: This is the configuration file for Ceedling.

Notice that Ceedling expects the source code to be in the src folder. It's time to move the source code into the src folder. For blinky, this means moving blinky.c, led.h and led.c. Unfortunately this might require some changes in your IDE to handle this new folder in the project tree, but this is the best way to set up your project.

Note that you can have any folder tree that you want below src, so you can move any existing source folders in there as well.

Ceedling Tip: You can confirm that Ceedling knows about your source files by running ceedling files:source:

projects\blinky> ceedling files:source
source files:
 - src/blinky.c
 - src/led.c
file count: 2

Yeah! Now Ceedling is installed in our project and ready to go.

Create a new test file

Now that Ceedling is installed, it's time to add some tests. The LED driver (led.c) is a good candidate here because it's an isolated module. Before we can add tests we'll need a new test file to put the them in.

Ceedling makes it easy to create the test files for existing modules with its module:create command. Typically this command creates a .c, .h and a test file for a new source module. If a file already exists though, the file is left untouched. This means we can use it to easily create a test file for led.c:

projects\blinky> ceedling module:create[led]
Generating 'led'...
mkdir -p ./test/.
mkdir -p ./src/.
File ./test/./test_led.c created
File ./src/./led.c already exists!
File ./src/./led.h already exists!

The test file it created is test/test_led.c. This is built from a template that includes the header files and the setUp() and tearDown() functions needed by any test. This saves us the time of having to manually copy/paste/edit this from another test file.

Getting it to build

Now that we have a test file for our LED module, we need to get it to build. Here's where the real fun begins! In this step we're going to chase down a bunch of errors as we try to find the "seams" of the LED module so that we can test it isolation.

This is going to involve setting up some mocks and configuring Ceedling. We're just going to read the error messages and use them to figure out what needs to be fixed at each stage.

Adding more source folders

After adding our first test file test_led.c, if we try to run the tests we get our first error:

projects\blinky> ceedling test:all


Test 'test_led.c'
-----------------
Generating runner for test_led.c...
Compiling test_led_runner.c...
Compiling test_led.c...
Compiling unity.c...
Compiling led.c...
src/led.c:5:27: fatal error: inc/hw_memmap.h: No such file or directory
 #include "inc/hw_memmap.h"
                           ^

If we take a look at led.c it includes a couple files from our processor library: hw_memmap.h and gpio.h. These are driver files provided by TI for controlling the GPIO:

#include "led.h"

#include <stdint.h>
#include <stdbool.h>
#include "inc/hw_memmap.h"
#include "driverlib/gpio.h"

void led_turn_on(void)
{
  GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_2, GPIO_PIN_2);
}

void led_turn_off(void)
{
  GPIOPinWrite(GPIO_PORTF_BASE, GPIO_PIN_2, 0);
}

When I installed TivaWare, these files were installed to C:\ti\TivaWare_C_Series-2.1.2.111 but Ceedling thinks that all of our source files are in the src folder. We need to tell Ceedling how to look in this other folder for source as well.

Ceedling is configured in the project.yml file. This is a YAML file that Ceedling loads each time it is run. In project.yml there is a section for :paths: which includes settings for :test:, :source:, and :support:. You add another source path (like C:\ti\TivaWare_C_Series-2.1.2.111) by adding another path to the :source: list:

:paths:
  :test:
    - +:test/**
    - -:test/support
  :source:
    - src/**
    - C:\ti\TivaWare_C_Series-2.1.2.111  # This is the new source path.
  :support:
    - test/support

Mocking hardware drivers from the header files

With the source path for the TI drivers set, we can try to run Ceedling again:

projects\blinky> ceedling test:all


Test 'test_led.c'
-----------------
Generating runner for test_led.c...
Compiling test_led_runner.c...
Compiling test_led.c...
Compiling unity.c...
Compiling led.c...
Compiling cmock.c...
Linking test_led.out...
build/test/out/led.o: In function `led_turn_on':
projects/blinky/src/led.c:10: undefined reference to `GPIOPinWrite'

Now that Ceedling can find driverlib/gpio.h, it knows that it needs to link in a GPIOPinWrite function. Now, we're not going use the real function since we're running on the host PC. So we need to mock it. We can mock all of the functions in gpio.h in our test by adding #include "mock_gpio.h" to test_led.c:

#include "unity.h"
#include "led.h"

#include "mock_gpio.h"  // This will mock the functions in driverlib/gpio.h.

void setUp(void)
{
}

void tearDown(void)
{
}

void test_module_generator_needs_to_be_implemented(void)
{
    TEST_IGNORE_MESSAGE("Implement me!");
}

CMock won't do paths to header files

Now we're getting somewhere! Let's try running Ceedling again:

projects\blinky> ceedling test:all


Test 'test_led.c'
-----------------
ERROR: Found no file 'gpio.h' in search paths.
rake aborted!

Oh, so Ceedling can't find driverlib/gpio.h so that it can mock it. Remember how we included mock_gpio.h in the test? Well Ceedling is looking in all of its configured source folders for gpio.h, not driverlib/gpio.h. We need add the driverlib folder to the source paths so that it can find gpio.h in there:

:paths:
  :test:
    - +:test/**
    - -:test/support
  :source:
    - src/**
    - C:\ti\TivaWare_C_Series-2.1.2.111
    - C:\ti\TivaWare_C_Series-2.1.2.111\driverlib  # Now we can find gpio.h.
  :support:
    - test/support

Including other header files in our mocks

What will the next error be?? Running the tests again gives us this one:

projects\blinky> ceedling test:all


Test 'test_led.c'
-----------------
Creating mock for gpio...
WARNING: No function prototypes found!
Generating runner for test_led.c...
Compiling test_led_runner.c...
In file included from build/test/mocks/mock_gpio.h:5:0,
                 from build/test/runners/test_led_runner.c:30:
C:/ti/TivaWare_C_Series-2.1.2.111/driverlib/gpio.h:153:50: error: unknown type name 'bool'
 extern uint32_t GPIOIntStatus(uint32_t ui32Port, bool bMasked);

Okay. So this is a problem with using off-the-shelf code from somewhere else (thank you TI). These TivaWare driver files (like gpio.h) are set up strangely. Even though gpio.h needs stdbool.h and stdint.h it doesn't actually #include them. As the user, you're supposed to include them in your source file before including gpio.h.

Unfortunately this means we need to include stdbool.h and stdint.h in our auto-generated mock files. Fortunately Ceedling has a setting for this in the :cmock: section of project.yml. We can add an :includes: setting like this:

:cmock:
  :mock_prefix: mock_
  :when_no_prototypes: :warn
  :enforce_strict_ordering: TRUE
  :plugins:
    - :ignore
    - :callback
  :treat_as:
    uint8:    HEX8
    uint16:   HEX16
    uint32:   UINT32
    int8:     INT8
    bool:     UINT8
  :includes:        # This will add these includes to each mock.
    - <stdbool.h>
    - <stdint.h>

Enabling mocks for extern-ed function prototypes.

With those include files added, let's run Ceedling again and get our next error:

projects\blinky> ceedling test:all


Test 'test_led.c'
-----------------
Creating mock for gpio...
WARNING: No function prototypes found!
Generating runner for test_led.c...
Compiling test_led_runner.c...
Compiling test_led.c...
Compiling mock_gpio.c...
Compiling unity.c...
Compiling led.c...
Compiling cmock.c...
Linking test_led.out...
build/test/out/led.o: In function `led_turn_on':
projects/blinky/src/led.c:10: undefined reference to `GPIOPinWrite'

Hmmm... we can't find GPIOPinWrite again. If we take a closer look, we can see a WARNING: No function prototypes found! message when trying to create the mock for gpio.h. Again, this Tiva library is strange -- this time because all the function prototypes in the header files are extern-ed:

extern void GPIOPinWrite(uint32_t ui32Port, uint8_t ui8Pins, uint8_t ui8Val);

By default Ceedling/CMock won't mock functions labeled extern. We need to tell CMock to mock these functions by adding the :treat_externs: setting:

:cmock:
  :mock_prefix: mock_
  :when_no_prototypes: :warn
  :enforce_strict_ordering: TRUE
  :plugins:
    - :ignore
    - :callback
  :treat_as:
    uint8:    HEX8
    uint16:   HEX16
    uint32:   UINT32
    int8:     INT8
    bool:     UINT8
  :includes:
    - <stdbool.h>
    - <stdint.h>
  :treat_externs: :include  # Now the extern-ed functions will be mocked.

Hooray! Now our test will finally build, and we can actually run the tests:

projects\blinky> ceedling test:all


Test 'test_led.c'
-----------------
Creating mock for gpio...
Generating runner for test_led.c...
Compiling test_led_runner.c...
Compiling test_led.c...
Compiling mock_gpio.c...
Compiling unity.c...
Compiling led.c...
Compiling cmock.c...
Linking test_led.out...
Running test_led.out...

-----------
TEST OUTPUT
-----------
[test_led.c]
  - ""

--------------------
IGNORED TEST SUMMARY
--------------------
[test_led.c]
  Test: test_module_generator_needs_to_be_implemented
  At line (16): "Implement me!"

--------------------
OVERALL TEST SUMMARY
--------------------
TESTED:  1
PASSED:  0
FAILED:  0
IGNORED: 1

Add an actual unit test

Setup and configuration is always a difficult part for embedded projects. Now though we've managed to fight through it... so that we can get down to the business of actually writing some unit tests.

Since the LED on our board is connectted to pin 2 of port F, we might want to test that our led_turn_on function uses GPIOPinWrite to set pin 2 of port F. We can create a new unit test function in test/test_led.c and use an expectation to do this:

include "inc/hw_memmap.h"

void test_when_the_led_is_turned_on_then_port_f_pin_2_is_set(void)
{
    // Expect PORTF pin 2 to be set.
    GPIOPinWrite_Expect(GPIO_PORTF_BASE, GPIO_PIN_2, GPIO_PIN_2);

    // Call the function under test.
    led_turn_on();
}

Note that we needed to #include "inc/hw_memmap.h" to get access to GPIO_PORTF_BASE, and GPIO_PIN_2.

And if we run our tests now, we can watch it pass:

projects\blinky>  ceedling test:all


Test 'test_led.c'
-----------------
Generating runner for test_led.c...
Compiling test_led_runner.c...
Compiling test_led.c...
Linking test_led.out...
Running test_led.out...

-----------
TEST OUTPUT
-----------
[test_led.c]
  - ""

--------------------
OVERALL TEST SUMMARY
--------------------
TESTED:  1
PASSED:  1
FAILED:  0
IGNORED: 0

The next steps

Now that you have a unit test framework set up for your project, your ready to start incrementally adding tests where you can -- gradually make your embedded software better.

You should consider trying to have tests for any new code you're writing, but do what you can. Are you chasing a bug? See if you can create a failing test for it first. Then make it pass. Bam, bug fixed!

Also -- to make it easier to run the tests -- you could set up your IDE to run ceedling test:all when you press a keyboard shortcut. Better yet, you could set it up to run ceedling test:<your_current_file> (with the current file in your editor). This only runs the tests for the file that your working on. Eventually when you have many more tests, this will be a lot faster.

Get the source code for this complete example on GitHub.

 
E-book

Get more out of Ceedling

Once you have Ceedling set up in your project, the best way to add new features is by test-driving.

Learn how to use Ceedling for test-driven development from the examples in my downloadable "how to" guide.

Sign up to get it here.