Skip to content

08 · Unit Testing C with Unity/CMocka

GoogleTest is C++ — it uses classes, templates, and exceptions. Plenty of code you'll test is plain C, sometimes compiled by a C-only toolchain for a microcontroller with 32 KB of RAM. This module covers the two dominant C test frameworks: Unity (tiny, embedded-friendly, zero dependencies) and CMocka (fuller featured, with mocking and leak detection built in).

1. Choosing a framework

Unity CMocka GoogleTest
Language C C C++
Size ~3 source files, a few KB Small library Large
Dependencies None libc (+ setjmp) C++ runtime, STL
Runs on bare metal Yes Usually Rarely
Built-in mocking Via CMock (separate) Yes Via GoogleMock
Leak detection No Yes No
Best for Firmware, MCU targets, minimal deps Host-side C testing, C with mocks C++ code

A common real-world split: Unity on the target, CMocka or GoogleTest on the host. If your C code is portable enough to compile for your PC, you can also just test it with GoogleTest — C++ can call C directly, and that's covered in section 5.

2. Unity — the minimal C framework

Unity is three files: unity.c, unity.h, unity_internals.h. You can literally copy them into your project.

git clone --depth 1 https://github.com/ThrowTheSwitch/Unity.git external/Unity

The code under test

src/temperature.h:

#ifndef TEMPERATURE_H
#define TEMPERATURE_H

/* Parses a decimal integer temperature in the inclusive range -40..125.
   On success writes the value to *out_celsius and returns 0.
   Returns -1 if text is NULL, out_celsius is NULL, or text is not a
   valid decimal integer. Returns -2 if the value is out of range.
   On any error, *out_celsius is left unmodified. */
int parse_temperature(const char *text, int *out_celsius);

#endif /* TEMPERATURE_H */

src/temperature.c:

#include "temperature.h"
#include <stdlib.h>
#include <errno.h>
#include <limits.h>

int parse_temperature(const char *text, int *out_celsius) {
    if (text == NULL || out_celsius == NULL) {
        return -1;
    }
    if (text[0] == '\0') {
        return -1;
    }

    char *end = NULL;
    errno = 0;
    long value = strtol(text, &end, 10);   /* strtol, not atoi -- atoi
                                              has no error reporting and
                                              overflow is UB */
    if (errno == ERANGE || *end != '\0') {
        return -1;                          /* trailing junk or out of long */
    }
    if (value < -40 || value > 125) {
        return -2;
    }

    *out_celsius = (int)value;
    return 0;
}

strtol over atoi is itself a testability decision

atoi("99999999999999") is undefined behaviour and reports nothing. strtol sets errno and tells you where parsing stopped. When you review a C codebase as a tester, atoi/atof calls on untrusted input are a reportable finding — they make correct error handling impossible.

The Unity test file

tests/test_temperature.c:

#include "unity.h"
#include "temperature.h"

/* Unity requires these two, even if empty. They run before/after EACH test. */
void setUp(void)    { }
void tearDown(void) { }

static void test_parses_nominal_value(void) {
    int out = 0;
    TEST_ASSERT_EQUAL_INT(0, parse_temperature("25", &out));
    TEST_ASSERT_EQUAL_INT(25, out);
}

static void test_parses_negative_value(void) {
    int out = 0;
    TEST_ASSERT_EQUAL_INT(0, parse_temperature("-15", &out));
    TEST_ASSERT_EQUAL_INT(-15, out);
}

static void test_accepts_lower_boundary(void) {
    int out = 0;
    TEST_ASSERT_EQUAL_INT(0, parse_temperature("-40", &out));
    TEST_ASSERT_EQUAL_INT(-40, out);
}

static void test_accepts_upper_boundary(void) {
    int out = 0;
    TEST_ASSERT_EQUAL_INT(0, parse_temperature("125", &out));
    TEST_ASSERT_EQUAL_INT(125, out);
}

static void test_rejects_below_lower_boundary(void) {
    int out = 0x5A5A5A5A;                       /* sentinel */
    TEST_ASSERT_EQUAL_INT(-2, parse_temperature("-41", &out));
    TEST_ASSERT_EQUAL_INT(0x5A5A5A5A, out);     /* must be unmodified */
}

