CMock and Unity: Embedded C Testing Frameworks Guide

CMock and Unity: Embedded C Testing Frameworks Guide

Unity is a lightweight C testing framework built for embedded systems. CMock auto-generates mock implementations from C headers, enabling you to test firmware logic without real hardware. Together they form the standard testing stack for embedded C — run on host before flashing to device.


The Embedded Testing Problem

Testing embedded firmware is hard:

  • Hardware may not be available (pre-silicon, lab kit shared by a team)
  • Hardware interactions are non-deterministic (timing, hardware state)
  • Flashing a device for every test run is too slow for TDD
  • Device failures are hard to debug remotely

The solution: run firmware logic tests on the host machine, isolating hardware access via mocks. Unit test the logic. Test hardware integration separately on the actual device.

Unity and CMock enable this workflow.


Unity: The Test Framework

Unity is a C testing framework designed for small footprint and portability:

  • Single .c and .h file
  • No dynamic memory
  • Works on any C89/C90 platform
  • Readable output on UART for bare-metal debugging

Installation

git clone https://github.com/ThrowTheSwitch/Unity.git
<span class="hljs-comment"># or via CMake FetchContent
include(FetchContent)
FetchContent_Declare(
    unity
    GIT_REPOSITORY https://github.com/ThrowTheSwitch/Unity.git
    GIT_TAG v2.5.2
)
FetchContent_MakeAvailable(unity)

Basic Unity Test

// tests/test_led_driver.c
#include "unity.h"
#include "led_driver.h"

void setUp(void) {
    // Called before each test
    LedDriver_Create();
}

void tearDown(void) {
    // Called after each test
    LedDriver_Destroy();
}

void test_LedsAreOffAfterCreate(void) {
    // All LEDs should be off on initialization
    for (int i = 1; i <= 16; i++) {
        TEST_ASSERT_EQUAL(0, LedDriver_IsOn(i));
    }
}

void test_TurnOnLedOne(void) {
    LedDriver_TurnOn(1);
    TEST_ASSERT_EQUAL(1, LedDriver_IsOn(1));
}

void test_TurnOnLedDoesNotAffectOtherLeds(void) {
    LedDriver_TurnOn(1);
    
    TEST_ASSERT_EQUAL(1, LedDriver_IsOn(1));
    TEST_ASSERT_EQUAL(0, LedDriver_IsOn(2));  // Others unaffected
}

void test_TurnOffLed(void) {
    LedDriver_TurnOn(1);
    LedDriver_TurnOff(1);
    TEST_ASSERT_EQUAL(0, LedDriver_IsOn(1));
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_LedsAreOffAfterCreate);
    RUN_TEST(test_TurnOnLedOne);
    RUN_TEST(test_TurnOnLedDoesNotAffectOtherLeds);
    RUN_TEST(test_TurnOffLed);
    return UNITY_END();
}

Unity Assertions

// Equality
TEST_ASSERT_EQUAL(expected, actual);          // int
TEST_ASSERT_EQUAL_INT(expected, actual);
TEST_ASSERT_EQUAL_UINT8(expected, actual);
TEST_ASSERT_EQUAL_HEX8(expected, actual);     // shows hex in failure
TEST_ASSERT_EQUAL_HEX16(expected, actual);
TEST_ASSERT_EQUAL_HEX32(expected, actual);

// Floating point
TEST_ASSERT_EQUAL_FLOAT(expected, actual);
TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual);

// Strings
TEST_ASSERT_EQUAL_STRING(expected, actual);

// Pointers
TEST_ASSERT_NULL(pointer);
TEST_ASSERT_NOT_NULL(pointer);
TEST_ASSERT_EQUAL_PTR(expected, actual);

// Boolean
TEST_ASSERT_TRUE(condition);
TEST_ASSERT_FALSE(condition);

// Arrays
TEST_ASSERT_EQUAL_INT_ARRAY(expected, actual, num_elements);
TEST_ASSERT_EQUAL_UINT8_ARRAY(expected, actual, num_elements);

// Bit fields
TEST_ASSERT_BITS(mask, expected, actual);  // check only masked bits
TEST_ASSERT_BIT_HIGH(bit, actual);         // specific bit is 1
TEST_ASSERT_BIT_LOW(bit, actual);          // specific bit is 0

Failure output:

tests/test_led_driver.c:28: FAILED
Expected 0x01 Was 0x00

The Problem with Real Hardware in Tests

Consider a temperature sensor driver:

// temperature_sensor.h
typedef struct {
    float temperature;
    uint8_t status;
} SensorReading;

