Wednesday, 15 October 2025

TDD for Embedded C: Project Layout + Unity & CMock (practical)

Keep tests separate from production, wire up a tiny runner, and mock hardware at the header boundary. Here’s the minimal, repeatable setup that works with CubeIDE/MDK/IAR or Make.


πŸ“ Project anatomy (add, don’t tangle)

/Core/Src -> app code (e.g., main.c, modules) /Drivers -> HAL/CMSIS or your drivers /Build, /Debug -> IDE artifacts (auto-generated) /TDD -> ✅ tests live here (excluded from production build) /tests -> test_*.c (Unity tests) /unity -> Unity sources (vendor drop-in) /cmock -> CMock sources + generated mocks /mocks -> <auto-generated> mock_*.c/.h

πŸ‘‰ In CubeIDE: right-click TDDResource Configurations → Exclude from Build… (all configs).


✅ Unity: tiny test + runner

// TDD/tests/test_my_module.c #include "unity.h" #include "my_module.h" void setUp(void) {} void tearDown(void) {} void test_get_value_returns_5(void) { TEST_ASSERT_EQUAL_UINT8(5, get_value()); } // TDD/tests/test_runner.c #include "unity.h" extern void test_get_value_returns_5(void); int main(void){ UNITY_BEGIN(); RUN_TEST(test_get_value_returns_5); return UNITY_END(); }

Build a test target that compiles: unity/*.c, tests/*.c, and module(s) under test.
(Optionally -DUNIT_TEST for test-only code paths.)


πŸ”Œ CMock: mock the hardware boundary

// Drivers/driver.h (production header to be mocked) uint8_t driver_get_byte(void); // TDD/tests/test_sensor.c #include "unity.h" #include "mock_driver.h" // generated by CMock from driver.h #include "sensor.h" // module under test void test_sensor_reads_driver_value(void){ driver_get_byte_ExpectAndReturn(0x2A); TEST_ASSERT_EQUAL_UINT8(0x2A, sensor_read()); }
# TDD/cmock.yml (example CMock config) :mock_path: "TDD/mocks" :plugins: [expect, ignore, return_thru_ptr] :enforce_strict_ordering: true

Run CMock over headers → emits mock_driver.* into /TDD/mocks. Compile tests against mocks, not real drivers.


πŸ§ͺ Build wiring (Make-ish sketch)

TEST_SRCS := TDD/tests/*.c TDD/unity/*.c TDD/mocks/*.c Core/Src/sensor.c PROD_SRCS := $(filter-out TDD/%,$(wildcard Core/Src/*.c Drivers/**/*.c)) # test target links TEST_SRCS; production target links PROD_SRCS only

In IDEs, make a Test build config that includes TDD/… and excludes hardware files you’re mocking.


⚡ Pro Tips

  • Keep tests fast & pure: no HAL clocks/ISR—mock them.

  • One behavior per test; readable names: test_<unit>_<behavior>_<expectation>.

  • Generate mocks from headers you own (stable API).

  • Gate test-only code with #ifdef UNIT_TEST.

  • Failures first: write the test, see it fail, make it pass, refactor.

🎯 Conclusion

Separate tests, add Unity runner, mock hardware with CMock—now your embedded code is testable without boards, fast, and refactor-friendly.


Written By: Musaab Taha


This article was improved with the assistance of AI.

No comments:

Post a Comment