static void test_rejects_above_upper_boundary(void) {
    int out = 0x5A5A5A5A;
    TEST_ASSERT_EQUAL_INT(-2, parse_temperature("126", &out));
    TEST_ASSERT_EQUAL_INT(0x5A5A5A5A, out);
}

static void test_rejects_non_numeric_text(void) {
    int out = 0x5A5A5A5A;
    TEST_ASSERT_EQUAL_INT(-1, parse_temperature("abc", &out));
    TEST_ASSERT_EQUAL_INT(0x5A5A5A5A, out);
}

static void test_rejects_empty_string(void) {
    int out = 0x5A5A5A5A;
    TEST_ASSERT_EQUAL_INT(-1, parse_temperature("", &out));
    TEST_ASSERT_EQUAL_INT(0x5A5A5A5A, out);
}

static void test_rejects_null_text(void) {
    int out = 0x5A5A5A5A;
    TEST_ASSERT_EQUAL_INT(-1, parse_temperature(NULL, &out));
    TEST_ASSERT_EQUAL_INT(0x5A5A5A5A, out);
}

static void test_rejects_null_output_pointer(void) {
    TEST_ASSERT_EQUAL_INT(-1, parse_temperature("25", NULL));
}

static void test_rejects_trailing_junk(void) {
    int out = 0x5A5A5A5A;
    TEST_ASSERT_EQUAL_INT(-1, parse_temperature("25x", &out));
    TEST_ASSERT_EQUAL_INT(0x5A5A5A5A, out);
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_parses_nominal_value);
    RUN_TEST(test_parses_negative_value);
    RUN_TEST(test_accepts_lower_boundary);
    RUN_TEST(test_accepts_upper_boundary);
    RUN_TEST(test_rejects_below_lower_boundary);
    RUN_TEST(test_rejects_above_upper_boundary);
    RUN_TEST(test_rejects_non_numeric_text);
    RUN_TEST(test_rejects_empty_string);
    RUN_TEST(test_rejects_null_text);
    RUN_TEST(test_rejects_null_output_pointer);
    RUN_TEST(test_rejects_trailing_junk);
    return UNITY_END();     /* returns the failure count -- CTest reads it */
}

The manual RUN_TEST list is Unity's main ergonomic cost — C has no way to auto-register functions portably. (Unity ships a Ruby script, generate_test_runner.rb, that generates this main() for you; on embedded projects that's usually wired into the build.)

Build and run

gcc -std=c11 -Wall -Wextra -g \
    -I src -I external/Unity/src \
    -o test_temperature \
    tests/test_temperature.c src/temperature.c external/Unity/src/unity.c

./test_temperature
tests/test_temperature.c:12:test_parses_nominal_value:PASS
tests/test_temperature.c:18:test_parses_negative_value:PASS
tests/test_temperature.c:24:test_accepts_lower_boundary:PASS
tests/test_temperature.c:30:test_accepts_upper_boundary:PASS
tests/test_temperature.c:36:test_rejects_below_lower_boundary:PASS
tests/test_temperature.c:43:test_rejects_above_upper_boundary:PASS
tests/test_temperature.c:50:test_rejects_non_numeric_text:PASS
tests/test_temperature.c:57:test_rejects_empty_string:PASS
tests/test_temperature.c:64:test_rejects_null_text:PASS
tests/test_temperature.c:71:test_rejects_null_output_pointer:PASS
tests/test_temperature.c:76:test_rejects_trailing_junk:PASS

-----------------------
11 Tests 0 Failures 0 Ignored
OK

A failure looks like:

tests/test_temperature.c:30:test_accepts_upper_boundary:FAIL: Expected 0 Was -2

-----------------------
11 Tests 1 Failures 0 Ignored
FAIL

Unity assertion macros

