Integrating the Unity test-framework with CMake
In this tutorial I will demonstrate practical CMake Unity integration in order to make it easy to run unit-tests in CI/CD environments.
Table of Contents
Prerequisites
The sample project described in this article is hosted on my github page. Projects used in this tutorial can be found under the following URLs:
Preparing the project tree
In this article I am assuming that you are using Git as your source code version control system. The Unity source code is hosted on github, thus making it perfectly suitable for being used as a Git Submodule. This is what I have done in the sample project as well.
In order to check out the sample project including its submodules the --recursive
option has to be passed to the git clone
command:
git clone --recursive https://github.com/rpoisel/cmake-unity-tutorial.git
More information on git submodules can be found here. Learning how git submodules work can be cumbersome in the beginning but it pays on the long run. Git submodules make it possible to manage what version of a specific other source tree is being used or referenced in a given source tree.
The structure of the sample project is as follows:
.
|- external
| \- (Unity sources)
|- main
| \- (entry point of the main application)
|- module_a
| \- (module_a sources)
\- test
\- (Unit-Tests sources)
The module_a module contains a library of shared code. In this project it is both referenced by the main application as well as the unit-tests. The external
directory contains the sources from external projects or other git submodules (in this case the Unity testing framework). The main directory contains the production code and the test directory contains the source of the unit-tests.
CMake Unity targets
First, let’s have a look at the main CMakeLists.txt
file:
project("Sample Project" C)
cmake_minimum_required(VERSION 3.0)
set(TARGET_GROUP production CACHE STRING "Group to build")
add_subdirectory(module_a)
if(TARGET_GROUP STREQUAL production)
add_subdirectory(main)
elseif(TARGET_GROUP STREQUAL test)
include(CTest)
add_subdirectory(external)
add_subdirectory(test)
else()
message(FATAL_ERROR "Given TARGET_GROUP unknown")
endif()
The TARGET_GROUP
variable determines whether to build the production code or the test code. It is important to put the include(CTest)
instruction in the outermost CMakeLists.txt
file. Otherwise ctest
will complain that there aren’t any tests.
The Unity framework has no default CMake configuration. Therefore it is provided at the closest place in our project tree (the external directory):
add_library(Unity STATIC
Unity/src/unity.c
)
target_include_directories(Unity PUBLIC
Unity/src
)
If desired it is also possible to make the Unity framework a dynamic library. However, unit-tests can now make use of the Unity framework by issuing target_link_libraries(Unity)
(see below).
The
target_link_libraries
command does more than just specifying which libraries to link to the target executable. It also pulls in allINTERFACE
andPUBLIC
properties of the referenced library such as include directories defined bytarget_include_directories
, compile definitions defined bytarget_compile_definitions
, etc. Thus, using thetarget_link_libraries
command also affects compile time of your build!
See the CMake documentation for more information on that.
Code to be tested
The sample code has been taken from the Unity documentation page. The library interface exposes the only function AverageThreeBytes():
#ifndef MODULE_A_H
#define MODULE_A_H
#include <stdint.h>
int8_t AverageThreeBytes(int8_t a, int8_t b, int8_t c);
#endif /* MODULE_A_H */
Due to the PUBLIC
keyword of the call to target_include_directories
, the directory containing the module’s header file is exposed to all other binary targets linking to module_a
‘s library.
add_library(module_a STATIC
module_a.c
)
target_include_directories(module_a PUBLIC
${CMAKE_CURRENT_LIST_DIR}
)
A sample unit-test
The unit-test executable suite_1_app
is added to the test suite_1_test
which is intended to be executed by ctest
. Please note that line 10 shows the simplified call of the add_test
command. See the CMake documentation of add_test on more information of its invocation!
add_executable(suite_1_app
suite_1.c
)
target_link_libraries(suite_1_app
module_a
Unity
)
add_test(suite_1_test suite_1_app)
The test suite consists of two tests both of which should pass. The state of executed tests is managed internally by the Unity framework (library). The unity.h
and module_a.h
header files can be found by the compiler due to the target_link_libraries
call pulling in all usage requirements of dependent libraries.
#include <unity.h>
#include <module_a.h>
void test_AverageThreeBytes_should_AverageMidRangeValues(void)
{
TEST_ASSERT_EQUAL_HEX8(40, AverageThreeBytes(30, 40, 50));
TEST_ASSERT_EQUAL_HEX8(40, AverageThreeBytes(10, 70, 40));
TEST_ASSERT_EQUAL_HEX8(33, AverageThreeBytes(33, 33, 33));
}
void test_AverageThreeBytes_should_AverageHighValues(void)
{
TEST_ASSERT_EQUAL_HEX8(80, AverageThreeBytes(70, 80, 90));
TEST_ASSERT_EQUAL_HEX8(127, AverageThreeBytes(127, 127, 127));
TEST_ASSERT_EQUAL_HEX8(84, AverageThreeBytes(0, 126, 126));
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_AverageThreeBytes_should_AverageMidRangeValues);
RUN_TEST(test_AverageThreeBytes_should_AverageHighValues);
return UNITY_END();
}
CTest only checks the exit code of unit-test executables. In case it is other than 0 (zero), a test suite has failed.
The UNITY_END()
macro aggregates the results of all TEST_ASSERT_*
macros. the return value is the sum of all failed tests.
Building and executing unit-tests
On the command-line the sample project’s unit-tests can be built using the Ninja build system by issuing the following commands:
cmake -GNinja -DTARGET_GROUP=test {path-to-source-tree}
ninja -v
The tests in turn can be executed by invoking ctest
. The --verbose
switch makes ctest more talkative and shows which of your unit-tests have passed (or failed).
user@machine:/tmp/build$ ctest --verbose
UpdateCTestConfiguration from :/tmp/build/DartConfiguration.tcl
Parse Config file:/tmp/build/DartConfiguration.tcl
UpdateCTestConfiguration from :/tmp/build/DartConfiguration.tcl
Parse Config file:/tmp/build/DartConfiguration.tcl
Test project /tmp/build
Constructing a list of tests
Done constructing a list of tests
Checking test dependency graph...
Checking test dependency graph end
test 1
Start 1: suite_1_test
1: Test command: /tmp/build/test/suite_1/suite_1_app
1: Test timeout computed to be: 1500
1: /tmp/cmake_unity/test/suite_1/suite_1.c:23:test_AverageThreeBytes_should_AverageMidRangeValues:PASS
1: /tmp/cmake_unity/test/suite_1/suite_1.c:24:test_AverageThreeBytes_should_AverageHighValues:PASS
1:
1: -----------------------
1: 2 Tests 0 Failures 0 Ignored
1: OK
1/1 Test #1: suite_1_test ..................... Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.00 sec
Alternatively, CTest can be invoked by the test target which is available to the used build system, e. g. ninja -v test
. The return code of the ctest call gives information whether the execution of configured unit-tests has been successful. It is 0 (zero) on success.
user@machine:/tmp/build$ echo $?
0
When changing any sources of your project, make sure to invoke your build system before executing CTest again!
Conclusion
This brief tutorial tries to explain the main steps of integrating the Unity test framework into a software project managed by CMake. The Unity source is provided as a git submodule and the framework is used as a static or dynamic library.