Skip to content
Skills Library
Programming · Testing

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.

Licence
MIT
Last repository change
09.10.2026

Assessment and security

100/ 100Class: good
Format25 / 25
Frontmatter with name and description, valid name, description up to 1,024 characters, name matching the folder.
Discoverability25 / 25
Description explains when to use the skill, has a reasonable length and uses third person.
Content35 / 35
At least 150 words and at most 500 lines, headings, step lists, examples or code.
Hygiene15 / 15
No placeholders such as TODO, no Windows paths, with a licence.
Riskno signals detected in the scan
Files in this package
1
Scripts
0
HTML comments
0
URLs in SKILL.md
3
Deprecation

No signals detected. Repository changed on 09.10.2026, without references to retired models or APIs.

Licence

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

  1. Knowledge Base
  2. Pattern 1: Ceedling Project Structure
  3. Pattern 2: Testable HAL Interface Pattern
  4. Pattern 3: CMock Expect Chaining
  5. Pattern 4: Testing Error Paths
  6. Pattern 5: On-Target Test Runner via ITM
  7. Anti-Patterns
  8. References
Original SKILL.md headings · 184 lines, 540 words
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…

Feedback and reports

Your message goes to Bartosz Sokoliński. An email address is needed only for a reply. See the privacy policy.