Macro Checks
TEST_ASSERT_TRUE(c) / TEST_ASSERT_FALSE(c) Boolean
TEST_ASSERT_EQUAL_INT(exp, act) Signed integers
TEST_ASSERT_EQUAL_UINT(exp, act) Unsigned integers
TEST_ASSERT_EQUAL_INT8/16/32/64 Fixed-width integers
TEST_ASSERT_EQUAL_HEX8/16/32 Integers, printed as hex on failure
TEST_ASSERT_EQUAL_PTR(exp, act) Pointers
TEST_ASSERT_NULL(p) / TEST_ASSERT_NOT_NULL(p) Null checks
TEST_ASSERT_EQUAL_STRING(exp, act) C strings by content
TEST_ASSERT_EQUAL_MEMORY(exp, act, len) Raw byte comparison
TEST_ASSERT_EQUAL_INT_ARRAY(exp, act, n) Element-wise array comparison
TEST_ASSERT_FLOAT_WITHIN(delta, exp, act) Float with tolerance
TEST_ASSERT_BIT_HIGH(bit, value) / _BIT_LOW Individual bits — very handy for register testing
TEST_FAIL_MESSAGE("...") Unconditional failure
TEST_IGNORE_MESSAGE("...") Skip with a reason

Note the fixed-width and hex variants. In embedded C you're constantly comparing uint8_t register values, and TEST_ASSERT_EQUAL_HEX8(0xA5, reg) prints Expected 0xA5 Was 0x25 — far more readable than decimal.

Every Unity assertion is an ASSERT, not an EXPECT

Unity uses longjmp to abort the test function on the first failed assertion. There is no "record and continue" mode. So each test reports at most one failure, and you should keep tests small and focused.

3. CMocka — richer C testing

CMocka adds fixtures, grouped test registration, and — its headline feature — built-in memory leak and buffer-overflow detection.

# Ubuntu/Debian
sudo apt install libcmocka-dev
# macOS
brew install cmocka
# Fedora
sudo dnf install libcmocka-devel

A CMocka test file

tests/test_temperature_cmocka.c:

#include <stdarg.h>     /* these four includes must come BEFORE cmocka.h */
#include <stddef.h>
#include <setjmp.h>
#include <stdint.h>
#include <cmocka.h>

#include "temperature.h"

static void test_parses_nominal_value(void **state) {
    (void)state;                       /* unused -- silences -Wunused-parameter */
    int out = 0;
    assert_int_equal(parse_temperature("25", &out), 0);
    assert_int_equal(out, 25);
}

static void test_accepts_upper_boundary(void **state) {
    (void)state;
    int out = 0;
    assert_int_equal(parse_temperature("125", &out), 0);
    assert_int_equal(out, 125);
}

static void test_rejects_above_upper_boundary(void **state) {
    (void)state;
    int out = 0x5A5A5A5A;
    assert_int_equal(parse_temperature("126", &out), -2);
    assert_int_equal(out, 0x5A5A5A5A);
}

static void test_rejects_null_text(void **state) {
    (void)state;
    int out = 0x5A5A5A5A;
    assert_int_equal(parse_temperature(NULL, &out), -1);
    assert_int_equal(out, 0x5A5A5A5A);
}

int main(void) {
    const struct CMUnitTest tests[] = {
        cmocka_unit_test(test_parses_nominal_value),
        cmocka_unit_test(test_accepts_upper_boundary),
        cmocka_unit_test(test_rejects_above_upper_boundary),
        cmocka_unit_test(test_rejects_null_text),
    };
    return cmocka_run_group_tests(tests, NULL, NULL);
}
gcc -std=c11 -Wall -Wextra -g -I src -o test_temp_cmocka \
    tests/test_temperature_cmocka.c src/temperature.c -lcmocka

./test_temp_cmocka
[==========] tests: Running 4 test(s).
[ RUN      ] test_parses_nominal_value
[       OK ] test_parses_nominal_value
[ RUN      ] test_accepts_upper_boundary
[       OK ] test_accepts_upper_boundary
[ RUN      ] test_rejects_above_upper_boundary
[       OK ] test_rejects_above_upper_boundary
[ RUN      ] test_rejects_null_text
[       OK ] test_rejects_null_text
[==========] tests: 4 test(s) run.
[  PASSED  ] 4 test(s).

The include order is mandatory