SensorReading TemperatureSensor_Read(void);
// thermostat.c — the logic under test
#include "temperature_sensor.h"
#include "heater.h"

void Thermostat_Update(float target_temp) {
    SensorReading reading = TemperatureSensor_Read();  // hardware call
    
    if (reading.temperature < target_temp) {
        Heater_TurnOn();
    } else {
        Heater_TurnOff();
    }
}

To test Thermostat_Update, you need to control what TemperatureSensor_Read() returns. But the real implementation talks to I2C hardware.

CMock generates a mock that lets you control this.


CMock: Auto-Generated Mocks

CMock generates mock implementations from C headers. Given temperature_sensor.h, CMock generates:

// Generated: mock_temperature_sensor.h
void TemperatureSensor_Read_ExpectAndReturn(SensorReading return_value);
void TemperatureSensor_Read_Expect(void);
void TemperatureSensor_Read_StubWithCallback(
    CMOCK_TemperatureSensor_Read_CALLBACK callback
);

Installation

gem install cmock  # CMock is Ruby-based
<span class="hljs-comment"># or use Ceedling (the full test toolchain)
gem install ceedling

Generating Mocks

ruby /path/to/CMock/lib/cmock.rb src/temperature_sensor.h
# Generates: mocks/mock_temperature_sensor.h
<span class="hljs-comment">#            mocks/mock_temperature_sensor.c

Using CMock in Tests

// tests/test_thermostat.c
#include "unity.h"
#include "mock_temperature_sensor.h"  // generated
#include "mock_heater.h"              // generated
#include "thermostat.h"

void setUp(void) {}
void tearDown(void) {}

void test_ThermostatTurnsOnHeaterWhenCold(void) {
    // Arrange: sensor returns 18°C, target is 22°C
    SensorReading cold_reading = {.temperature = 18.0f, .status = 0};
    TemperatureSensor_Read_ExpectAndReturn(cold_reading);
    
    // Heater should be turned on
    Heater_TurnOn_Expect();
    
    // Act
    Thermostat_Update(22.0f);
    
    // CMock automatically verifies all expectations were called
}

void test_ThermostatTurnsOffHeaterWhenWarm(void) {
    SensorReading warm_reading = {.temperature = 25.0f, .status = 0};
    TemperatureSensor_Read_ExpectAndReturn(warm_reading);
    
    Heater_TurnOff_Expect();
    
    Thermostat_Update(22.0f);
}

void test_ThermostatIgnoresBadSensorReading(void) {
    SensorReading error_reading = {.temperature = -273.0f, .status = 0xFF};
    TemperatureSensor_Read_ExpectAndReturn(error_reading);
    
    // Heater should not be called on bad reading
    // CMock verifies no Heater_* calls were made
    
    Thermostat_Update(22.0f);
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_ThermostatTurnsOnHeaterWhenCold);
    RUN_TEST(test_ThermostatTurnsOffHeaterWhenWarm);
    RUN_TEST(test_ThermostatIgnoresBadSensorReading);
    return UNITY_END();
}

CMock Expectation Patterns

// Expect one call, return a value
TemperatureSensor_Read_ExpectAndReturn(reading);

// Expect with specific argument
UART_Write_Expect(0x48);  // expect exactly 0x48 to be written

// Expect any argument
UART_Write_IgnoreArg_byte();  // don't care what byte is written

// Expect N calls
TemperatureSensor_Read_ExpectAndReturn(reading);
TemperatureSensor_Read_ExpectAndReturn(reading);  // call twice

// Stub (return value without strict call count)
TemperatureSensor_Read_Stub(my_sensor_callback);

// Ignore all calls to a function
UART_Write_Ignore();

// Expect call that sets an output parameter
void DMA_Read_ExpectWithArrayAndReturn(
    uint8_t* buf, int buf_len,
    int return_code
);

Ceedling: The Full Toolchain

Ceedling ties Unity, CMock, and CTest together into a convention-driven project structure:

gem install ceedling
ceedling new my_firmware_project
cd my_firmware_project

Structure:

my_firmware_project/
├── project.yml          # Ceedling config
├── src/
│   ├── temperature_sensor.c
│   └── thermostat.c
├── include/
│   ├── temperature_sensor.h
│   └── thermostat.h
└── test/
    └── test_thermostat.c
# project.yml
:project:
  :build_root: build
  :test_file_prefix: test_

:paths:
  :source: ["src/**"]
  :include: ["include/**"]
  :test: ["test/**"]

:cmock:
  :mock_prefix: mock_
  :plugins: [:expect, :return_thru_ptr]

