Unit Testing for C
- 1 Unit Testing
- 1.1 Definition
- 1.2 Benefits
- 1.3 Cost
- 1.4 Pitfalls
- 1.5 Test Frameworks
- 1.5.1 Comparisons
- 1.6 Unity & CMock
- 1.6.1 Unity
- 1.6.2 CMock
- 1.6.3 CMock Plugins
- 2 Test Driven Development
- 3 Philosophy
- 4 Basics of Unity & CMock
- 4.1 How it works
- 4.1.1 Run Tests
- 4.1.2 Files of the Test Infrastructure
- 4.1.3 How it Builds
- 4.2 Available APIs
- 4.3 To Mock or not to Mock?
- 4.3.1 Mock Variations
- 4.3.2 Parameters
- 4.3.3 Pointer Parameters
- 4.4 Add a Unit-Test file
- 4.1 How it works
- 5 Unit Test Best Practices
- 6 Unit Test Examples
- 6.1 Test Template
- 6.1.1 Test Results
- 6.2 Test Hello World
- 6.3 An Example without a Mock
- 6.3.1 Typical Code
- 6.3.2 Improved Code
- 6.3.3 Unit Test Simplified Persistency Module
- 6.4 An Example with a Mock
- 6.4.1 Mock with a Callback
- 6.4.2 Expects stack up
- 6.5 Test Parameters
- 6.5.1 More ways to test Parameters
- 6.5.2 Stack Parameters
- 6.5.3 Verify struct data
- 6.5.4 Verify struct * pointers
- 6.6 The CMock ReturnThruPtr Plugin
- 6.6.1 ReturnThruPointer
- 6.6.2 ReturnArrayThruPointer
- 6.6.3 ReturnMemThruPtr
- 6.7 Test static
- 6.7.1 static functions
- 6.7.2 static variables
- 6.7.3 Caveats
- 6.7.3.1 Unable to use static inside of a function
- 6.7.3.2 CMock compiler error
- 6.8 Test a large blob
- 6.8.1 Improvement A
- 6.8.2 Improvement B
- 6.9 Test forever loops
- 6.10 Test the main Function
- 6.11 Test the 'Untestable'
- 6.1 Test Template
- 7 Tips & Tricks
- 7.1 Refactor
- 7.2 The NOOP trick
- 7.3 Common Headers
- 7.3.1 sl_unit_test_facilitator.h
- 7.3.2 common_header.h
- 8 Code Labs
- 9 Code Coverage
Unit Testing
Each line of code you design is testable, and the most efficient way to test it is through unit-tests. In fact, we should test code before the inception of the code itself. Sounds strange? Not really, this is called Test-Driven-Development, or TDD. Without unit-testing, we may be really shit developing rather than software developing. Unit-Tests provide an important barrier very, very close to the developer to ensure quality at inception of the code.
Without unit-tests:
You cannot assess how well the code will work (if at all)
Someone could alter the code and easily break it (software regression)
Refactoring and improving the code is much harder, and time consuming
Definition
Typically automated; GDB is not a replacement
Testing one code module at a time, such as
file.cWrite code to test code (same programming language)
Write a test case for each line of production code
Write a code module and test it, such as
memory_buffer.c
Unit Testing (UT) is not necessarily testing it on your board
Typically, UT means you test it on the machine that is compiling the code Hence, no need to load the code to the target, or the embedded processor
Using a
printfto manually inspect whether your code works is not unit-testing
In some industries such as Aerospace, it is required to run UTs on your target
This validates that the compiler for the target is free of bugs
For example, if you run UT on x86 machine, and your code is targeted on Cortex-M4, then that proves that the code will work on x86, but not necessarily on the M4
Benefits
Develop without hardware
Refactor code with confidence
Reduce the costly debugging sessions
Significantly Accelerate development
Establish a strong barrier against bug leaks
Serves as a double check during development
Reduce the discovery, investigation, and patches for software bugs
Get immediate feedback if the code is working as the developer intended
Cost
The cost of writing unit-tests is negligible compared to the cost of finding and patching a bug at a later time. This cost is not just the developer time alone, but also the time of many other parties involved, including the customer itself which suffers from lost productivity. It is easy to test weird scenarios your code could get into during unit-tests, and it is difficult to force your code to go into complex scenarios during product's integration tests.
Clean C++
The cost of writing a UI test is often high, and hard to automate. In terms of raw code, it is easy to write an automated test and focus towards a very precise functionality and it is usually difficult to inject and create certain test scenarios towards the higher end of the pyramid. The cost of creating the anti-pattern below is prohibitively high.
Clean C++
Pitfalls
Unit-Tests take too much time and slows me down
This is the most popular fictional statement. It is like saying that you do not have time to fix your bicycle when you have miles ahead to travel.
Unit-Tests are lot of maintenance
This should signal code smell, possibly in the unit-test code, production code, or both. Fundamentally, code should be modularized, and designed for simplicity to reduce maintenance. If unit-tests are a lot of maintenance, then they can probably be improved, and this may indicate potential problems with the production code.
We will hire an intern to do unit-testing later
This is an lol statement. Usually, later will never happen anyway. Further, if your code was not designed with testing in mind, then it is probably not testable. Without tests in the first place, the code likely suffers design and quality issues, and trying to perform unit-testing after the fact is not that useful. If the thought of UTs comes after the code is written, some of the benefits are already stolen from you.
It is the developers responsibility to deliver code that is believed to be bug-free, unit-tests provide a good way to ensure high quality before another un-biased party tests your code as a product black-box.
Test Frameworks
Unfortunately C and C++ do not have unit-testing built into the language, and therefore there are many test frameworks that came to existence.
None
Simply use
<assert.h>
C & C++ Frameworks
Check (C)
CGreen (C and C++)
Boost (C++)
Google Test & Google Mocks (C++)
Unity & CMock – C Test framework
This README is focused on this
Small foot-print to run on the microcontroller itself
Combined with Cmock, provides huge value to UT and “mock” APIs
Comparisons
Unity & CMock
The Unity & CMock framework is one of the best frameworks we have found for C language. There has not been anything fundamental this framework lacks, and it reduces a lot of coding effort because it generates the Mock functions for you. You can read about these two frameworks here but it is advisable to read this article first.
Unity
There are two basics things to understand about a unit-test framework. The first is the unit-test framework itself; this only provides the ability to perform assertions and write tests in a way that the framework understands how to run. Some frameworks allow you to “register” for the tests, and then they will invoke all of the registered tests. Other frameworks may either use macros that register themselves, or use scripts at compile time to register and run your test methods. Unity provides the ability to run tests in a structured way.
#include "unity.h" // Single Unity Test Framework include
void setUp(void) {
}
void tearDown(void) {
}
void test_something(void) {
TEST_ASSERT_EQUAL(1, 1);
}CMock
CMock creates mocks and stubs for C functions. It's useful for interaction-based unit testing, where you want to test how one module interacts with other modules. Instead of trying to compile all those real units together, CMock helps you by creating fake versions of all the "other" modules. You can then use those fake versions to verify that your module is working properly!
A secondary artifact for a test framework is the "mock" functionality. The mocks provide the ability to mock-out an API and hijack the functions calls outside of your code module. For instance, you can mock an API that is going to delete a database and perform assertions. The objective would be that you are testing a code module that is interfacing to a database, but you don’t want to use the database code module itself and instead want to use a dummy mock. This will make a whole lot of sense during our unit-test examples.
CMock is useful when you wish to test a piece of code without inheriting another code module. In the following example, our focus is to test my_app() but we want to Mock database_connect() function and make it return either NULL or a pointer to the database to test further code. CMock in this scenario would allow you to "stub" the database.h API, inject, verify parameters, and make functions return whatever values you wish.
#include "database.h"
void my_app(void) {
db_s *db = database_connect("google");
if (NULL != db) {
// Test this code
}
}In your unit-test, you can then do this:
#include "Mockdatabase.h"
void test_my_app(void) {
// This 'Expect' API is auto-generated by CMock
database_connect_ExpectAndReturn("google", NULL);
my_app();
}CMock Plugins
CMock has many plugins that are built when you compile the library. You can simply enable all of them because it is up to your test code to use it or not. Including more plugins than necessary should not cause any side effects.
ignoreignore_argexpect_any_argsarraycexceptioncallbackreturn_thru_ptr
Test Driven Development
This design technique should be used when the requirements of the code are clearly stated. It is meant to simplify code, and produce minimal code necessary to solve a problem.
One of the goals to keep in mind is that It is about how little code solves a problem, not how much and TDD helps you with this.
Create empty tests
Write failing test
Write just enough code to pass
Refactor the code, cleanup and optimize
Repeat until all tests are passing
Benefits
No dead code
Sign off with stakeholder
Unit Tests shape your production code
In TDD, after you write a test, and just enough code for the test to pass, it reaches a critical point of success:
Now we have reached a remarkable point in the process. If the tests pass now, we always have 100% unit test coverage at this step. Always! Not only 100% in the sense of a technical test coverage metric, such as function coverage, branch coverage, or statement coverage. No, much more important is, that we have 100% unit test coverage regarding the requirements that were already implemented at this point!
Stephan Roth. “Clean C++.”
Example
Let's design a buffer module with TDD.
TODO Screencast with example
Philosophy
The most popular argument against writing unit-tests is that they slow down development. Absolutely nothing could be farther away from reality for this statement. If I could let me nerves respond to this comment, I would respond back by saying that whoever has made such claims is either an incompetent developer, or is simply not experienced enough. This sentiment may exist because the developer has simply not practiced unit-testing to reveal the benefits to debunk this assumption.
Clean C++ shares more or less the same sentiment on the benefits of unit-tests:
Fixing bugs after software has shipped is more expensive than having unit tests in place
Unit-tests give an immediate feedback about your entire code base. Provided that test coverage is sufficiently high (approx. 100%), developers know in just a few seconds if the code works correctly.
Unit tests give developers the confidence to refactor their code without fear of doing something wrong that breaks the code. In fact, a structural change in a code base without a safety net of unit tests is dangerous and should not be called Refactoring.
A high coverage with unit tests can prevent time-consuming and frustrating debugging sessions.
Unit tests are a kind of executable documentation because they show exactly how the code is designed to be used. They are, so to speak, something of a usage example.
Unit tests can easily detect regressions, that is, they can immediately show things that used to work, but have unexpectedly stopped working after a change in the code was made.
Unit testing fosters the creation of clean and well-formed interfaces. It can help to avoid unwanted dependencies between units. A Design for Testability is also a good Design for Usability...
Refactoringwithout tests isn’t refactoring, it is just moving shit around— Corey Haines
Aim for 100% Test Coverage
Setting a high bar yields high quality software. Anything less than 100% coverage is an arbitrary number, and therefore, the only acceptable measure should be exactly 100% code coverage. As developers write a line of code, it should be immediately tested. This discipline pays off well because the code is testable to begin with, and often times, the developers would be motivated to write less code to solve a problem. Remember that It is not how much code you write, it is how little said one of my past co-workers, and I still remember that statement today.
Emphasize the tests that matter
There may be code that is way too trivial to test. For example, if an RTOS task code, or the code of a main() function is kept simple (and branchless), then that may be once place you can skip the unit-test effort. Generally, creating rules, and then creating exceptions is not the way to go. However, there are certain situations where this particular logic makes sense.
The case we are setting forth is that top level “glue code” may be exempt from 100% code coverage. Experience suggested that when the code is modularized, it was always the modules that were at fault, but not the code that glued different pieces together. This glue code should have the following properties:
No branches
Uses dependency injection to connect objects
Runs a periodic loop or spawns a task
We encourage this rule because even if we were to test this branchless code, the only thing we would test is that certain code is called in the right order with the appropriate parameters. Let’s demonstrate this by example using a top level RTOS task.
void rtos_task(void) {
buffer_s buffer;
char buffer_space[512];
buffer_init(&buffer, buffer_space, sizeof(buffer_space));
tcp_connection_s conn;
tcp_connection_listen(&conn, 1200, &buffer);
FOREVER {
tcp_connection_service(&conn, our_callback);
}
}In the code above, we have these things going on:
Setup a buffer of 512 bytes
TCP connection being setup on port 1200
Loop to service the TCP connection
The unit-test for this code would look like this:
void test_rtos_task(void) {
buffer_init_Expect(ignore_stack_var, ignore_stack_var, 512);
tcp_connection_listen_Expect(ignore_stack_var, 1200, ignore_stack_var);
tcp_connection_service_Expect(ignore_stack_var, our_callback);
// Carry out the test to validate expected function calls
rtos_task();
}You should be able to now reason with this approach, but we have to proceed with a discipline that rightfully justifies this exception. In the test code above, all we are really doing is testing if certain functions are getting called, but there is no branch logic to test. The 100% code coverage of buffer, tcp_connection is actually responsible to make sure that the code works well, and there is little that could go wrong in this top-level RTOS task. Sure, you could go ahead and test that the tcp connection is not getting passed a NULL pointer for the buffer, but it is trivial enough to ensure this by running perhaps the simplest test on your target platform.
The task level code should just be the glue code that connects the TCP connection to our buffer, and then the connection utilizes the buffer to perform I/O. Statistically speaking, there is little that can go wrong in this code as it is just few jigsaw pieces that we need to connect. The actual bugs are likely to occur inside of the buffer or the TCP connection code modules, and the suggestion is to maximize the testing to 100% in these modules, rather than focusing on top level glue code that simply pieces things together.
Positive and Negative Testing
Positive and negative testing is a fundamental mindset when testing any unit. It is the idea of testing against both valid inputs and invalid inputs. Testing against invalid inputs ensure the unit under test is sufficiently robust enough to handle irrational inputs.
/*
* Calculate current: I = V/R
* Where I = current, V = voltage, R = resistance
*/
float calculate_current(float voltage, float resistance)
{
float current = 0.0F;
if (resistance > 0.0F) {
current = (voltage / resistance);
}
return current;
}In this scenario, the valid input domain for resistance R is (0.0, FLOAT_MAX]. Voltage can be anything in this case.
Positive test cases (test rational resistances)
Expect
calculate_current(0.0, 1.0) == 0.0Expect
calculate_current(0.0, FLOAT_MAX) == 0.0
Negative test cases (test irrational resistances)
Expect
calculate_current(0.0, 0.0) == 0.0(No runtime divide by 0 exception should occur)Expect
calculate_current(0.0, -1.0) == 0.0(No runtime divide by 0 exception should occur)
Basics of Unity & CMock
The Unity and CMock unit-test infrastructure relies on developer discipline to create files with consistent names:
your_module.hExample:gps_string_parser.hyour_module.cExample:gps_string_parser.ctest_your_module.cExample:test_gps_string_parser.c
The Test framework automatically picks up source code that starts with test_ and then begins to create an executable specifically to perform the unit-test of one file at a time.
How it works
Unity and CMock framework uses Ruby and Rake to turn your test_your_module.c into a standalone executable What this means is that each test_* is actually a separate executable and is compiled by resolving the header files you included in this test_your_module.c source file.
Run Tests
Running the Unity test framework with rake is very simple.
Go to the folder that contains
rakefileType
rakeon the command-promptTo run a single test, type
rake unit single_file=code_test\test_simple.c
The
rakebuild system will run all unit-tests as separate executablesEnsure that
gcc.ymlcontains the paths to your source code
Files of the Test Infrastructure
There are a few ruby files that glue things together.
rakefileThis is the entry point when you type
raketo run the testsAdditional logic can be added here to customize the unit-test framework
rakefile_helper.rbrakefileuses this code to compile and run testsWe built this from the Cmock example, and customized it
gcc.ymlrakefile.rbuses this configuration to compile your code
How it Builds
The ruby script forms a list of all tests that begin with
test_in yourcode_testfolderScript compiles a separate executable for each test
This means each unit-test file, such as
test_buffer.cis a standalone programAll dependencies need to be #included in your
test_file
As part of the compilation of the unit-test executable, files are mocked
Each
#includethat begin with#include "Mock"will not build the real code, but instead it will build Mocked codeFor
foo.h, it will beMockfoo.handMockfoo.catut_build/mocks
Be careful of nested dependencies, such as your
code_under_test.cdepending onbuffer.cIf there is a dependency you
#includewhich has another dependency, you will also need to#includethose dependencies in yourtest_code_under_test.cas well. So if you do not mockbuffer.c, then you will also need to build the real sources that thebuffer.cdepends on.The other option is to mock the header file to avoid picking up nested dependencies. So if you
#include "Mockbuffer.h"then you do not need to worry about the nested dependencies ofbuffer.c
Behind the scenes, the script will include all files you included in your test_buffer.c and use that to build the executable. The trick is that the files that are #included as Mockfoo.h are mocked, meaning that the header file is used to initially compile, but the code is linked to the CMock, instead of the real foo.c file.
Available APIs
In a lot of the sample code below, we will see some "magical" APIs, so let us first unravel the mysteries and point out what kind of function you get with Unity and CMock.
Unity
Unity provides you with:
Assertion APIs
setUp(),tearDown()invocations before and after each test method
// Check unity.h for more examples
#define TEST_ASSERT_EQUAL(expected, actual) ...
#define TEST_FAIL_MESSAGE(message) ...
#define TEST_FAIL() ...
#define TEST_IGNORE_MESSAGE(message) ...
#define TEST_IGNORE() ...
#define TEST_ONLY() ...
// There are many more asserts, but we do not want to repeat them hereCMock
The idea behind the Mocks is that sometimes you want to test a module and you do not want to inherit the functionality of another object that you have little or no control over. Consider a naive piece of code that you wish to test.
// @file database.h
typedef struct {
void *data;
} db_s;
db_s *database_connect(int id);
#include "database.h"
void app_test(void) {
db_s *db = database_connect(10);
if (NULL == db) {
} else {
}
}In the naive example above, you wish to perform two tests:
When
database_connect()returnsNULLWhen
database_connect()returns a valid database pointer
CMock provides the Mock functionality. It extends the Unity assertions, but fundamentally provides you with a make system to stub out an external module's functionality with a fake one.
The APIs are dynamic and auto-generated based on the file/module being mocked. What really happens is that when your test_app.c performs a #include "Mockdatabase.h", then the build system purposely omits the real database.c and replaces the implementation with Mock replacements.
// If your header file contains a single function, such as:
db_s *database_connect(int id);
// Then the following APIs are auto-generated
#define database_connect_IgnoreAndReturn(cmock_retval)
#define database_connect_ExpectAnyArgsAndReturn(cmock_retval)
#define database_connect_ExpectAndReturn(id, cmock_retval)
#define database_connect_IgnoreArg_id()The test code for app_test() would look like this:
#include "unity.h"
#include "app.h"
#include "Mockdatabase.h"
void test_app(void) {
database_connect_ExpectAndReturn(10, NULL);
app_test();
}To Mock or not to Mock?
This is an important decision. A general guideline is that:
Do not Mock APIs that are very trivial
Example:
bit_count.hLet the bit counting happen the way its meant to be
Mock code modules when
Code modules are more complex; example:
database_connect()Code modules may create a distraction from the object under test
You have two options; the first option is to #include "bit_count.h" at your unit-test file. This way, you do not mock this file but inherit the real functionality of this file.
Sometimes, it is easier to build the real code, such as a simple utility called bit_count.h and it is better to not mock this file. At other times, it is better to be able to hijack an API and make it do what you want for the sake of ease of unit-testing. The idea is that you are unit-testing your module, and you don't want to depend on a behavior of another module you inherited. The testing of the other module is the other module's responsibility, and not yours.
#include "unity.h"
#include "app.h"
// Build the real code using 'bit_count.c'
#include "bit_count.h"
void test_app(void) {
// ...
}The second option is to mock this header file such that you can hijack its function calls, inject and return data for the sake of unit-testing. Which option you choose depends on your test.
When you include Mockbit_count.h, the UT framework will not build and include the real file bit_count.h. Instead, it will redirect the API as given in your header file with the mock framework's implementation which will impose Expect requirements in your unit-tests. The Expect API has a few flavors as listed in the next section.
#include "unity.h"
#include "app.h"
// Use the 'database.h' and do not build 'database.c'
// Instead, generate Mock APIs referencing 'database.h'
#include "Mockdatabase.h"
void test_app(void) {
// ...
}Mock Variations
Mocks always provide StubWithCallback which means you can install your own Mock function. Typically, other variations provided should be able to do the job, but the callback can be used to do perhaps a fancy operation inside of the Mock.
The Ignore() should rarely be used. It means that always ignore whenever a function invocation occurs. The ExpectAnyArgs() is better because at least you are stating that a function is expected. Once you do Ignore() in a test function, then whether a function is called zero times, or a million times, it really is ignored.
Expectfor functions that do not return a valueExpectAndReturnfor functions that return a valueExpectAnyArgsAndReturnto ignore input arguments but return a valueIgnoredangerous method of just ignoring the expectationIgnoreAndReturnIgnore but always return something for functions that return a valueStubWithCallbackUse a custom callback that can have a small test driver of your own
What this means is that:
For a function with no return values:
void foo(void), you will have:foo_Expect()- Expect a function call to occurfoo_Ignore()- Ignore the function call (if any)foo_StubWithCallback(your_func)- Go to youryour_func()whenfoo()is called
For a function with a return value:
int foo(void), you will have the wordAndReturn:foo_ExpectAndReturn(#)foo_IgnoreAndReturn(#)foo_StubWithCallback(func_ptr)
Mocks for functions that have arguments are covered in the next section, but here is a brief summary to get the idea of the generated API.
For a function with arguments:
void foo(int arg_name), you will have:foo_Expect()- Expect a function call with specific argument valuesfoo_ExpectAnyArgs()- Expect the function with no checks on any of the argumentsfoo_IgnoreArg_arg_name()- After expecting a function call, ignore a particular argumentfoo_StubWithCallback(func_ptr)
Parameters
For each function parameter, you would have another API available per parameter. The Ignore_arg_name() API should be called after setting up the Expect().
For example, if a function to mock is void foo(int a, int b), then the following APIs are generated:
foo_Expect(#, #)- Expect a function call with specific argsfoo_ExpectAnyArgs()- Expect the function call with any argsfoo_Ignore()- Ignore the function call (if any)foo_IgnoreArg_a()- Invoke afterfoo_Expect(a, #)to ignore the first parameter valuefoo_IgnoreArg_b()- Invoke afterfoo_Expect(#, b)to ignore the second parameter value
Pointer Parameters
Functions that use pointers as parameters have more variations. For each pointer parameter, you have the following options available:
ReturnMemThruPtrModify something inside of a parameter that is a pointerFor example, if the function parameter was named
ptrthen:ReturnThruPtr_ptr()will be availableGood candidate if your pointers are
void*and the length of the data is not known
ReturnThruPtrUse when your type is known, such as
void foo(int *)You can then ask the Mock to return a type:
int value = 2; foo_ReturnThruPtr(&value);
ReturnArrayThruPtrSimilar to
ReturnThruPtrbut you can return an array of integersint values[] = {11, 22}; ReturnArrayThruPtr(&value, 2);
Add a Unit-Test file
If you don't want to get your hands dirty, and you want to simply leverage from the test infrastructure that is setup for you, then there is little to do:
Create a new C module
You may have to edit
gcc.ymlto include the new folder path of your new module
Create a new file that begins with
test_and put it in your test folderExample:
test_buffer.c
Go to your test folder in a command-shell
Type
raketo run all unit-testsAll test files at
<your_test_dir>/testswill execute their unit-tests
Unit Test Best Practices
Exclude third party code
Unit-test should run extremely quickly (in second)
Keep tests focused; one test per functional
Code quality for tests should be equal to the production code
Avoid hacks and blocks of
#ifdef UNIT_TESTINGin your production code
SIBROS TECHNOLOGIES, INC. CONFIDENTIAL