Skip to content
Home » Software Design » Testing Embedded C Code — Unit Tests, Mocks, and Frameworks

Testing Embedded C Code — Unit Tests, Mocks, and Frameworks

Testing Embedded C featured image with blue-purple background, Quality badge, Ts icon, Unit Tests Mocks and Frameworks subtitle in the Software Design in C series by NerdyElectronics
Software Design Principles in C
Part 28 of 31View Full Path →

⚡ KEY TAKEAWAYS

  • Most embedded C logic can be tested on a PC without hardware
  • Use mocks to replace hardware dependencies during testing
  • Unity + CMock is the standard framework combination for embedded C
  • Structure code for testability: separate logic from hardware, use dependency inversion
  • Run unit tests frequently and automatically — catch bugs before they reach hardware

Why Test Embedded C Code?

Embedded code is hard to debug. You cannot always step through it with a debugger, and bugs in the field can mean product recalls, safety hazards, or damaged hardware. Testing on your development machine — before the code ever runs on target hardware — catches most bugs early, cheaply, and safely.

The key insight: if your code is well-structured with clean module boundaries and dependency inversion, you can test most of it on a PC without any hardware.

What Can You Test Without Hardware?

  • Business logic — calculations, state machines, protocol parsing, data validation
  • Module interfaces — does the API behave correctly for valid and invalid inputs?
  • Edge cases — buffer boundaries, overflow, timeout conditions, null pointers
  • State transitions — does the FSM handle all events in all states correctly?

What you cannot easily test on a PC: real timing, actual hardware registers, interrupt behavior. Those need integration testing on real hardware.

Example 1: Testing a Temperature Converter

This is the simplest case — pure logic with no hardware dependencies.

// temp_convert.h
float celsius_to_fahrenheit(float c);
float fahrenheit_to_celsius(float f);
float raw_adc_to_celsius(int raw, float vref, float offset);

// temp_convert.c
float celsius_to_fahrenheit(float c) {
    return c * 9.0f / 5.0f + 32.0f;
}

float fahrenheit_to_celsius(float f) {
    return (f - 32.0f) * 5.0f / 9.0f;
}

float raw_adc_to_celsius(int raw, float vref, float offset) {
    if (raw  4095) return -999.0f;  // error sentinel
    float voltage = (float)raw * vref / 4096.0f;
    return (voltage - offset) * 100.0f;
}

Test File

// test_temp_convert.c
#include "temp_convert.h"
#include <assert.h>
#include <math.h>
#include <stdio.h>

#define ASSERT_FLOAT_EQ(a, b) assert(fabs((a) - (b))  0.0f && t < 200.0f);

    // Out of range — should return error sentinel
    assert(raw_adc_to_celsius(-1, 3.3f, 0.5f) == -999.0f);
    assert(raw_adc_to_celsius(4096, 3.3f, 0.5f) == -999.0f);
    printf("PASS: raw_adc_boundaryn");
}

int main(void) {
    test_celsius_to_fahrenheit();
    test_raw_adc_boundary();
    printf("All tests passed.\n");
    return 0;
}

// Compile and run on PC:
// gcc -o test_temp test_temp_convert.c temp_convert.c -lm
// ./test_temp

Example 2: Testing with Mocks (Faking Hardware)

When your module calls hardware functions like adc_read(), you cannot run that on a PC. The solution: replace the real hardware call with a fake (mock) during testing.

// sensor.h
typedef enum { SENSOR_OK, SENSOR_ERR } SensorStatus;
SensorStatus sensor_read(float *out);

// sensor.c
#include "sensor.h"
#include "adc.h"  // hardware dependency

static int s_channel = 0;

SensorStatus sensor_read(float *out) {
    int raw = adc_read(s_channel);  // calls real hardware
    if (raw  4095) return SENSOR_ERR;
    *out = (float)raw * 3.3f / 4096.0f * 100.0f;
    return SENSOR_OK;
}

The Mock

// mock_adc.c — fake hardware for testing
static int mock_adc_value = 0;

void mock_adc_set_value(int val) {
    mock_adc_value = val;
}

int adc_read(int channel) {
    (void)channel;
    return mock_adc_value;  // return whatever the test sets
}

The Test

// test_sensor.c
#include "sensor.h"
#include <assert.h>
#include <stdio.h>

extern void mock_adc_set_value(int val);

void test_sensor_normal_reading(void) {
    mock_adc_set_value(2048);  // midpoint
    float value;
    assert(sensor_read(&value) == SENSOR_OK);
    assert(value > 0.0f);
    printf("PASS: normal readingn");
}

void test_sensor_error_on_bad_adc(void) {
    mock_adc_set_value(-1);  // simulate hardware error
    float value;
    assert(sensor_read(&value) == SENSOR_ERR);
    printf("PASS: error on bad ADCn");
}

int main(void) {
    test_sensor_normal_reading();
    test_sensor_error_on_bad_adc();
    printf("All tests passed.\n");
    return 0;
}

// Compile: link sensor.c with mock_adc.c instead of real adc.c
// gcc -o test_sensor test_sensor.c sensor.c mock_adc.c

The trick: at link time, you link mock_adc.c instead of the real adc.c. The sensor module calls adc_read() and gets the mock. No hardware needed.

Testing Frameworks for C

Unity (Recommended for Embedded)

Unity is a lightweight, single-header test framework designed for embedded C. It provides assertions, test runners, and output formatting.

#include "unity.h"
#include "temp_convert.h"

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

void test_boiling_point(void) {
    TEST_ASSERT_FLOAT_WITHIN(0.01f, 212.0f, celsius_to_fahrenheit(100.0f));
}

void test_freezing_point(void) {
    TEST_ASSERT_FLOAT_WITHIN(0.01f, 32.0f, celsius_to_fahrenheit(0.0f));
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_boiling_point);
    RUN_TEST(test_freezing_point);
    return UNITY_END();
}

CMock (Auto-Generated Mocks)

CMock automatically generates mock files from your header files. If you have adc.h with int adc_read(int channel);, CMock generates a mock_adc.c that lets you set expectations and return values.

CppUTest

A C/C++ test framework with memory leak detection. Useful if your project mixes C and C++.

Structuring Code for Testability

  • Separate logic from hardware — pure calculations in one module, hardware access in another
  • Use dependency inversion — pass function pointers or interface structs instead of calling hardware directly
  • Keep functions small and focused — a function that does one thing is easier to test than one that does five
  • Return error codes — so tests can verify error handling paths
  • Avoid global state — or provide reset functions that tests can call between test cases

The Testing Pyramid for Embedded

LevelWhatWhereSpeed
Unit testsIndividual functions and modulesPC (host)Milliseconds
Integration testsMultiple modules working togetherPC or targetSeconds
Hardware-in-the-loopReal hardware behaviorTarget boardMinutes
System testsFull product behaviorTarget + peripheralsMinutes to hours

Run the most tests at the bottom of the pyramid (unit tests) — they are fast, cheap, and catch most bugs. Use hardware testing for what cannot be verified on a PC.

Key Takeaways

  • Most embedded C logic can be tested on a PC without hardware
  • Use mocks to replace hardware dependencies during testing
  • Unity + CMock is the standard framework combination for embedded C
  • Structure code for testability: separate logic from hardware, use dependency inversion
  • Run unit tests frequently and automatically — catch bugs before they reach hardware

Leave a Reply

Your email address will not be published. Required fields are marked *