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
.cand.hfile - 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 FetchContentinclude(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 0Failure output:
tests/test_led_driver.c:28: FAILED
Expected 0x01 Was 0x00The 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 ceedlingGenerating 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.cUsing 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_projectStructure:
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"># verboseCeedling 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-failureUnity 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