cmocka.h depends on stdarg.h, stddef.h, setjmp.h and stdint.h being included first and will not compile otherwise. This trips up everyone on their first CMocka file.

CMocka assertion macros

Macro Checks
assert_true(c) / assert_false(c) Boolean
assert_int_equal(a, b) / assert_int_not_equal Integers
assert_string_equal(a, b) / assert_string_not_equal C strings
assert_memory_equal(a, b, size) Raw bytes
assert_ptr_equal(a, b) Pointers
assert_null(p) / assert_non_null(p) Null checks
assert_in_range(v, min, max) Inclusive range
assert_float_equal(a, b, epsilon) Float with tolerance
fail() / fail_msg("...") Unconditional
skip() Skip the current test

Fixtures with per-test state

CMocka's void **state parameter is a slot for per-test data, managed by setup and teardown functions:

typedef struct {
    RingBuffer *buffer;
} TestState;

static int setup_ring(void **state) {
    TestState *s = malloc(sizeof(TestState));
    if (s == NULL) return -1;              /* non-zero aborts this test */
    s->buffer = ring_create(64);
    *state = s;
    return 0;
}

static int teardown_ring(void **state) {
    TestState *s = *state;
    ring_destroy(s->buffer);
    free(s);
    return 0;
}

static void test_new_buffer_is_empty(void **state) {
    TestState *s = *state;
    assert_int_equal(ring_size(s->buffer), 0);
}

int main(void) {
    const struct CMUnitTest tests[] = {
        cmocka_unit_test_setup_teardown(test_new_buffer_is_empty,
                                        setup_ring, teardown_ring),
    };
    return cmocka_run_group_tests(tests, NULL, NULL);
}

Built-in leak detection

This is the reason to prefer CMocka over Unity for host-side C testing. Replace malloc/free/calloc/realloc with CMocka's instrumented versions:

/* Put these in the code under test, or define them project-wide when
   building for tests. CMocka provides _test_malloc / _test_free. */
#ifdef UNIT_TESTING
extern void* _test_malloc(size_t size, const char* file, int line);
extern void  _test_free(void* ptr, const char* file, int line);
#define malloc(size) _test_malloc(size, __FILE__, __LINE__)
#define free(ptr)    _test_free(ptr, __FILE__, __LINE__)
#endif

Now a test whose code under test leaks fails automatically:

[  ERROR   ] --- Blocks allocated...
  0x5601a2c0 : src/ring_buffer.c:23
  ERROR: test_create_buffer leaked 1 block(s)
[  FAILED  ] test_create_buffer

CMocka also plants guard bytes around each allocation, so writing past the end of a malloc'd block is caught at free time. It is not a replacement for AddressSanitizer (Level 2), but it is free, works everywhere, and catches the two most common C memory bugs with no extra build configuration.

4. Wiring C tests into CMake and CTest

