Testing in C: CTest, CMocka, and Unity – A Practical Guide
Learn how to unit test C code using CTest, CMocka, and Unity.
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
- ✓Basic knowledge of C programming (functions, pointers, memory management)
- ✓Familiarity with CMake build system
- ✓A C compiler (GCC or Clang) installed
- ✓Optional: CMocka and Unity libraries installed
- CTest is CMake's test runner, not a unit test framework; it orchestrates test executables.
- CMocka provides assertion macros and mock support for C unit testing.
- Unity is a lightweight, single-header unit test framework ideal for embedded systems.
- All three can be combined: write tests with Unity/CMocka, run them via CTest.
- Proper testing catches memory leaks, buffer overflows, and logic errors early.
Think of testing like a safety net for a trapeze artist. CTest is the ringmaster who calls out each trick, CMocka and Unity are the nets that catch mistakes. Without them, you risk crashing your program in front of a live audience.
Imagine deploying a payment processing system that silently corrupts transaction amounts due to an integer overflow. Or a medical device that miscalculates drug dosages because of an off-by-one error. These are not hypotheticals—they are real-world consequences of untested C code. C remains the backbone of embedded systems, operating systems, and high-performance applications, where reliability is non-negotiable. Yet many C developers rely on ad-hoc testing or skip it entirely, assuming their code is correct. This tutorial introduces three essential testing frameworks: CTest, CMocka, and Unity. CTest is CMake's test driver, perfect for automating test execution. CMocka offers rich assertions and mock functions for complex dependencies. Unity is a minimalist framework ideal for resource-constrained environments. By the end, you'll be able to write robust unit tests, integrate them into your build system, and avoid common pitfalls that lead to production outages.
1. Setting Up CTest with CMake
CTest is part of CMake and provides a way to run tests and report results. It does not write tests for you; you need a test framework like CMocka or Unity. First, create a CMakeLists.txt that adds test executables and enables testing. Use and enable_testing() to register each test. CTest can run tests in parallel, filter by name, and output results in various formats (JUnit, XML). For example, to add a test named 'test_math' that runs the executable 'test_math', you write:add_test()
``cmake ``enable_testing() add_executable(test_math test_math.c) add_test(NAME test_math COMMAND test_math)
Then run cmake --build . && ctest to execute all tests. CTest returns non-zero if any test fails, making it CI-friendly.
ctest --output-on-failure to see test output immediately.2. Writing Unit Tests with CMocka
CMocka is a lightweight C unit testing library that supports mock objects, assertion macros, and test fixtures. To use it, include <cmocka.h> and link against -lcmocka. Each test is a function returning void that uses assert_int_equal, assert_string_equal, etc. Group tests using cmocka_unit_test_setup_teardown for setup/teardown. Example: test a function add that adds two integers.
```c #include <stdarg.h> #include <stddef.h> #include <setjmp.h> #include <cmocka.h>
int add(int a, int b) { return a + b; }
static void test_add_positive(void **state) { assert_int_equal(add(2, 3), 5); }
static void test_add_negative(void **state) { assert_int_equal(add(-1, -2), -3); }
int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_add_positive), cmocka_unit_test(test_add_negative), }; return cmocka_run_group_tests(tests, NULL, NULL); } ```
Compile with gcc test_add.c -lcmocka -o test_add && ./test_add.
cmocka_set_test_filter() to run specific tests during debugging.3. Lightweight Testing with Unity
Unity is a single-header C unit test framework designed for embedded systems. It has no dependencies and provides simple macros like TEST_ASSERT_EQUAL, TEST_ASSERT_TRUE, etc. To use Unity, include unity.h and define a setUp and tearDown function if needed. Tests are functions that call RUN_TEST. Example:
```c #include "unity.h"
void setUp(void) {} void tearDown(void) {}
int multiply(int a, int b) { return a * b; }
void test_multiply_positive(void) { TEST_ASSERT_EQUAL_INT(6, multiply(2, 3)); }
void test_multiply_zero(void) { TEST_ASSERT_EQUAL_INT(0, multiply(5, 0)); }
int main(void) { UNITY_BEGIN(); RUN_TEST(test_multiply_positive); RUN_TEST(test_multiply_zero); return UNITY_END(); } ```
Compile with gcc test_multiply.c unity.c -o test_multiply && ./test_multiply. Unity outputs a simple pass/fail report.
4. Integrating Tests with CTest and CMake
To combine CTest with CMocka or Unity, you need to register each test executable in CMakeLists.txt. Use add_test with the executable name. You can also pass arguments to tests, e.g., add_test(NAME test_math COMMAND test_math --verbose). CTest supports test fixtures: FIXTURES_SETUP, FIXTURES_CLEANUP. Example for a project with multiple test files:
```cmake cmake_minimum_required(VERSION 3.10) project(Calculator C)
enable_testing()
# CMocka tests add_executable(test_add test_add.c) target_link_libraries(test_add PRIVATE cmocka) add_test(NAME test_add COMMAND test_add)
# Unity tests add_executable(test_multiply test_multiply.c unity.c) add_test(NAME test_multiply COMMAND test_multiply) ```
Then build and run: mkdir build && cd build && cmake .. && make && ctest. CTest will run both tests and report results. Use ctest -V for verbose output.
CTEST_OUTPUT_ON_FAILURE=1 environment variable to see test output on failure in CI.5. Mocking External Dependencies with CMocka
Mocking is essential when testing code that calls external functions (e.g., database, network). CMocka provides to set return values and will_return() to retrieve them. Use mock()expect_* functions to verify arguments. Example: test a function process_data that calls .get_sensor_value()
```c #include <cmocka.h>
int get_sensor_value(void); // external
int process_data(void) { int val = get_sensor_value(); if (val < 0) return -1; return val * 2; }
// Mock implementation int __wrap_get_sensor_value(void) { return (int)mock(); }
static void test_process_data_positive(void **state) { will_return(__wrap_get_sensor_value, 10); assert_int_equal(process_data(), 20); }
static void test_process_data_negative(void **state) { will_return(__wrap_get_sensor_value, -1); assert_int_equal(process_data(), -1); }
int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_process_data_positive), cmocka_unit_test(test_process_data_negative), }; return cmocka_run_group_tests(tests, NULL, NULL); } ```
Link with -Wl,--wrap=get_sensor_value to replace the real function with the mock.
time()) to make tests deterministic.6. Testing Memory Management and Edge Cases
C programs are prone to memory errors. Unit tests should verify correct memory allocation, deallocation, and boundary conditions. Use CMocka's assert_ptr_equal to check pointers, and expect_any for flexible argument matching. For example, test a function that allocates a buffer and copies a string:
```c #include <cmocka.h> #include <stdlib.h> #include <string.h>
char duplicate_string(const char src) { if (src == NULL) return NULL; size_t len = strlen(src) + 1; char* dst = malloc(len); if (dst) memcpy(dst, src, len); return dst; }
static void test_duplicate_normal(void *state) { char result = duplicate_string("hello"); assert_non_null(result); assert_string_equal(result, "hello"); free(result); }
static void test_duplicate_empty(void *state) { char result = duplicate_string(""); assert_non_null(result); assert_string_equal(result, ""); free(result); }
static void test_duplicate_null(void **state) { assert_null(duplicate_string(NULL)); }
int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_duplicate_normal), cmocka_unit_test(test_duplicate_empty), cmocka_unit_test(test_duplicate_null), }; return cmocka_run_group_tests(tests, NULL, NULL); } ```
This tests null input, empty string, and normal case. Always free allocated memory in tests to avoid leaks.
7. Best Practices and CI Integration
- Write tests before code (TDD) or alongside it.
- Keep tests small and focused on one behavior.
- Use descriptive test names.
- Run tests frequently, ideally on every commit via CI.
- Measure code coverage (e.g., with gcov) to identify untested paths.
- Use CMocka's
for custom error messages.cmocka_print_error()
For CI integration, add a step to build and run tests. Example GitHub Actions workflow:
``yaml name: C Test on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install dependencies run: sudo apt-get install cmake libcmocka-dev - name: Build and test run: | mkdir build && cd build cmake .. make ctest --output-on-failure ``
This ensures tests run automatically on every push.
The Silent Data Corruption: A $1M Bug
- Always test boundary conditions (e.g., maximum input length).
- Use memory sanitizers (e.g., AddressSanitizer) in test builds.
- Integrate tests into CI to catch regressions early.
- Mock external dependencies to isolate unit tests.
- Review code coverage reports to identify untested paths.
gcc -fsanitize=address -g test.c -o test./test| File | Command / Code | Purpose |
|---|---|---|
| CMakeLists.txt | cmake_minimum_required(VERSION 3.10) | 1. Setting Up CTest with CMake |
| test_add.c | int add(int a, int b) { | 2. Writing Unit Tests with CMocka |
| test_multiply.c | void setUp(void) {} | 3. Lightweight Testing with Unity |
| test_process.c | int get_sensor_value(void); | 5. Mocking External Dependencies with CMocka |
| test_string_dup.c | char* duplicate_string(const char* src) { | 6. Testing Memory Management and Edge Cases |
| .github | name: C Test | 7. Best Practices and CI Integration |
Key takeaways
Common mistakes to avoid
5 patternsNot testing edge cases like NULL or empty inputs.
Forgetting to free memory in tests.
Using global state without resetting between tests.
Mocking too many functions, making tests brittle.
Not integrating tests into CI.
Interview Questions on This Topic
Explain how CTest integrates with CMake to run unit tests.
enable_testing() in CMakeLists.txt. Test executables are added with add_executable, and each is registered with add_test(NAME <name> COMMAND <executable>). Running ctest executes all registered tests and reports results.Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
That's C Basics. Mark it forged?
3 min read · try the examples if you haven't