smhk

Setting up Unity test framework for CMake

Unity is a test framework written in C, which is commonly used for unit testing embedded software. It can be integrated with CMake, but official support is “a work in progress and not suitable for use yet”. However, as shown by this blog post from November 2022 and its corresponding example project, Unity can integrate well with CMake.

It’s been two years since that blog post was published, and both Unity and CMake have changed a little, so I decided to share the steps I took today in November 2024. The main differences between my steps and the aforementioned are:

  • Fixing a minor issue (that arose due to changes in Unity).
  • Updating the usage of CMake (due to changes in CMake).
  • Aligning some things with my personal preference.

Project structure §

Following is how I have structured my project:

.
├── submodules/
│   └── external/
│       └── unity_cmake/
│           ├── Unity           # Git submodule
│           └── CMakeLists.txt  # Builds `Unity`
├── dumb_example/
│   ├── DumbExample.c
│   ├── DumbExample.h
│   └── CMakeLists.txt          # Builds `DumbExample_lib`
├── app/
│   ├── main.c
│   └── CMakeLists.txt          # Builds application that uses `DumbExample_lib`
├── test/
│   ├── dumb_example/
│   │   ├── TestDumbExample.c
│   │   └── CMakeLists.txt      # Builds `DumbExample_test`
│   └── CMakeLists.txt          # Builds all tests
└── CMakeLists.txt              # Top-level

From top to bottom, the folders are:

  • submodules/external/unity_cmake: The Unity test framework, included as a Git submodule, with a basic CMakeLists.txt to build it as a library.
  • dumb_example: An example module (library) named DumbExample.
  • app: An application, which contains the main() function, and uses the DumpExample library.
  • test: A suite of tests, which test the DumbExample library, but can easily be extended to test more modules as they are added.

Including Unity as a Git Submodule §

Add the submodule §

Use this command to add Unity as a submodule:

git submodule add https://github.com/ThrowTheSwitch/Unity.git submodules/external/unity_cmake/Unity

This includes the entire Unity repo at submodules/external/unity_cmake/Unity.

Strictly speaking, to use Unity you only need to include these three files:

  • unity.c
  • unity.h
  • unity_internals.h

However, I chose to include the entire Unity Git repo as a submodule so that we have traceability and can easily upgrade to new releases. I also prefer to keep third-party code stored separately when possible, so that there is a clear division in ownership, hence the use of an external/ directory.

Add a CMakeLists.txt §

We must provide a CMakeLists.txt which builds Unity as a static library.

Create a CMakeLists.txt in unity_cmake/:

CMakeLists.txt
add_library(Unity STATIC
    Unity/src/Unity.c
)

target_include_directories(Unity PUBLIC
    Unity/src
)

This provides Unity as a library, which we use when building the tests.

DumbExample §

The files DumbExample.c and DumbExample.h come from the Unity docs on “Getting Started” (no anchor tag so you’ll have to scroll down quite far). They are quite simple:

DumbExample.h
#include <stdint.h>

int8_t AverageThreeBytes(int8_t a, int8_t b, int8_t c);
DumbExample.c
#include "DumbExample.h"

int8_t AverageThreeBytes(int8_t a, int8_t b, int8_t c)
{
	return (int8_t)(((int16_t)a + (int16_t)b + (int16_t)c) / 3);
}

The CMakeLists.txt builds this as a library called DumbExample_lib:

CMakeLists.txt
add_library(DumbExample_lib STATIC
    DumbExample.c
)

target_include_directories(DumbExample_lib PUBLIC
    ${CMAKE_CURRENT_LIST_DIR}
)

Application §

The application is not needed for testing purposes, but to provide a complete example, this is where one would fit into the picture:

main.c
#include <stdio.h>
#include "DumbExample.h"

int main(void)
{
    printf("Average of 4, 5, and 6: %d\n", AverageThreeBytes(4, 5, 6));
    return 0;
}

To build the application:

CMakeLists.txt
add_executable(App
    main.c
)

target_link_libraries(App
    DumbExample_lib
)

Suite of tests §

For this example, we will only add tests for one module: DumbExample_lib. In a real project, there would likely be multiple modules, and a suite of tests per module, each in their own folder.

Tests for DumbExample_lib §

The test/dumb_example/ directory contains the suite of tests for DumbExample_lib. Populate TestDumbExample.c with some test code to verify DumbExample_lib:

TestDumbExample.c
#include "unity.h"
#include "DumbExample.h"

void setUp()
{
}

void tearDown()
{
}

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();
}

A key difference between this and the aforementioned blog post is the inclusion of setUp() and tearDown().1

The CMakeLists.txt for building these tests works by linking against DumbExample_lib and Unity:

CMakeLists.txt
add_executable(DumbExample_app
    TestDumbExample.c
)

target_link_libraries(DumbExample_app
    DumbExample_lib
    Unity
)

add_test(NAME DumbExample_test COMMAND DumbExample_app)

In add_test, NAME argument is “simply a string name for the test […] that will be displayed by testing programs”. The COMMAND argument is “the executable to run”.

Build all tests §

The file test/CMakeLists.txt lists all test suites. For now, we only have one:

CMakeLists.txt
add_subdirectory(dumb_example)

Top-level CMakeLists.txt §

We use CTest to run the tests, which is an executable provided by CMake. This is required for the add_test() command from earlier.

We use a top-level CMakeLists.txt which:

  • Defines the project metadata.
  • Adds CTest.
  • Builds the library under test.
  • Builds the application.
  • Builds Unity, the test framework.
  • Builds all the tests.

Pulling it all together:2

CMakeLists.txt
project("Example project" C)
cmake_minimum_required(VERSION 3.15.0)

# Add CTest for running the tests.
include(CTest)

# Build `DumbExample_lib`.
add_subdirectory(dumb_example)

# Build `App`.
add_subdirectory(app)

# Build `Unity`.
add_subdirectory(submodules/external/unity_cmake)

# Build all our tests.
add_subdirectory(test)

Building §

To build, from the project root:3

cmake -S . -B build
cmake --build build

Many older examples will show different ways of calling CMake, but this is the preferred modern way. Using cmake --build build, instead of calling the specific make or ninja build tool directly, makes it simpler to follow the same build steps for different build systems.

The outputs will be in the build/ directory.

Running the tests §

Run CTest with:4

ctest --test-dir build

If all has gone well, you should see a passing test!


  1. There is an open pull request to fix this on the example project. See also this SO answer↩︎

  2. If you want to get a little more advanced, you can see this example which uses TARGET_GROUP to allow building the application or the tests. I decided to keep it simple for this example. ↩︎

  3. If you added the TARGET_GROUP option, then you would build the tests with cmake -DTARGET_GROUP=test -S . -B build↩︎

  4. Using --test-dir tells CTest to that directory. If you call ctest from the wrong directory, you get the error No test configuration file found!. This will happen if you run ctest without arguments in the project root. The solution is to either use --test-dir <dir> to specify the build directory, or to ensure you run ctest from inside the build directory by doing cd <dir> && ctest. In my view the --test-dir option is preferable. ↩︎