Field Manual For Ceedling
Field Manual For Ceedling
Matt Chernosky
Copyright © 2017 Matt Chernosky.
All rights reserved.
Thank you for purchasing this book, I hope you find it useful. Feel free to share it within your
development teams, but please don't share it outside your teams. If you want to recommend
it to others, please suggest that they purchase a copy of their own. This helps me keep
creating useful stuff for you and for everyone else.
--Matt
ElectronVector
Colorado, USA
http://www.electronvector.com
Revision 2
Table of Contents
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.4. Versions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.5. Definitions. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
2. Installing Ceedling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.1. Linux. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.1.2. Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3.1. Conventions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
4. Writing Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
5.5. Integers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
5.6. Strings . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
5.7. Floats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
5.7.2. Equality . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
5.8. Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
5.9. Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
5.10. Structs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
5.10.1. Contents. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
5.10.2. Instances . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5.11. Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
You don’t need to install Unity or CMock separately, they come included with
Ceedling.
Ceedling makes writing and running unit tests easier by automatically discovering and
running tests — and generating mocks so that you can test modules in isolation. Ceedling
minimizes the amount of boilerplate code you need to write to create a unit test suite.
Sure… unit tests are just code, and you could probably roll your own build system to run your
unit tests. You could even figure out how to use Unity and CMock (or other frameworks) by
themselves.
Do yourself a favor though, and use Ceedling instead. Don’t get hung up on perfecting your
build system. Use the off-the-shelf unit test build system provided by Ceedling — and start
writing unit tests as quickly as possible.
By running tests on the host, we make them easier and faster to run. This makes it more
likely that we’re actually going to run the tests (and keep them up to date). We also won’t be
dependent on the hardware for testing — which can be an advantage if it isn’t available.
To make testing on the host easier, you’ll want to design your software so that any hardware
dependencies are isolated in modules that can be mocked. A common way to do this is to use
a hardware abstraction layer (HAL). If you don’t take this into consideration then unit testing
on the host is still possible — just more difficult.
Chapter 1. Introduction 1
1.3. Your IDE
The simplest approach to using Ceedling is to run it alongside your IDE. So use your IDE to
build your application binary but jump out into a command line to run rake for your Ceedling
tests.
The simplest IDE integration would be to configure a shortcut key to run rake test:all so
that you can run Ceedling without leaving the IDE.
If you really want to get fancy, if there is a way you can access the name of the file you are
currently editing you could configure a shortcut key to run rake test:<current_file>. This
would only run the tests for the module you are working on — which will run a lot faster
once you have a lot of tests. You might need to manipulate the name of the file to remove the
extension or any test_ prefix (if it’s the test file you have open).
1.4. Versions
The examples in this book were developed and tested with Ceedling 0.25.0 and Ruby 2.3.1.
1.5. Definitions
binary
A software application that has been compiled and linked from source code. A binary can
be executed — either on the host or the target — depending on how it was was compiled.
Ceedling
A build and test system for C language applications. It provides automatic test execution
and mock generation.
CMock
The mocking framework used by Ceedling. It can be used to create mocks for functions to
be called during a test.
collaborator
A software module used by another to do its work (i.e. a dependency).
cross compilation
Compiling on system with the intention of running on another. This is what you do when
you use your computer to compile for your target. You use a cross-complier (like gcc-arm-
none-eabi) which can be run on the host, but produces binaries that can only be run the
2 Chapter 1. Introduction
target.
dependency
When one software unit requires another one to do some operation. When module_a calls
functions in module_b, we say that module_a has a dependency on module_b.
dependency chain
The module-to-module links created when a function calls a function in another module,
and then that function calls a function in another module, etc.
function prototype
The declaration of a function’s signature. For "public" functions, this is usually defined in a
header file which other modules can include to use the function.
"functional" test
A test that doesn’t require mocking any functions to execute.
GCC
GNU Compiler Collection - A collection of free software compilers from the GNU
project — only the C compiler is relevant here. This is the C compiler Ceedling uses by
default.
HAL
hardware abstraction layer
host
Your computer. This is where you develop and compile your code. You can also run your
tests here as explained in this book.
integration test
Generally, this is a test of more than one component at a time. In this document an
integration test is a software integration test, which is a test of multiple software units.
mock
A "stand-in" for another module, that can be used to isolate a software module from its
dependencies for unit testing. It can be used to simulate behavior and test interactions.
module
Chapter 1. Introduction 3
A software component consting of source file (.c) and corresponding header file (.h). The
header file defines the "public interface" to the module by declaring the functions that can
be called. The source file contains the implementation. Often used interchangeably with
unit.
native compilation
Compiling on the same system you’re going to run the application on. This is what you’re
doing when you use gcc to compile your tests. These tests can be run on the host — not the
target.
stub
An empty function declaration used in place of a real one. When unit testing, stubs are
used to independently compile modules with dependencies on other modules.
target
Your embedded system. This is were you load your application and run it.
test assertion
A statement in a unit test that validates some expected outcome. With Ceedling we use the
TEST_ASSERT_ style macros provided by Unity.
TDD
test driven development - A development approach where unit tests are created at the
same time as the code they’re testing.
unit
The smallest independently testable component of your software application. This is
typically a single a c source file (with corresponding header file).
unit test
A single function that tests some behavior of a software module. There are usually many
of these per software module.
Unity
The unit test framework used by Ceedling. It provides all of the TEST_ASSERT macros
needed to create unit tests.
4 Chapter 1. Introduction
Chapter 2. Installing Ceedling
In this section we’ll 1) set your system up to run Ceedling and 2) install it into an existing
project.
If you don’t have an existing project yet, you’ll probably want to start with an example
project for your processor or board. This project should be able to build your application
from your source code, load it to the target and run it. We won’t need any of this to run host-
based unit tests with Ceedling - but will be useful if you actually want to use the modules
you’ll be unit testing.
We need to install a native GCC for our host PC, not any kind of cross-
compiler for a target processor. Our goal in this guide is to set up to run unit
tests on the host PC.
Ceedling will be run from the command line, and so you’ll also need to make sure that your
path is set up correctly to point to both Ruby and GCC.
2.1.1. Linux
Installing the tools for Linux should be relatively easy. The simplest way to install everything
in Linux is with a package manager like apt-get.
2.1.2. Windows
Setting things up on Windows requires a bit more effort (but not too much!).
Ruby
Ruby can be installed for Windows from rubyinstaller.org. You may want to check out the
information on the downloads page about which version of Ruby to install. The examples in
this book are tested with version 2.3.1.
Download the correct installer for your platform — either the 32-bit or the 64-bit (x64)
version — and run the installer.
During installation, I recommend using the default installation folder and selecting to have
Ruby added to the path. This will make it easier to install and use Ceedling from the
command line.
Don’t install Ruby into a location with spaces in the path like C:\Program
Files.
Be sure to select the option for Add Ruby executables to your PATH.
This shows that we’re running with our expected Ruby version.
GCC
GCC is our C compiler. There are two primary ways to install GCC on Windows: via MinGW or
Cygwin. I recommend MinGW for use with Ceedling.
Download and run the installer. The default options should work fine.
Don’t install MinGW into a location with spaces in the path like C:\Program
Files.
Eventually you’ll come to the package selection screen. Click on the box next to mingw32-
base and select Mark for Installation:
Then click the Apply button and everything will be installed. When it’s done, you can close
the installer.
If you need any more help installing MingGW, see the Getting Started guide at mingw.org.
I’ve recently had an issue trying to use the gem command to install Ceedling. It’s a
problem with the RubyGems server (where Ceedling and other gems are hosted). If you
get an error like this:
ERROR: Could not find a valid gem 'ceedling' (>= 0), here is why:
Unable to download data from https://rubygems.org/ - SSL_connect
returned=1 errno=0 state=SSLv3 read server certificate B: certificate
verify failed (https://api.rubygems.org/specs.4.8.gz)
The quick fix is to install a new certificate into Ruby. Download this certificate file:
https://raw.githubusercontent.com/rubygems/rubygems/master/lib/rubygems/ssl_certs/
index.rubygems.org/GlobalSignRootCA.pem
Before we make any changes to the code, your code should be in source
control.
Keeping your code in source control is good practice in general. It allows you to freely
take risks and experiment with the code (like installing Ceedling!) without the fear of
ruining anything or losing any work. You can throw away any changes at any time and
return to a known good state.
That being said, maybe you’re going to ignore my advice. In that case please — at
least — make a backup copy of your project before making any changes. I don’t want
you to blame me for ruining your project.
Ceedling works best when all of the source code is in a single folder called src at the top level
of the project. This is one of the most basic Ceedling conventions and how Ceedling knows
where to find your source files to test.
So, you’ll want to create a folder at the top level of your project and put all of your project
source in it. It’ll look something like this:
my_project/
├── my_project.config
└── src
├── display.c
├── display.h
├── led_driver.c
├── led_driver.h
└── main.c
It’s perfectly fine to have other project configuration files (like my_project.config) at the top
level, but just make sure all your source code is under src.
If you’re using any third party libraries, Ceedling needs to know about them if you’re
going to use them in your unit tests. I wouldn’t necessarily recommend trying to unit
test these libraries, but you may want to create mocks based on their header files.
Despite what some hardware vendors do by default, you shouldn’t put any of this code
elsewhere on your computer — it should be inside of your project folder (and stored in
source control with your project). You can do this by putting any library folders directly
in the src folder, however you might want to put them somewhere else for
organizational purposes (good candidates include creating a new lib folder or using the
vendor folder where Ceedling lives). If you’re not using the src folder, you’ll need to
add your library folder to the source paths in the project.yml configuration file (see
the section on Changing Source File Paths in Chapter 8: Changing the Configuration).
It’s also fine to have whatever complicated source tree you want within the src folder. For
example, your project could look something like this:
my_project/
├── my_project.config
└── src
├── display
│ ├── display.c
│ └── display.h
├── drivers
│ └── led
│ ├── led_driver.c
│ └── led_driver.h
└── main.c
In this case we have a more complicated source tree, but everything is still in the src folder.
Ceedling isn’t great about handling source files with the same names but in
different folders (especially when it comes to mocking). Try to avoid this
situation if you can. In the above example, the led_driver files are in an led
folder so it seems like it might be redundant to include the prefix led_ in
their names. This is done intentionally here to avoid conflicts with other
files named driver that might be in other folder paths.
Once you have everything rearranged and can build your application though your normal
build tool/IDE, check it all back into source control and you’re ready to install Ceedling into
the project.
> dir ①
my_project
This adds all a whole bunch of files to your project. Let’s take a look at the new folder now,
after adding Ceedling to the project:
my_project/
├── build ①
├── my_project.config
├── project.yml ②
├── rakefile.rb ③
├── src ④
├── test ⑤
│ └── support
└── vendor ⑥
└── ceedling
├── docs
├── lib
├── plugins
└── vendor
① The build directory is where Ceedling puts everything that it generates when it runs.
③ Since Ceedling is based on Rake, rakefile.rb is what "installs" all the Ceedling tasks we’ll
use to run tests.
⑥ The vendor directory contains Ceedling itself. Inside vendor/ceedling/docs you’ll find
documentation for Ceedling, as well for Unity, CMock and CException — three other
components used by Ceedling.
Note that we have Ceedling installed on our system, but we just installed that copy of Ceedling
into our project. That means that this particular version of Ceedling will stay with the project
(if we check it into source control — which we should do) no matter where it goes or what
system it gets installed on. There isn’t any dependency on each developer’s local
environment. This is particularly useful when we want to keep close track of the versions of
the tools we’re using, like when developing a product for a regulated industry.
--------------------
OVERALL TEST SUMMARY
--------------------
No tests executed.
You can use rake from anywhere within the project tree — you don’t
necessarily have to be at the top level project folder.
Since we install all of Ceedling into the project folder, using rake lets you run your tests
on a system that doesn’t have Ceedling installed. All you need is Ruby and Rake. To use
ceedling — even if you have installed Ceedling into your project — you would need to
install Ceedling on your system. Keeping track of Ceedling versions could be an issue
here if you have multiple projects. Also if you’re typing it out, rake is a few characters
shorter.
With Ceedling installed in the project, it’s time to check it back into source control. Every
thing in your project should go into source control except for the contents of the build folder.
Here are the details:
build
This folder should go in source control but you should ignore all of it’s contents. The
folder needs to exist to run Ceedling, but all of its contents are generated and thus
shouldn’t be under source control.
project.yml
This contains your project’s Ceedling configuration and should go in source control.
rakefile.rb
You need this to run Ceedling — it should go in source control.
src
This is your application! Obviously everything in this directory should go in source
control.
vendor
The vendor directory should go in source control as well. This allows you to run your unit
tests after a clean checkout on a new machine without having to install Ceedling again.
These are all of the commands you can run from within your project now. Try running a few
to see what they do. For example, take a look at what Ceedling thinks are your source files:
For now though, let’s look at some reasonable conventions suggested by Ceedling. I think
these are good practice to follow.
• All source code goes in the src folder (for third party libraries located elsewhere, see
[Third Party Libaries] in Chapter 2: Installing Ceedling and Changing Source File Paths in
Chapter 8: Changing the Configuration).
• Every source module is implemented in a .c file with a corresponding .h file. The .h file
defines the public interface of the module — the interface that we will be exercising in our
tests.
• Each source module has a corresponding unit test file (a .c file) that goes in the test
folder. This file is named test_module.c, where the module is the name of the module
we’re testing.
Ceedling actually provides an easy way for us to create modules that follow these
conventions, the module generator.
The simple_examples.c and simple_examples.h files generated are sparse, but they have
everything we need to get started:
Listing 5. simple_examples.h
#ifndef _SIMPLE_EXAMPLES_H
#define _SIMPLE_EXAMPLES_H
#endif // _SIMPLE_EXAMPLES_H
Listing 6. simple_examples.c
#include "simple_examples.h"
The test file is a little bit handier because it creates the little bit of boilerplate that we need to
get started writing unit tests. :
#include "unity.h" ①
#include "simple_examples.h" ②
void setUp(void) ③
{
}
void tearDown(void) ④
{
}
void test_simple_examples_NeedToImplement(void) ⑤
{
TEST_IGNORE_MESSAGE("Need to Implement simple_examples"); ⑥
}
⑥ The TEST_IGNORE_MESSAGE macro tells Unity to ignore the test (not counted as passing or
failing) and print the given message.
Run all tests in the project with rake test:all and you can see what we’ve created:
Test 'test_simple_examples.c'
-----------------------------
Generating runner for test_simple_examples.c... ①
Compiling test_simple_examples_runner.c...
Compiling test_simple_examples.c...
Compiling unity.c...
Compiling simple_examples.c...
Compiling cmock.c...
Linking test_simple_examples.out...
Running test_simple_examples.out...
-----------
TEST OUTPUT
-----------
[test_simple_examples.c]
- ""
--------------------
IGNORED TEST SUMMARY
--------------------
[test_simple_examples.c]
Test: test_simple_examples_NeedToImplement
At line (14): "Need to Implement simple_examples" ②
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 1
PASSED: 0
FAILED: 0
IGNORED: 1
① This is Ceedling telling us what it’s actually doing. This can come in handy when things
start to get more complicated.
Listing 9. test_simple_example.c
void test_simple_examples_NeedToImplement(void) ①
{
TEST_IGNORE_MESSAGE("Need to Implement simple_examples");
}
void test_that_will_pass(void) ②
{
TEST_ASSERT_EQUAL(1,1);
}
Note the use of the TEST_ASSERT_EQUAL(1,1) in the new test. Every test needs to actually test
something with a test assertion. If the assertion fails, then the test fails. In this case we test
that 1 is equal to 1 — which will always be true — so the test will pass. The test assertions
used in Ceedling come from the Unity unit test framework (and there are quite a variety of
them). You can see a lot more of them in Chapter 5: Testing with Unity.
When the module generator creates our first test function from its template
(test_simple_examples_NeedToImplement), it suggests a naming convention where our
tests should start with test_<module_name>_. Every test function does need to start with
test_, but there really isn’t a good reason to include the module name as well.
Typically in a C application we prefix all of our function names with our unique
module name so we don’t run into collisions with similar functions from other
modules.
That’s not necessary with Ceedling though since each test file is compiled into its own
binary. So don’t put the name of the module under test in your test name, it’s not
necessary and will just add clutter.
rake test:all
This runs all of the tests in your project.
rake test:<module_name>
This runs the tests from only a single module — the tests in the file
test_<module_name>.c. For example, run our simple_examples tests with rake
test:simple_examples. This comes in handy when your project has a lot tests.
rake test:delta
Run the tests for only for modules that have been changed since the last time the tests
were run. Tests will be re-run if the test file or the module .c file have been changed. The is
the default when you simply run rake test.
Listing 11. Creating deeply nested modules with the module generator.
Note that this creates a tree within the test folder that matches the source file tree:
simple_tests
├── build
├── project.yml
├── rakefile.rb
├── src
│ ├── path
│ │ └── to
│ │ └── another
│ │ ├── module.c ①
│ │ └── module.h
│ ├── simple_examples.c
│ └── simple_examples.h
├── test
│ ├── path
│ │ └── to
│ │ └── another
│ │ └── test_module.c ②
│ ├── support
│ └── test_simple_examples.c
└── vendor
② The test folder contains a tree matching the one under src.
void test_simple_examples_NeedToImplement(void)
{
printf("Something to help debug this test");
TEST_IGNORE_MESSAGE("Need to Implement simple_examples");
}
$ rake test:all
Test 'test_simple_examples.c'
-----------------------------
Running test_simple_examples.out...
-----------
TEST OUTPUT
-----------
[test_simple_examples.c]
- "Something to help debug this test" ①
- ""
--------------------
IGNORED TEST SUMMARY
--------------------
[test_simple_examples.c]
Test: test_simple_examples_NeedToImplement
At line (15): "Need to Implement simple_examples"
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 1
PASSED: 0
FAILED: 0
IGNORED: 1
One thing to note though is that there isn’t any way to tell which specific unit test is
producing the output (the only indication is which test file it came from). If you have multiple
tests with lots of output, it’s going to be hard to tell what is going on. So use output in your
tests sparingly. I’ll usually only use it temporarily to debug a problem with a complicated
test — and then remove it.
To make Ceedling build and run a test file, just put it in the test folder and give it a name that
starts with test_. By default any test file created like this will be executed by Ceedling (also
remember that test function names need to start with test_ to be executed).
To test a state machine implementation in state_machine.c, we’d create a test file named
test_state_machine.c in the test folder.
In the test file, we need to include unity.h as well as the header of the module under test. To
test the logic in state_machine.c we’d include it’s corresponding header file
state_machine.h (the one that exposes its "public" interface).
The other thing the test file must have are setUp and tearDown functions. Without these,
Ceedling will fail with lots of errors. You don’t actually need to use them, but you at least need
to have empty implementations. Altogether, the minimum requirements for a test file look
like this:
#include "unity.h" ①
#include "state_machine.h" ②
void setUp(void) ③
{
}
void tearDown(void) ④
{
}
Any code you put in the setUp function is executed before each test function is run. If you
have three test functions in your test file, setUp gets called three times. This can be useful for
simplifying your tests (by reducing code duplication) if you have common set up code that
you need to run before each of your tests. The tearDown works similarly but after each test
function is run.
typedef enum {
RESET,
READY,
} state_t; ①
② This function determines the next state based on the current state.
The behavior of the state machine can be tested by using the state_get_next function.
Suppose that after RESET, the next state should be READY. Here’s what the test file would look
like:
Listing 18. Add a simple test for the state machine (test_state_machine.c)
#include "unity.h" ①
#include "state_machine.h" ②
void setUp(void) ③
{
}
void tearDown(void)
{
}
void test_reset_goes_to_ready(void) ④
{
state_t current = RESET;
state_t next = state_get_next(current); ⑤
TEST_ASSERT_EQUAL(READY, next); ⑥
}
② Include the header file for the module under test. This instructs Ceedling to include the
state_machine module logic (implemented in state_machine.c) in our test.
④ Here’s our test. We are testing that we go from the RESET state to the READY state.
⑤ Calling state_get_next(current) is how we get the state machine to give us the next state
based on the current state.
⑥ Use a Unity test assertion to verify that we got the correct next state.
In this test we set up some initial conditions (the current state), exercised the module under
test by calling one of its functions, and used Unity to verify that we got the correct results.
It’s a lot easier to test functions that don’t call other functions.
Listing 19. Simple implementation of state_get_next that passes the test (state_machine.c).
Listing 20. A more complicated implementation of state_get_next that calls another function
(state_machine.c)
#include "button.h" ①
① Include the header file for another collaborator. The button module is now a dependency.
② Call a function in the button module to determine the button status (and thus the next
state).
Here’s a new test file to test the state machine that collaborates with a the button module:
Listing 21. A new test file that mocks the button module interface
(test_state_machine_with_collaborator.c).
#include "unity.h"
#include "state_machine_with_collaborator.h"
#include "mock_button.h" ①
void setUp(void)
{
}
void tearDown(void)
{
}
void test_reset_stays_at_reset_when_button_pressed(void)
{
button_is_pressed_ExpectAndReturn(true); ②
TEST_ASSERT_EQUAL(RESET, next); ④
}
① Tell Ceedling to use CMock to mock all of the functions it can find in button.h. These get
built into the test instead of anything in a button.c.
② Tell CMock what should happen when state_get_next calls button_is_pressed. In this case,
we want it to return true to simulate that the button is actually pressed.
This is just a taste of the way CMock works — for more details see the
Chapter 6: Mocking with CMock.
We could write a similar test for the other case — when the button is not pressed. In this case
we instruct CMock to return false for the call to button_is_pressed:
Listing 22. Test that we do go from the RESET to the READY state when the button is not pressed
void test_reset_goes_to_ready_when_button_not_pressed(void)
{
button_is_pressed_ExpectAndReturn(false); ①
TEST_ASSERT_EQUAL(READY, next); ②
}
In this section we’ll cover most of your options with Unity, but it’s not necessarily exhaustive.
The documentation for Unity can be found right in your own project folder (after you’ve
installed Ceedling into it) in vendor/ceedling/docs. Depending on which version of Ceedling
you have, there will be one or more documents about Unity (the most recent versions have
more documents with better information). These are really good guides to all the different
flavors of TEST_ASSERT_ macros (there are a lot of them!), and you should really take a look at
some point.
Most of the example tests in this chapter are trivial — they don’t do anything!
These are just simple examples to show you the features of Unity and what
types of tests you can create. In your real tests, you’ll be calling functions
and testing their return values.
Listing 23. Use Unity by including its header file in the test file.
#include "unity.h"
This is done automatically if you use the module generator to create your
test file.
The simplest test assertion is TEST_ASSERT(condition). This test passes if the boolean condition
evaluates to true and fails if it evaluates to false. For example, this test passes:
void test_simple_pass(void)
{
TEST_ASSERT(1);
}
void test_simple_fail(void)
{
TEST_ASSERT(2 == 1);
}
If we put these tests in a file named test_basics.c and run them, we get output that looks like
this:
Test 'test_basics.c'
--------------------
Generating runner for test_basics.c...
Compiling test_basics_runner.c...
Compiling test_basics.c...
Linking test_basics.out...
Running test_basics.out...
-----------
TEST OUTPUT
-----------
[test_basics.c]
- ""
-------------------
FAILED TEST SUMMARY
-------------------
[test_basics.c]
Test: test_simple_fail
At line (19): "Expression Evaluated To FALSE"
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 2
PASSED: 1
FAILED: 1
IGNORED: 0
This TEST_ASSERT() macro is the simplest way to create a test. Actually, we could use
TEST_ASSERT() for all of our tests, but we don’t for two reasons.
The first reason is that we don’t get much information when the test fails. In the above
example, the test failure told us Expression Evaluated to FALSE but didn’t tell us about what
values we were actually comparing.
The second reason is that some of the comparison logic — like when we’re testing all the
values in an array — would be a pain to create or reuse for each different type of test.
void test_multiple_things(void)
{
int x = 0;
int y = 0;
get_two_values(&x, &y);
TEST_ASSERT(x == 1);
TEST_ASSERT(y == 2);
}
When you have multiple assertions, the first one to fail will cause your test fail and stop
executing. This means that no code after the first failing test will be run.
Also, this still counts as a single test in the report generated by Ceedling when it’s finished. So
if this test passes (both assertions are true), we would get something that looks like:
Listing 28. A test report for single passing test with multiple assertions.
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 1
PASSED: 1
FAILED: 0
IGNORED: 0
TEST_ASSERT_EQUAL_type(expected, actual);
where type is the type of thing that we’re comparing. For example, you can compare two
integers like this:
void test_integers(void)
{
TEST_ASSERT_EQUAL_INT(2, 1);
}
Test: test_integers
At line (24): "Expected 2 Was 1" ①
5.5. Integers
There are actually lots of ways to compare integers, based on the width of the integer,
whether it is signed or unsigned, and if you want the results in decimal or hex.
There are tests for all of the unsigned and signed C99 standard types:
• TEST_ASSERT_EQUAL_UINT8(expected, actual)
• TEST_ASSERT_EQUAL_UINT16(expected, actual)
• TEST_ASSERT_EQUAL_UINT32(expected, actual)
• TEST_ASSERT_EQUAL_UINT64(expected, actual)
• TEST_ASSERT_EQUAL_INT8(expected, actual)
• TEST_ASSERT_EQUAL_INT16(expected, actual)
Make sure to use correctly sized types for your target. If you’re testing a
uint8_t, make sure to use TEST_ASSERT_EQUAL_UINT8. If you don’t, you may get
unexpected errors from comparing the wrong number of bits — depending
on what’s next to your values in memory.
In the C99 version of the C language standard, the standard integer types in stdint.h
were introduced. Use them! Even though C99 has been around for a while, I still see a
lot of embedded code that doesn’t use standard types. Instead, each project rolls its own
version of Byte and Word, etc. I really recommend using these standard types like
uint8_t or int32_t whenever possible. It makes it easier to understand your code. Oh
and use stdbool.h too.
There are also tests that will print your results in hex:
• TEST_ASSERT_EQUAL_HEX(expected, actual)
• TEST_ASSERT_EQUAL_HEX8(expected, actual)
• TEST_ASSERT_EQUAL_HEX16(expected, actual)
• TEST_ASSERT_EQUAL_HEX32(expected, actual)
• TEST_ASSERT_EQUAL_HEX64(expected, actual)
The numbers (8, 16, 32, 64) in these names are the widths of the values (in bits) like the stdint
types. Note that these are all compared as unsigned values. As advertised, this test:
Listing 31. A test using a hex test assertion to report errors with hex values.
void test_hex(void)
{
TEST_ASSERT_EQUAL_HEX8(0x20, 0x10);
}
Test: test_hex
At line (29): "Expected 0x20 Was 0x10" ①
5.6. Strings
To compare two null-terminated character arrays (ahem, "strings") use:
TEST_ASSERT_EQUAL_STRING(expected, actual)
void test_strings_that_dont_match(void)
{
TEST_ASSERT_EQUAL_STRING("yes", "no"); ①
}
void test_strings_that_match(void)
{
char actual[] = "yes";
TEST_ASSERT_EQUAL_STRING("yes", actual); ②
}
5.7. Floats
5.7.1. Within a Range
As you probably know, because of the way floating point numbers are represented you
usually don’t want to test them directly for equality. Instead, you usually want to test that
they are within some tolerance of each other. This is done in Unity with
TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual). This tests that the actual value is within
the range of the expected value +/- delta.
void test_floats_close_enough(void)
{
TEST_ASSERT_FLOAT_WITHIN(0.1f, 2.2f, 2.23f); ①
}
① This test passes because the actual value of 2.23 is within the range of 2.1 to 2.3 (2.2 +/- 0.1).
5.7.2. Equality
Despite the typical problems comparing floats for equality, Unity does have a way to do it:
TEST_ASSERT_EQUAL_FLOAT(expected, actual). It works because it’s a bit more sophisticated
than directly testing for equality. It tests that the values are "very close" to each other. How
close they have to be is recalculated for each test based on the expected value. Pretty magical
huh?
void test_floats_equal(void)
{
TEST_ASSERT_EQUAL_FLOAT(1.0f, 1.0f);
}
There are also a variety of ways to test if a single float value is not a number (NaN), infinity, or
negative infinity:
• TEST_ASSERT_FLOAT_IS_INF (value)
• TEST_ASSERT_FLOAT_IS_NEG_INF (value)
• TEST_ASSERT_FLOAT_IS_NAN (value)
There are quite a few other variants too. See the Unity documentation in
vendor/ceedling/docs for all the options.
For each FLOAT test, there is a corresponding DOUBLE test that works on double values. For
example:
To enable double precision tests in your project, you need to define UNITY_INCLUDE_DOUBLE.
This can be done in the project.yml configuration file by adding these lines:
:unity:
:defines:
- UNITY_INCLUDE_DOUBLE
See the section on Unity Options in Chapter 8: Changing the Configuration for more
information.
5.8. Pointers
Test pointers with TEST_ASSERT_EQUAL_PTR(expected, actual):
void test_pointers(void)
{
int x; ①
int* x_ptr = &x; ②
TEST_ASSERT_EQUAL_PTR(&x, x_ptr); ③
}
① Declare an integer.
This compares the two pointer values. The data they point to is not tested.
5.9. Memory
Compare arbitrary blocks of memory with TEST_ASSERT_EQUAL_MEMORY(expected_ptr,
actual_ptr, size):
void test_memory(void)
{
uint8_t ones[] = {1, 1, 1, 1, 1}; ①
uint8_t buffer[5];
memset(buffer, 1, 5); ②
TEST_ASSERT_EQUAL_MEMORY(ones, buffer, 5); ③
}
This is a powerful test that can be used in a variety of ways, e.g. to test structs or arrays. On a
failure, you’ll get a message telling you which bytes don’t match, e.g. Memory Mismatch. Byte 4
Expected 0x02 Was 0x01.
5.10. Structs
5.10.1. Contents
When testing the contents of a struct, you can compare all of it’s members individually:
typedef struct ①
{
int x;
int y;
} point_t;
void test_matching_members(void)
{
point_t actual;
set_to_origin(&actual); ②
TEST_ASSERT_EQUAL_INT(0, actual.x); ③
TEST_ASSERT_EQUAL_INT(0, actual.y);
}
② The function we’re testing should set the actual point to the origin.
Or, you can directly compare the memory between two structures with
TEST_ASSERT_EQUAL_MEMORY(expected, actual, size). This just compares the raw memory.
typedef struct
{
int x;
int y;
} point_t;
void test_matching_memory(void)
{
point_t expected = { ①
.x = 0,
.y = 0,
};
point_t actual;
set_to_origin(&actual);
If this test fails, you’re just going to get a simple error message like Memory
Mismatch. Byte 0 Expected 0x00 Was 0x01. To get a better indication of what
went wrong, test each structure member individually instead.
To test that two structures are the same instance in memory, you want to compare pointers
with TEST_ASSERT_EQUAL_PTR(expected, actual).
typedef struct
{
int x;
int y;
} point_t;
void test_same_struct_instance(void)
{
point_t a = { .x = 1, .y = 1 };
point_t b = { .x = 2, .y = 2 };
TEST_ASSERT_EQUAL_PTR(&a, closest_to_origin); ②
}
① get_closest_to_origin takes in two point pointers and returns the one that is closest to the
origin.
Using a memory comparison to compare two structures is a convenient shortcut (and it’s
used by CMock when expecting structures passed by reference). You need to watch out
though, because this can cause false errors in some situations.
In a memory comparison, it’s just two raw areas of memory being compared — without any
regard for what’s stored there. When you create a structure in C, it’s possible to end up with
holes when the members don’t align with word size of the machine — and you need to make
sure these get initialized correctly.
Consider an adc_config_t struct that is composed of an 8-bit value, followed by two 32-bit
values:
① 8 bits
② 32 bits
③ 32 bits
When the compiler constructs this in memory on my 32-bit PC it’s going to use three 32-bit
values, which will look something like this:
If I allocate one of these structures on the stack by creating a local function variable, none of
the structure memory will be initialized. If I then set the values of all the structure members,
this isn’t enough to initialize all of the memory allocated for the structure. The holes remain
uninitialized. This means that they could contain any values at all.
For example, if I create a struct on the stack and initialize it like this:
Then only the bytes used by these fields are initialized — here is what happens to the
memory:
Figure 2. Setting all structure members does not initialize all memory
Usually this wouldn’t be a problem — if you’re just accessing a structure by its members. But
when we use a memory comparison to compare structures, these uninitialized memory holes
can cause false errors.
The way to deal with this is to always zero initialize your structures. Here’s a convenient way
to do this:
This = {0} syntax is C99. You can also use a memset from string.h if you’re not
using C99.
5.11. Arrays
Many of the Unity test assertions also have variants for testing arrays. For most
TEST_ASSERT_EQUAL_type(expected, actual) assertions, there is a corresponding
TEST_ASSERT_EQUAL_type_ARRAY(expected, actual, count) assertion. The array variant takes an
extra parameter — the number of elements in the array.
void test_int_array(void)
{
int a[] = {1, 2, 3, 4, 5};
int b[] = {1, 2, 3, 4, 5};
TEST_ASSERT_EQUAL_INT_ARRAY(a, b, 5);
}
Array tests are supported for the integer types (INT8, UIN32, etc…), hex types (HEX, HEX8,
HEX16, etc..), pointers, strings and memory:
• TEST_ASSERT_EQUAL_INT_ARRAY (expected, actual, count)
• TEST_ASSERT_EQUAL_INT8_ARRAY (expected, actual, count)
• TEST_ASSERT_EQUAL_INT16_ARRAY (expected, actual, count)
• TEST_ASSERT_EQUAL_INT32_ARRAY (expected, actual, count)
void test_that_is_ignored(void)
{
TEST_IGNORE(); ①
TEST_ASSERT_EQUAL_INT(0,1); ②
}
② Even though this test would fail, we don’t get here because the ignore statement ends the
test at the line above.
An ignored test shows up in the IGNORED count report, and is not recorded in the TESTED, PASSED
or FAILED counts:
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 0
PASSED: 0
FAILED: 0
IGNORED: 1
void test_fail_under_special_condition(void)
{
int i = 0;
int j = 0;
if ((i == 0) && (j == 0))
{
TEST_FAIL(); ①
}
}
① If we don’t ever reach the TEST_FAIL() statement, then this test will pass.
This failure reports our custom error message when it’s run:
Test: test_integers_with_message
At line (108): "Expected 2 Was 1. 2 cannot equal 1!"
For example, here’s a function (from a temperature module) that calls an adc function:
#include "adc.h"
The other option (and the one detailed here) is to mock the adc module in this test. We can
use Ceedling and CMock to generate a mock version of the adc module and compile it into
our test for us. Once we mock a module with CMock we don’t need to worry about any of the
dependencies of the adc module anymore. We break the dependency chain and we can test the
original temperature module in isolation.
In our previous example, our temperature module used functions from the adc module by
including adc.h. This header file should contain prototypes for all of the adc functions that
can be used by other modules — i.e. the inteface for this module.
You must define your module interfaces in header files in order for CMock
to create mocks for them. This might be a common convention — but the C
language doesn’t do anything to enforce it — so it’s possible (likely?) that you
may need to reorganize your code a bit to use CMock. See the Conventions
section of the Creating Modules and Running Tests chapter for more details.
To have generate mocks for a header file, all we need to do is include the mock header file
name in one of our unit test files. The mock header file name is the original header name
prepended with mock_.
For example, to create mocks for the functions in adc.h, we just include mock_adc.h in our
test instead:
#include "mock_adc.h"
Test 'test_temperature.c'
-------------------------
Creating mock for adc... ①
Generating runner for test_temperature.c...
Compiling test_temperature_runner.c...
Compiling test_temperature.c...
Compiling mock_adc.c... ②
Compiling unity.c...
Compiling temperature.c...
Compiling cmock.c...
Linking test_temperature.out...
Running test_temperature.out...
Since CMock builds mocks from header files, you don’t even need an
implementation of a module to create a mock for it. This can be useful for
defining (and testing against) interfaces that you’re going to need but don’t
exist yet. You can come back and implement the modules behind those
interfaces, but still develop unit tests for other modules that might use them.
If you want to see all the mock control functions that CMock has generated
for a particular module, you can find them in the .h files in
build/test/mocks.
You use this function in your tests to tell CMock that you expect adc_get_sample to be called
and what value you want it to return. You do this by calling adc_get_sample_ExpectAndReturn
Listing 51. Using a mock to expect a function call and return a value (test_temperature.c)
#include "unity.h"
#include "temperature.h"
#include "mock_adc.h"
void setUp(void) {}
void tearDown(void) {}
void test_temperature_over_limit(void)
{
adc_get_sample_ExpectAndReturn(151); ①
TEST_ASSERT_TRUE(is_temperature_over_limit()); ②
}
② Call the function under test. When this happens, the mock adc_get_sample is called and 151
is returned. Then we use a Unity test assertion to verify that is_temperature_over_limit
returns the correct value.
We can also test the other case (when the temperature is not over the limit) by returning a
different value from adc_get_sample:
void test_temperature_not_over_limit(void)
{
adc_get_sample_ExpectAndReturn(150); ①
TEST_ASSERT_FALSE(is_temperature_over_limit()); ②
}
It’s important to note that CMock is strict about expectations. If a function is called but you
don’t set up an expectation for it, CMock will fail your test. So if we write a test like this:
void test_that_fails_for_unexpected_call(void)
{
is_temperature_over_limit();
}
it will fail because adc_get_sample will get called, but we didn’t tell CMock to expect it. Here’s
what the error output looks like when running:
-------------------
FAILED TEST SUMMARY
-------------------
[test_temperature.c]
Test: test_that_fails_for_unexpected_call
At line (25): "Function adc_get_sample. Called more times than expected."
#include "mock_bar.h"
foo_function_to_test(); ②
}
② The test passes if foo_function_to_test calls the expected functions in the expected order.
Listing 56. A function to initialize the adc — it takes a channel number as an argument
The temperature module might want to initialize the adc from its own
temperature_initialize function:
This Expect function takes a single argument — the channel number from the original
function. If the original function had more arguments, the generated Expect function would
have corresponding arguments for each of them.
When we call adc_initialize_Expect from a test, the value we pass for channel tells CMock
what value we expect to be passed to adc_initialize from the function under test.
Here’s a test for temperature_initialize that expects adc_initialize to be called with channel
void test_correct_channel_is_initialized(void)
{
adc_initialize_Expect(3); ①
temperature_initialize(); ②
}
If the mocked call isn’t called with the arguments that we expect, then the test will fail with a
message from CMock. If we changed the value we expect so that the test will fail:
Listing 59. Expect the wrong argument to to adc_initialize so that this test will fail
void test_that_fails_for_incorrect_channel_initialization(void)
{
adc_initialize_Expect(4); ①
temperature_initialize();
}
This test now fails with a message showing us what went wrong and where:
-------------------
FAILED TEST SUMMARY
-------------------
[test_temperature.c]
Test: test_that_fails_for_incorrect_channel_initialization
At line (34): "Expected 0x04 Was 0x03. Function adc_initialize Argument
channel. Function called with unexpected argument value."
• The expected value and the actual value that was encountered.
:cmock:
:plugins:
- :ignore
- :callback
:cmock:
:plugins:
- :ignore
- :callback
- :ignore_arg
For more details see Enabling CMock Plugins in Chapter 8: Changing the Configuration.
For each function with arguments, a new function is generated for each argument that lets
you tell CMock to ignore it. So for our adc_initialize function:
void adc_initialize_IgnoreArg_channel(void);
You use it by calling it after you’ve expected a call. So if we wanted to test adc initialization,
but didn’t care what channel was initialized, we’d do this:
void test_initializaton_but_dont_care_about_channel(void)
{
adc_initialize_Expect(0); ①
adc_initialize_IgnoreArg_channel(); ②
temperature_initialize();
}
① Expect adc_initialize. It doesn’t matter we pass in for channel here since we’re going to
ignore it.
typedef struct {
uint8_t channel;
uint32_t clock_rate;
uint32_t sample_rate;
} adc_config_t; ①
config.channel = 3; ②
config.clock_rate = 40000000;
config.sample_rate = 1000;
adc_initialize_configuration(&config); ③
}
① Create a new adc_config_t struct. Note the zero initialization of the structure with = {0},
this is very important see The Trouble with Comparing Structures as Memory in Chapter 5:
Testing with Unity for details.
How would we test (with a mock) that adc_initialize_configuration was called with a struct
containing our expected values? Well, CMock makes it easy because you can expect a pointer
argument and it will automatically test the data the pointer points to and not the pointer
value itself. Pretty slick! It looks like this:
expected_config.channel = 3; ②
expected_config.clock_rate = 40000000;
expected_config.sample_rate = 1000;
adc_initialize_configuration_Expect(&expected_config); ③
temperature_initialize_with_config(); ④
}
When this test is run, CMock compares the memory pointed to by expected_config with the
memory provided in the call to adc_initialize_configuration through the configuration
argument pointer. This tests that we passed the data we expected.
When you expect a pointer argument, by default CMock will compare the data referenced by
the pointer. If you want to verify the value of a pointer and not the data it points to, you need
to change the CMock configuration in project.yml.
This is a global setting. If you change this, you won’t be able to compare
pointer data in any other tests.
To enable this you need to set :when_ptr: to :compare_ptr under the :cmock: settings in
project.yml. For more details see the CMock Options section in Chapter 8: Changing the
Configuration.
Listing 65. A project configuration file (project.yml) set to compare pointer values
:cmock:
:when_ptr: :compare_ptr
Try not to need this. The default behavior — to compare the data — is much
more useful in most circumstances.
This requires enabling of the return_thru_ptr CMock plugin in your project.yml file.
Add it to the this list:
:cmock:
:plugins:
# ... other plugins ...
- :return_thru_ptr
For more details see the Enabling CMock Plugins section in Chapter 8: Changing the
Configuration.
In C, it’s common to return data from functions by putting it into structures passed in
through pointers. If we wanted to get the current configuration of our adc module, we might
have a function called adc_get_configuration that takes a pointer argument to an
adc_config_t:
typedef struct {
uint8_t channel;
uint32_t clock_rate;
uint32_t sample_rate;
} adc_config_t;
This function works by returning the current configuration settings in the adc_config_t
structure passed in. It might be used from the temperature module in
is_temperature_configured:
return (config.channel == 3)
&& (config.clock_rate == 40000000)
&& (config.sample_rate == 1000);
}
void test_temperature_configuration_check(void)
{
adc_config_t mock_config = {0}; ①
mock_config.channel = 3;
mock_config.clock_rate = 40000000;
mock_config.sample_rate = 1000;
adc_get_configuration_Expect(0); ②
adc_get_configuration_IgnoreArg_configuration(); ③
adc_get_configuration_ReturnThruPtr_configuration(&mock_config); ④
TEST_ASSERT_TRUE(is_temperature_configured()); ⑤
}
① Create a "mock" configuration structure. This is what will be returned to the caller when
adc_get_configuration is mocked.
⑤ Use a Unity test to verify that is_temperature_configured() returns the correct value.
#include "logger.h" ①
Listing 70. A test expecting a mock function call with a string argument
#include "mock_logger.h" ①
void test_initialization_with_log_of_string(void)
{
log_message_Expect("temperature module initialized"); ②
temperature_initialize_with_log();
}
If we don’t really care about what the log messages are, we can ignore these function calls in
our tests. This makes the tests simpler.
void test_while_ignoring_log_functions(void)
{
log_message_Ignore(); ①
adc_get_sample_ExpectAndReturn(151);
TEST_ASSERT_TRUE(is_temperature_over_limit_with_log()); ②
}
Once you ignore a function it gets ignored for the rest of the test. You can’t
go back and "unignore" it.
When the array plugin is enabled, CMock provides an ExpectWithArray function your each
mocked function that contain pointer arguments. For each pointer argument the
ExpectWithArray function takes an additional argument which specifies the number of array
elements to compare. For example, mocking this function:
#include "bar.h"
void foo_function_with_array_argument_call(void)
{
int luggage_code[5] = {1, 2, 3, 4, 5};
bar_with_int_array(luggage_code);
}
we could write a test that checks the array passed in like this:
#include "mock_bar.h"
foo_function_with_array_argument_call();
}
② Use the ExpectWithArray function to this. Note that we need to provide the number of
elements to test — 5 in this case.
You can use array checking with pointers to custom structures too.
For each mocked function, CMock creates a corresponding StubWithCallback function which
takes a function pointer as argument. You define and provide this function to implement a
custom callback. The signature for this function is the same as the original function, with one
additional argument — an int which provides the number of times the callback has been
called.
Whatever you return from your callback is what is provided to the calling function during
your test — just like the custom callback was called instead of the mock function.
void test_temperature_is_filtered(void)
{
adc_get_sample_StubWithCallback(mock_adc_get_sample); ②
for (int i = 0; i <= 150; i++)
{
TEST_ASSERT_FALSE(is_temperature_over_limit()); ③
}
TEST_ASSERT_TRUE(is_temperature_over_limit()); ④
}
④ Once we’ve called our callback 151 times (0 through 150), it will start returning a value
over the limit (151).
You can also verify arguments (or anything else) in your custom callback
functions with TEST_ASSERT calls.
When you build your application, you typically have a whole bunch of source files that are
compiled and linked into a single binary that are then loaded on the the target.
The goal of a unit test is to test a software module in isolation. This what you get by default
when you use Ceedling to create a module with rake module:create[]. A corresponding test
file is created for each source module file. When Ceedling "runs" each of these tests, it
compiles a separate binary for each test and runs it on the host:
So… why does Ceedling create all these separate test binaries, instead of just compiling
into one monolithic test binary? Well, one reason is that it’s easy to run a single test just
by running the binary for that test. The other reason has to do with breaking the
dependencies between the modules in your application, which we’ll see in a moment.
When Ceedling runs, how does it know that test_module_a.c needs to be compiled and linked
with module_a.c — and not any of the other files in your application? Well, it has to do with
the files that are included (with #include) in the test source file.
Listing 75. Basic test file (test_module_a.c) created by the module generator
#include "unity.h"
#include "module_a.h" ①
void setUp(void)
{
}
void tearDown(void)
{
}
void test_module_generator_needs_to_be_implemented(void)
{
TEST_IGNORE_MESSAGE("Implement me!");
}
You can see that the module generator included module_a.h for us by default. This is the
convention used to tell Ceedling to look for module_a.c and build it into our test. Ceedling
assumes that there is corresponding .c file for each header file included in the test. This is
convenient because we’re using Ceedling to manage the building of the test right from the
test source code. We don’t need to mess with Make or some other tool to specify how to build
the tests.
This will become even more important when we have modules with dependencies.
The name of the test source file (test_module_a.c) does not influence what
Ceedling actually tries to build into the test. This name is just a convention
for us to use, so that we know what is being tested. If the test file named
test_module_a.c had a #include "module_b.h", Ceedling would compile and
link module_b into the test instead.
So if we start calling module_b functions from module_a, we can’t build and run our tests
for module_a like we used to. Ceedling needs a little more information to determine how to
deal with module_b.
There are two options at this point. You select the one you want by how you include the
module_b.h header in the test file.
Ceedling does this if you include module_b.h into the test with #include "module_b.h".
When functions in module_b are called, the code in module_b is executed.
Ceedling does this if you #include "mock_module_b.h" in the test file. This header file
doesn’t exist but Ceedling automatically creates it. Ceedling also creates the
corresponding mock_module_b.c, which is what gets linked in to the test.
The mock module doesn’t contain any of the implementation of the original module.
Instead it contains mocks (or stubs) for each of the module_b function calls, which satisfy
the dependencies of module_a, and allow us to compile the test. The mock also allows us
to simulate and test the interactions between module_a and module_b.
Ceedling knows what functions it must create stubs for by the functions that are declared
in module_b.h.
Being able to independently unit test the modules in your application is all
about managing the dependencies between them. The more dependencies a
unit has, the more difficult it is going to be to test it in isolation.
In this example, module_a depends on both module_b and module_c. Both module_b and
module_c depend on module_d.
As discussed previously in this chapter, we control what files are tested by the way we
include header files in the test file.
You could create a unit test for module_a by mocking module_a and module_b. The headers
included in this case would be:
Listing 76. Include statements to create an isolated unit test for module_a
#include "module_a.h"
#include "mock_module_b.h"
#include "mock_module_c.h"
You might name your test file test_module_a_b_c.c, and the included headers would look
like:
Listing 77. Include statements to create an integration test for modules a, b and c
#include "module_a.h"
#include "module_b.h"
#include "module_c.h"
#include "mock_module_d.h"
Figure 10. An integration test built by including module_a, module_b, and module_c (and mocking
module_d)
Grouping modules together like this is a strategy that can be useful when trying to add tests
to existing code. If there are many, complicated inter-module dependencies, it may make
sense to attempt to break dependencies only where they are a bit simpler.
Summary
• You can test one or more software modules at a time with Ceedling.
• Testing units in isolation requires breaking the dependencies between modules with
mocks.
• Include a module in your test by including it in the test file (e.g. #include "module_b.h").
• Create integration tests by including multiple module headers in your test, creating mocks
for modules only where you need to break dependencies.
The project.yml file is a YAML file. YAML a simple file syntax for setting key-value pairs.
Indentation is used to create hierarchies and sequences (lists) are created with dash (-)
characters. You should be able to understand the format by looking at the file that Ceedling
creates by default — it has a enough entries in there to see the patterns.
In this section, heirarchies are indicated with the arrow (→) character. So
:project: → :build_root: is the setting for the :build_root: located under
the :project: setting.
Comments start with # characters. Comments can start anywhere in a line and go to the end
(just like // in C).
One thing to watch out for is that YAML files must uses spaces for
indentation. Do not use tabs for indentation in your project.yml file or you
may run into problems.
# This is a comment.
:project:
:build_root: build ①
:test_file_prefix: test_
:plugins:
:enabled:
- stdout_pretty_tests_report ②
- module_generator # Here is another comment.
① A simple key-value pair (nested below :project:. This tells Ceedling the name to use for
the "build" folder.
For list elements, the order may be important — like when setting the compiler and linker
command line flags.
YAML uses colons between the key and the value, but the colons at the
beginning of each key (like in :project:) are a Ruby thing. If you look for
YAML help elsewhere you won’t see these extra leading colons, but Ceedling
needs them.
Ceedling also has internal default settings which are used if you don’t set everything in
project.yml. This also means that the default project.yml created does not have all of the
possible settings. Anything that you put in project.yml takes precedence over any default
internal setting.
We’ll cover some of the important options here, but check out the Ceedling documentation in
vendor/ceedling/docs to find out about more options.
:paths:
:test: ①
- +:test/**
- -:test/support
:source: ②
- src/**
:support: ③
- test/support
① The :test: paths are where Ceedling looks for test files to run.
② The :source: paths are where Ceedling looks for your source code.
The ** is a special glob syntax to include the entire tree below a particular folder. Thus src/**
includes the folder named src as well as all folders (at any depth) below it.
Each of the path configurations takes a list of paths below it, using the dash (-) to start each
line. You can add to paths just by adding a line. To add an additional source file path — maybe
for a third party library stored elsewhere on your machine — just add another item to the
:source: entry:
:paths:
:source:
- src/**
- C:\path\to\libary ①
① Add another source path. Note that you can use absolute paths too like this Windows
machine path.
The default settings for the :test: paths show us how to select and exclude particular
folders — put a +: in front of an item to add it, and use a -: to exclude it:
Listing 81. The default test path settings show how to select individual paths for inclusion and
exclusion
:paths:
:test:
- +:test/** ①
- -:test/support ②
Sometimes though, you might want to include or exclude a particular file. In this case there is
the :files: setting. It’s not included in the default project.yml, but you can add it anywhere
at the top level of the YAML hierarchy (i.e. no indentation before :files:). You can use it to
add and remove :source:, :test: or :include: files:
:files:
:source:
- -:src/temp.c ①
:test:
- -:test/*_temp.c ②
:include:
- +:C:\path\to\library\api.h ③
② Exclude all files in the test folder that end with _temp.
To see the files found in each of your configured paths try rake files:source,
rake files:test or rake files:header.
:project:
:test_file_prefix: Test ①
For this there is the top-level :defines: section. To set a definition during your unit tests, add
it to the :defines: → :test: list.
:defines:
:commmon: &common_defines []
:test: ①
- *common_defines
- TEST
- STM32F4 ②
- USE_RTOS=1 ③
① Add your own preprocessor definitions to the :test: list under :defines:.
Notice that TEST define up there? By default TEST is defined when running
your unit tests. That means you can do special things in your code inside
#ifdef TEST blocks that will only get included when your unit tests are run.
This isn’t always a great idea, but can be useful in some situations. Also note
that this can cause a problem in some builds… if you already define TEST in
your own code. You can remove it from here (or change its name) to address
this problem.
:plugins:
:load_paths: ①
- vendor/ceedling/plugins
:enabled: ②
- stdout_pretty_tests_report
- module_generator
① The :load_paths: are where Ceedling will attempt to load plugins from. If you take a look
in the default vendor/ceedling/plugins folder, you’ll see a few plugins to choose from.
② This is the list of plugins to enable. These are the names of folders found in the
:load_paths:.
CMock also has plugins. When working with Ceedling plugins, make sure
you’re at the top level of the YAML file heiarchy — and not under :cmock:.
Many of the included plugins modify the reporting functionality — e.g. there are plugins for
gtest, ide, teamcity, xml, etc. There is also a plugin for gcov (the GNU test coverage tool) that
looks like it could be interesting.
Also there is a fake_function_framework plugin (full disclosure: I wrote it) that uses the fake
function framework (FFF) as a mocking library instead of CMock.
Where possible, look for a README file in the plugin folder that explains what it is or how to
:unity:
:defines:
- UNITY_INT_WIDTH=16 ①
- UNITY_INCLUDE_DOUBLE ②
You should prefer to use stdint.h types like uint16_t instead of plain int, so
settings like UNITY_INT_WIDTH=16 won’t be necessary. I do… and I don’t
typically find that I need to set Unity configuration options like this.
CMock has its own plugins, enabled by adding them to the list in :cmock: → :plugins:. The
default plugins are:
:cmock:
:plugins:
- :ignore
- :callback
ignore
Enables Ignore functions for ignoring calls to mocked functions.
ignore_arg
Enables IgnoreArg functions for ignoring specific arguments passed to mock functions.
array
Enables ExpectWithArray functions for testing arrays passed to mocked functions.
callback
Enable StubWithCallback functions for providing your own callback functions to be called
instead of mocked functions.
return_thru_pointer
Enable ReturnThruPtr functions for returning data through pointer arguments provided to
mocked functions. This also enables ReturnArrayThruPtr and ReturnMemThruPtr
functions.
The default name for mock files is to prepend a header file name with mock_. If you want to
change this, set :cmock: → :mock_prefix: to whatever convention you want:
:cmock:
:mock_prefix: Mock ①
By default, Ceedling won’t mock a function that is prototyped with an extern. So if the
function below was found in a header file you wanted to mock, a mock wouldn’t be
generated for it:
This is sometimes needed for legacy code that uses extern statements in its function
prototypes.
Sometimes the header files you want to mock don’t #include all of the header files that they
need — and you can’t change the code for whatever reason to fix it.
For example, I’ve worked with libraries that define functions like this:
bool is_spi_ready(void);
but don’t #include <stdbool.h>. This will cause CMock problems during compilation. But you
can use the :cmock: → :includes: list to have CMock include additional header files in each of
the generated mock files.
:cmock:
:includes:
- <stdbool.h>
- <stdint.h>
Whatever you list here will be included with a #include in the generated mock files, allowing
them to compile correctly.
You can use this to include your own header files too — not just standard
ones like stdbool and stdint.
When you expect a pointer argument in a mocked function the default behavior is to test the
data referenced by the pointer. If you want to test the value of the pointer instead, set :cmock:
→ :when_ptr: to :compare_ptr
By default, Ceedling configures CMock to use strict call order checking. That means that your
function under test must call your mocks in the exact order that you expect them. For
example, if your function under test looks like this:
void functions_called_out_of_order(void)
{
function3();
function2();
function1();
}
void test_functions_out_of_order(void)
{
function1_Expect();
function2_Expect();
function3_Expect();
functions_called_out_of_order();
}
You can turn off this "order checking" by setting :cmock: → :enforce_strict_ordering: to
FALSE:
Usually you can only see the complier commands executed by Ceedling when there is an
error. To see them during a successful run, set the verbosity to 4. Set it when running a rake
task like this:
You need to set the verbosity each time you run rake. The setting is not saved
between runs.
When looking at the output, you’ll notice that Ceedling does a few different types of
operations, and they use different compiler options:
Compiling test_functions.c...
> Shell executed command: ②
gcc.exe -I"test" -I"test/support" -I"src" -I"C:/dev/unity/src" -I"C:/field-manual
-for-ceedling/examples/8-changing-the
-configuration/vendor/ceedling/vendor/unity/src" -I"C:/field-manual-for
-ceedling/examples/8-changing-the-configuration/vendor/ceedling/vendor/cmock/src"
-I"build/test/mocks" -DTEST -DGNU_COMPILER -g -c "test/test_functions.c" -o
"build/test/out/test_functions.o"
Linking test_functions.out...
> Shell executed command: ③
gcc.exe "build/test/out/test_functions_runner.o"
"build/test/out/test_functions.o" "build/test/out/unity.o"
"build/test/out/cmock.o" -o "build/test/out/test_functions.out"
① This is using gcc to determine dependencies. It uses options like -E, -MM, and -MG to do this.
To change the compiler or its settings, you need to add the :tools: → :test_compiler section
to project.yml. The default settings are listed below. The :argmuments: section is a little
complicated, so you’ll probably want to copy this whole section into project.yml and modify
it as you need.
:tools:
:test_compiler:
:executable: gcc ①
:name: test_compiler
:arguments: ②
- -I"$": COLLECTION_PATHS_TEST_TOOLCHAIN_INCLUDE ③
- -I"$": COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR
- -D$: COLLECTION_DEFINES_TEST_AND_VENDOR
- -DGNU_COMPILER
- -g
- -c ${1} ④
- -o ${2} ⑤
① The compiler application that is executed. gcc is the default. This is an example using gcc
in the system path, but you can provide an absolute path here too.
② This is where the arguments to the compiler are specified. You’ll want to add any
additional arguments to this list. Note that the order of these is the order in which they are
passed to the compiler.
③ These are all the include paths. These "all caps" names like
COLLECTION_PATHS_TEST_TOOLCHAIN_INCLUDE are the lists of paths created by Ceedling based
on other configuration settings.
So if you wanted to turn on most gcc warnings you’d add -Wall to the list:
:tools:
:test_compiler:
:executable: gcc
:name: test_compiler
:arguments:
- -I"$": COLLECTION_PATHS_TEST_TOOLCHAIN_INCLUDE
- -I"$": COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR
- -D$: COLLECTION_DEFINES_TEST_AND_VENDOR
- -DGNU_COMPILER
- -g
- -c ${1}
- -o ${2}
- -Wall ①
You need to use two - characters for each flag like - -Wall. The first is for the
YAML list, and the second is part of the option passed to gcc.
The linker is configured in the :tools: → :test_linker: section of project.yml. Below are the
default settings. The :argmuments: section is a little complicated, so you’ll probably want to
copy this whole section into project.yml and modify it as you need.
:tools:
:test_linker:
:executable: gcc ①
:name: 'test linker'
:arguments: ②
- ${1} ③
- -o ${2} ④
① The name of the linker application. Either put it in the path like here or use an absolute
path.
② The list of arguments to the linker. Add additional arguments or flags here.