embedded-testing-patterns
/embedded-testing-patterns
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.
- Author
- ormus-solutions ↗
- Repository
- libreembed-claude-code ↗
- Licence
- MIT
- Last repository change
- 09.10.2026
Assessment and security
- Files in this package
- 1
- Scripts
- 0
- HTML comments
- 0
- URLs in SKILL.md
- 3
No signals detected. Repository changed on 09.10.2026, without references to retired models or APIs.
MIT, open: use and modify while retaining author attribution.
What it does and when to use it
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.
Original author 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.
How it works
- Knowledge Base
- Pattern 1: Ceedling Project Structure
- Pattern 2: Testable HAL Interface Pattern
- Pattern 3: CMock Expect Chaining
- Pattern 4: Testing Error Paths
- Pattern 5: On-Target Test Runner via ITM
- Anti-Patterns
- References
SKILL.md preview
---
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)
Versions
The database has no other skills named embedded-testing-patterns.
Database snapshot: 13.11.2026 · package version d1e3647f438c (SHA-256 digest of the entire package)
Downloads in the last 30 days: 0 · ranking
Report a problem with this skill: harmful content, incorrect descriptions or an author’s removal request.
Similar skills
- 100unity-ceedling-integration
Use when adding, configuring, or debugging Unity, Ceedling, CMock, or embedded C unit tests, mocks, fixtures, build variants, or CI test runs - 100ceedling-build-runner
Author and run the Ceedling build system for C unit testing - the canonical build orchestration on top of Unity (assertions) + CMock (mocks) + CException (exceptions). Covers ceedling new scaffolding, the project.yml schema (:project / :paths / :files / :defines / :flags /… - 100qemu-embedded-simulation
QEMU embedded simulation skill for bare-metal MCU testing. Use when running ARM/RISC-V firmware in QEMU, selecting machine models, loading -kernel ELF, or GDB debugging without hardware. Activates on queries about QEMU bare metal, qemu-system-arm -kernel, STM32 QEMU machine,… - 100firmware-build
Implements microcontroller firmware within hard limits: static memory, bounded interrupt handlers, a testable HAL boundary, measured stack and flash budgets, and on-target verification. Use for embedded work on an MCU, driver or RTOS task code, or a change that must be proven on… - 100rust-embedded
Design, implement, review, and validate embedded Rust firmware, including no_std, targets, runtime and startup, embedded-hal drivers, interrupts, DMA, shared state, async executors, hardware mocks, cross-compilation, flashing, and hardware acceptance evidence. Use when users ask… - 100embedded-stm32
Best practices for embedded C/C++ development on STM32 microcontrollers using the HAL, covering peripherals, DMA, interrupts, memory constraints, and hardware-focused testing. Use when writing STM32 HAL code, configuring peripherals generated by STM32CubeMX, working with…