Firmware test patterns with Unity, CMock, Ceedling, and QEMU: project layout, testable HAL interfaces, CMock expect chaining, error-path tests, and an on-target test runner over ITM. Use when making embedded C testable or writing tests for it.
Installs into .claude/skills of the current project.
Are you the author of Embedded Testing Patterns?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/hermeticormus-embedded-testing-patterns)
---
name: "embedded-testing-patterns"
description: "Firmware test patterns with Unity, CMock, Ceedling, and QEMU: project layout, testable HAL interfaces, CMock expect chaining, error-path tests, and an on-target test runner over ITM. Use when making embedded C testable or writing tests for it."
---
# embedded-testing-patterns
## Knowledge Base
Embedded C test patterns using Unity, CMock, Ceedling, and QEMU.
---
## Pattern 1: Ceedling Project Structure
```
project/
├── project.yml # Ceedling configuration
├── src/
│ ├── sensor.c
│ └── sensor.h
├── test/
│ ├── test_sensor.c
│ └── support/ # Test helpers, stubs
└── vendor/ # Unity, CMock (git submodule or gem)
```
Minimal `project.yml`:
```yaml
:project:
:build_root: build
:test_file_prefix: test_
:paths:
:source: ["src/**"]
:test: ["test/**"]
:include: ["src"]
:tools:
:test_compiler:
:arguments:
- -std=c11
- -DUNIT_TEST
:plugins:
:load_paths: ["vendor/ceedling/plugins"]
:enabled: [gcov, xml_tests_report]
:cmock:
:enforce_strict_ordering: TRUE
:plugins: [:ignore, :return_thru_ptr, :callback]
```
Run:
```bash
ceedling test:all # All tests
ceedling test:sensor # Single module
ceedling gcov:all # Coverage
ceedling utils:gcov # HTML coverage report
```
---
## Pattern 2: Testable HAL Interface Pattern
The golden rule: drivers never call `HAL_I2C_Master_Transmit` directly. They call through a pointer.
```c
/* hal_i2c.h */
typedef int (*i2c_write_fn)(uint8_t dev_addr, uint8_t reg_addr,
const uint8_t *data, uint8_t len);
typedef int (*i2c_read_fn)(uint8_t dev_addr, uint8_t reg_addr,
uint8_t *data, uint8_t len);
typedef struct {
i2c_write_fn write;
i2c_read_fn read;
} i2c_ops_t;
/* sensor.c uses i2c_ops_t, not HAL directly */
static const i2c_ops_t *s_i2c;
int sensor_begin(const i2c_ops_t *i2c_ops) {
s_i2c = i2c_ops;
return s_i2c->write(SENSOR_ADDR, REG_RESET, (uint8_t[]){0xB6}, 1);
}
```
Production: pass a `&stm32_i2c_ops` that wraps `HAL_I2C_Master_Transmit`.
Test: pass a `&mock_i2c_ops` backed by function pointers set by CMock.
---
## Pattern 3: CMock Expect Chaining
Order matters. CMock verifies calls happen in declared order when `enforce_strict_ordering: TRUE`.
```c
void test_bme280_init_sequence(void)
{
/* Expect exact I2C register writes in order */
i2c_write_ExpectAndReturn(BME280_ADDR, 0xF3, 0x00, 0); /* status check */
i2c_write_ExpectAndReturn(BME280_ADDR, 0xF4, 0x27, 0); /* ctrl_meas */
i2c_write_ExpectAndReturn(BME280_ADDR, 0xF5, 0xA0, 0); /* config */
int rc = bme280_init();
TEST_ASSERT_EQUAL_INT(0, rc);
/* CMock verifies all 3 Expects were called, in order, at test end */
}
```
If fewer calls happen than expected, CMock fails with "Called fewer times than expected."
---
## Pattern 4: Testing Error Paths
```c
void test_sensor_init_returns_error_on_i2c_failure(void)
{
/* Inject I2C failure on first write */
i2c_write_ExpectAndReturn(0x76, REG_RESET, 0xB6, I2C_ERR_NACK);
sensor_error_t result = sensor_init();
TEST_ASSERT_EQUAL(SENSOR_ERR_BUS, result);
}
void test_sensor_handles_crc_mismatch(void)
{
uint8_t bad_data[4] = { 0xAA, 0xBB, 0xCC, 0x00 }; /* Wrong CRC */
i2c_read_ExpectAndReturn(0x76, REG_DATA, bad_data, 4, 0);
i2c_read_ReturnThruPtr_data(bad_data);
int32_t temp;
sensor_error_t result = sensor_read_temperature(&temp);
TEST_ASSERT_EQUAL(SENSOR_ERR_CRC, result);
}
```
Error path coverage is often the most valuable. Always inject failures.
---
## Pattern 5: On-Target Test Runner via ITM
For tests that must run on hardware (timing-sensitive, peripheral integration):
```c
/* On-target Unity output via ITM */
void unity_output_char(char c)
{
ITM_SendChar((uint32_t)c);
}
/* main.c in test build */
int main(void)
{
SystemInit();
itm_enable(168000000UL, 2000000UL);
UNITY_BEGIN();
RUN_TEST(test_sensor_read_temperature_converts_raw);
RUN_TEST(test_sensor_init_configures_i2c);
return UNITY_END();
}
```
Output captured via J-Link SWO Viewer or OpenOCD SWO. Pass/fail visible on host.
---
## Anti-Patterns
- **Testing HAL calls instead of business logic**: mock the HAL, test what your code does with the result.
- **Global state between tests**: reset all static variables in `setUp()`. Static state leaks cause false passes.
- **Single large test per function**: one test should test one behavior. Refactor to separate `test_X_when_Y`.
- **Ignoring all mock calls with `_IgnoreAndReturn`**: fine for stubs, but hides regressions. Be explicit.
## References
- Unity source + docs: https://github.com/ThrowTheSwitch/Unity
- CMock: https://github.com/ThrowTheSwitch/CMock
- Ceedling: https://github.com/ThrowTheSwitch/Ceedling
- James Grenning, "Test-Driven Development for Embedded C" (Pragmatic Bookshelf)