cmake_minimum_required(VERSION 3.14)
project(temperature LANGUAGES C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
add_compile_options(-Wall -Wextra)

add_library(temperature src/temperature.c)
target_include_directories(temperature PUBLIC src)

enable_testing()

# --- Unity ---
add_library(unity external/Unity/src/unity.c)
target_include_directories(unity PUBLIC external/Unity/src)

add_executable(test_temperature_unity tests/test_temperature.c)
target_link_libraries(test_temperature_unity PRIVATE temperature unity)
add_test(NAME UnityTemperatureTests COMMAND test_temperature_unity)

# --- CMocka (optional -- only if installed) ---
find_library(CMOCKA_LIB cmocka)
if(CMOCKA_LIB)
    add_executable(test_temperature_cmocka tests/test_temperature_cmocka.c)
    target_link_libraries(test_temperature_cmocka PRIVATE temperature ${CMOCKA_LIB})
    add_test(NAME CMockaTemperatureTests COMMAND test_temperature_cmocka)
else()
    message(STATUS "cmocka not found -- skipping CMocka tests")
endif()
cmake -S . -B build && cmake --build build
ctest --test-dir build --output-on-failure
    Start 1: UnityTemperatureTests
1/2 Test #1: UnityTemperatureTests ............   Passed    0.00 sec
    Start 2: CMockaTemperatureTests
2/2 Test #2: CMockaTemperatureTests ...........   Passed    0.00 sec

100% tests passed, 0 tests failed out of 2

Both frameworks return a non-zero exit code on failure (UNITY_END() returns the failure count; cmocka_run_group_tests returns the number of failed tests), so the CTest contract from Module 6 works unchanged.

5. Testing C code from GoogleTest

If your C code compiles for the host, you can skip the C frameworks entirely and use GoogleTest. The one thing you must get right is extern "C":

#include <gtest/gtest.h>

extern "C" {
#include "temperature.h"      // C header -- prevents C++ name mangling
}

TEST(ParseTemperatureTest, AcceptsUpperBoundary) {
    int out = 0;
    EXPECT_EQ(parse_temperature("125", &out), 0);
    EXPECT_EQ(out, 125);
}

Without extern "C", the C++ compiler mangles parse_temperature into a C++ symbol and the linker cannot find the C implementation — producing an undefined reference to parse_temperature(char const*, int*) error that confuses everyone the first time.

A more robust habit is to put the guard in the header itself, so callers don't have to remember:

#ifndef TEMPERATURE_H
#define TEMPERATURE_H

#ifdef __cplusplus
extern "C" {
#endif

int parse_temperature(const char *text, int *out_celsius);

#ifdef __cplusplus
}
#endif

#endif /* TEMPERATURE_H */
Approach Choose when
Unity Target is an MCU; no C++ runtime; minimal footprint required
CMocka Host-side C testing; you want leak detection and mocking
GoogleTest + extern "C" The C code builds for the host and the project already uses gtest for C++

Mixing is fine and common: Unity for the on-target smoke suite, GoogleTest for the host-side bulk.

6. Static functions — the C testability problem

C's static keyword gives a function internal linkage, so a test file cannot call it:

/* parser.c */
static int validate_checksum(const uint8_t *frame, size_t len) { ... }

Three standard workarounds:

Approach How Trade-off
#include the .c file Test file does #include "parser.c" instead of the header Works everywhere, no production change; but you must not also link parser.c, or you get duplicate symbols
Conditional static #ifdef UNIT_TESTING #define STATIC #else #define STATIC static #endif, then declare STATIC int validate_checksum(...) Production keeps internal linkage; but the test build is not byte-identical to production
Don't test it directly Reach it through the public function that calls it Purest black-box approach; but hard to cover error paths
/* testable_static.h -- the conditional-static idiom */
#ifdef UNIT_TESTING
#define STATIC
#else
#define STATIC static
#endif

The third option is the right default. Reach for the first two when a static helper contains genuinely intricate logic (a checksum, a state transition table) whose error paths you cannot reach from outside. If you find yourself needing them everywhere, that's a design finding worth raising: the file is doing too much and wants splitting.

7. Frameworks compared on the same test

The same boundary case, three ways:

static void test_accepts_upper_boundary(void) {
    int out = 0;
    TEST_ASSERT_EQUAL_INT(0, parse_temperature("125", &out));
    TEST_ASSERT_EQUAL_INT(125, out);
}
/* plus: RUN_TEST(test_accepts_upper_boundary); in main() */
static void test_accepts_upper_boundary(void **state) {
    (void)state;
    int out = 0;
    assert_int_equal(parse_temperature("125", &out), 0);
    assert_int_equal(out, 125);
}
/* plus: cmocka_unit_test(test_accepts_upper_boundary), in the array */
TEST(ParseTemperatureTest, AcceptsUpperBoundary) {
    int out = 0;
    EXPECT_EQ(parse_temperature("125", &out), 0);
    EXPECT_EQ(out, 125);
}
/* no registration needed */

Note the argument order difference: Unity and CMocka put expected first (TEST_ASSERT_EQUAL_INT(0, actual)), GoogleTest puts actual first (EXPECT_EQ(actual, 0)). Getting this backwards doesn't break the test, but it inverts every failure message — "Expected 3 Was 0" when you meant the opposite. Check the convention before you write a hundred tests.

How It Actually Works: registration without C++ classes

Unity and CMocka solve the same "find and run every test" problem as GoogleTest, but C has no classes or static-initializer-before-main idiom in the same form, so they do it differently — and less automatically.

  • Unity has no runtime registry at all. RUN_TEST(test_name) is a macro that directly calls test_name() wrapped in a setUp()/tearDown() pair and increments Unity's global pass/fail counters based on whether a TEST_ASSERT_* macro set its internal failure flag via longjmp. There is no discovery step — the list of tests that runs is exactly and only the RUN_TEST() lines you wrote in main(). This is why forgetting to add a new test to the runner file means it silently never executes, with no error — a real footgun Unity projects have to guard against with generator scripts.
  • TEST_ASSERT_* failure uses setjmp/longjmp, not exceptions. C has no stack unwinding mechanism, so when an assertion fails, Unity calls longjmp back to a setjmp point captured at the start of the current test, which abandons the rest of the test function's execution (any free() calls after the assertion never run — this is the practical reason CMocka tests, and careful Unity tests, do cleanup in tearDown() rather than after assertions).
  • CMocka does have a registry, built from an array of function pointers. cmocka_unit_test(fn) isn't a macro that registers anything by itself — it expands to a struct CMUnitTest literal { "fn", fn, NULL, NULL, NULL } that you place into an array, which cmocka_run_group_tests() then iterates with an ordinary for loop, fork()-ing a child process per test when CMocka's process-isolation mode is enabled so a SIGSEGV in one test case doesn't take the whole suite down with it — closer in spirit to how ctest isolates whole binaries than to GoogleTest's in-process model.
  • CMocka's mock return values are a manually-managed queue, not vtable interception. will_return() pushes a value onto a per-symbol linked list; mock() pops the next value off that list. There's no compiler magic — it works only because your production code calls mock() directly (via a macro or #ifdef UNIT_TESTING seam), which is the real reason "mockable" C code needs a function-pointer or preprocessor seam that C++ virtual dispatch gets for free (see Level 2's GoogleMock module for that contrast).

Exercise

Build a complete C unit test suite for a ring buffer.

  1. Implement src/ring_buffer.c / .h with this API:

    typedef struct RingBuffer RingBuffer;
    
    RingBuffer *ring_create(size_t capacity);   /* NULL on failure */
    void        ring_destroy(RingBuffer *rb);   /* safe on NULL */
    /* Writes up to n bytes; returns bytes written, or -1 on NULL args. */
    int         ring_write(RingBuffer *rb, const uint8_t *src, size_t n);
    /* Reads up to n bytes; returns bytes read, or -1 on NULL args. */
    int         ring_read(RingBuffer *rb, uint8_t *dest, size_t n);
    size_t      ring_size(const RingBuffer *rb);
    size_t      ring_capacity(const RingBuffer *rb);
    
  2. Write a Unity suite with at least 15 tests, driven by the boundary table you built in Module 5's exercise. It must include:

  3. ring_write of 0 bytes, 1 byte, capacity−1, capacity, capacity+1
  4. a write-then-read round trip verified with TEST_ASSERT_EQUAL_MEMORY
  5. wraparound: fill, read half, write more so the data wraps the internal index, then read everything and verify the bytes and order
  6. NULL for each pointer parameter, one at a time
  7. ring_destroy(NULL) must not crash

  8. Write a CMocka version of at least five of those tests, and enable CMocka's leak detection by defining UNIT_TESTING and the malloc/free macros in ring_buffer.c. Then deliberately remove the free() inside ring_destroy and confirm CMocka reports the leak with a file and line number. Restore it afterwards.

  9. Register both suites with CTest as in section 4 and confirm ctest --test-dir build -N lists two tests.

  10. Add a static helperstatic size_t bytes_free(const RingBuffer *rb) — and test it using the conditional-STATIC idiom from section 6. Then write two sentences arguing either for or against doing this, given that you could instead infer free space from ring_capacity() - ring_size().

  11. Cross-check with GoogleTest. Add extern "C" guards to ring_buffer.h and write three of the same tests in a GoogleTest file. Confirm all three suites pass, and note in one sentence which framework gave you the most useful failure message when you broke ring_write on purpose.