Run tests:

ceedling test:all          <span class="hljs-comment"># run all tests
ceedling <span class="hljs-built_in">test:thermostat   <span class="hljs-comment"># run specific test file
ceedling <span class="hljs-built_in">test:all -v       <span class="hljs-comment"># verbose

Ceedling auto-generates mocks when test files #include "mock_*.h".


Dependency Injection for Testability

The real power of Unity/CMock comes from designing code for testability. Use function pointers or struct-based dependency injection:

// Instead of direct hardware calls:
void Thermostat_Update(float target) {
    SensorReading r = TemperatureSensor_Read();  // hard to test
    ...
}

// Use dependency injection via function pointers:
typedef SensorReading (*SensorReadFn)(void);
typedef void (*HeaterControlFn)(void);

typedef struct {
    SensorReadFn read_sensor;
    HeaterControlFn turn_on;
    HeaterControlFn turn_off;
} ThermostatDeps;

void Thermostat_Update(ThermostatDeps* deps, float target) {
    SensorReading r = deps->read_sensor();
    if (r.temperature < target) {
        deps->turn_on();
    } else {
        deps->turn_off();
    }
}

In production:

ThermostatDeps real_deps = {
    .read_sensor = TemperatureSensor_Read,
    .turn_on = Heater_TurnOn,
    .turn_off = Heater_TurnOff,
};
Thermostat_Update(&real_deps, 22.0f);

In tests (no CMock needed):

static int heater_on_called = 0;
static SensorReading fake_sensor_reading;

SensorReading fake_read_sensor(void) { return fake_sensor_reading; }
void fake_turn_on(void) { heater_on_called = 1; }
void fake_turn_off(void) { heater_on_called = 0; }

void test_HeaterOnWhenCold(void) {
    fake_sensor_reading = (SensorReading){.temperature = 18.0f};
    ThermostatDeps deps = {fake_read_sensor, fake_turn_on, fake_turn_off};
    
    Thermostat_Update(&deps, 22.0f);
    
    TEST_ASSERT_EQUAL(1, heater_on_called);
}

Testing Hardware Registers

Embedded code often directly manipulates memory-mapped registers. Test by pointing the register address to a variable:

// led_driver.c
#define LED_REGISTER (*((volatile uint16_t*)0x40021018))

void LedDriver_TurnOn(int led) {
    LED_REGISTER |= (1 << (led - 1));
}

For testing, use a virtual register:

// tests/test_led_driver.c
#include "unity.h"

// Virtual register for testing
static uint16_t virtual_led_register;

// Override the macro for tests
#define LED_REGISTER virtual_led_register
#include "led_driver.c"  // include source directly with macro override

void setUp(void) {
    virtual_led_register = 0;  // clear on each test
}

void test_TurnOnLedOneSetsCorrectBit(void) {
    LedDriver_TurnOn(1);
    TEST_ASSERT_EQUAL_HEX16(0x0001, virtual_led_register);
}

void test_TurnOnLed8SetsCorrectBit(void) {
    LedDriver_TurnOn(8);
    TEST_ASSERT_EQUAL_HEX16(0x0080, virtual_led_register);
}

void test_TurnOnMultipleLeds(void) {
    LedDriver_TurnOn(1);
    LedDriver_TurnOn(2);
    TEST_ASSERT_EQUAL_HEX16(0x0003, virtual_led_register);
}

CI for Embedded Tests

Run host-side tests in CI like any C project:

name: Embedded C Tests

on: [push, pull_request]

jobs:
  unity-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install dependencies
        run: |
          sudo apt install -y cmake gcc
          gem install ceedling
      
      - name: Run Unity tests
        run: ceedling test:all
      
      - name: Run with CMake
        run: |
          cmake -B build
          cmake --build build
          cd build && ctest --output-on-failure

Unity vs Google Test for Embedded

Unity Google Test
Language C (pure) C++
Footprint ~3KB Much larger
Dynamic memory None Required
Bare-metal Yes Limited
Mocking CMock GMock
Toolchain integration Ceedling CMake

Use Unity + CMock for: microcontrollers, RTOS environments, bare-metal firmware. Use gtest + GMock for: Linux-based embedded, automotive ECUs with full OS, host-side testing of portable C++ code.


Next Steps

  • Try Ceedling for convention-driven project setup — it's the fastest way to start
  • Apply dependency injection — redesign hardware access to use function pointers before adding tests
  • Check GMock if you're testing C++ firmware code
  • Add AddressSanitizer to your host-side test builds — see the memory testing guide

Read more