embedded-testing-patterns · diff
git:20260225.a4aeb64 to git:20260226.186740f
163 added, 69 removed. Audit C to C.
- # Embedded Testing Patterns
-
- A comprehensive pattern library and knowledge base for embedded-testing.
+ # embedded-testing-patterns
## Knowledge Base
- ### Core Concepts
- - **Fundamentals**: The foundational principles that govern embedded-testing
- - **Terminology**: Standard vocabulary and definitions used in the domain
- - **Standards**: Industry standards and specifications that apply
- - **Tools**: Common tools and frameworks used for embedded-testing
+ Embedded C test patterns using Unity, CMock, Ceedling, and QEMU.
- ### Architecture Principles
- - Separation of concerns within embedded-testing implementations
- - Modularity and reusability of components
- - Scalability considerations for growing systems
- - Integration patterns with adjacent domains
+ ---
- ### Quality Attributes
- - **Correctness**: Implementations must meet functional requirements
- - **Maintainability**: Code and artifacts should be easy to understand and modify
- - **Performance**: Implementations should meet non-functional requirements
- - **Security**: Sensitive data and operations must be properly protected
+ ## Pattern 1: Ceedling Project Structure
- ## Patterns
+ ```
+ 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)
+ ```
- ### Pattern 1: Structured Approach
- - Start with requirements analysis
- - Design before implementing
- - Validate against acceptance criteria
- - Document decisions and rationale
+ Minimal `project.yml`:
+ ```yaml
+ :project:
+ :build_root: build
+ :test_file_prefix: test_
- ### Pattern 2: Iterative Refinement
- - Begin with a minimal viable implementation
- - Gather feedback early and often
- - Refine based on real-world usage
- - Continuously improve based on metrics
+ :paths:
+ :source: ["src/**"]
+ :test: ["test/**"]
+ :include: ["src"]
- ### Pattern 3: Convention Over Configuration
- - Follow established conventions where they exist
- - Configure only what needs to deviate from defaults
- - Document any non-standard choices
- - Prefer explicit over implicit behavior
+ :tools:
+ :test_compiler:
+ :arguments:
+ - -std=c11
+ - -DUNIT_TEST
- ### Pattern 4: Defense in Depth
- - Validate at multiple levels
- - Handle errors gracefully at each layer
- - Provide meaningful feedback for failures
- - Log important events for debugging
+ :plugins:
+ :load_paths: ["vendor/ceedling/plugins"]
+ :enabled: [gcov, xml_tests_report]
- ## Anti-Patterns
+ :cmock:
+ :enforce_strict_ordering: TRUE
+ :plugins: [:ignore, :return_thru_ptr, :callback]
+ ```
- ### Anti-Pattern 1: Premature Optimization
- - Optimizing before understanding the actual bottleneck
- - Adding complexity without measured need
- - Sacrificing readability for marginal performance gains
+ Run:
+ ```bash
+ ceedling test:all # All tests
+ ceedling test:sensor # Single module
+ ceedling gcov:all # Coverage
+ ceedling utils:gcov # HTML coverage report
+ ```
- ### Anti-Pattern 2: Copy-Paste Without Understanding
- - Duplicating code without understanding its purpose
- - Propagating bugs through mechanical copying
- - Missing opportunities for abstraction
+ ---
- ### Anti-Pattern 3: Ignoring Standards
- - Deviating from conventions without clear justification
- - Creating inconsistency across the codebase
- - Making onboarding harder for new contributors
+ ## Pattern 2: Testable HAL Interface Pattern
- ### Anti-Pattern 4: Over-Engineering
- - Building for hypothetical future requirements
- - Adding abstraction layers without clear benefit
- - Creating complex solutions for simple problems
+ The golden rule: drivers never call `HAL_I2C_Master_Transmit` directly. They call through a pointer.
- ## References
+ ```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);
- ### Documentation
- - Official documentation for related tools and frameworks
- - Industry standards and specifications
- - Community best practices and guides
+ typedef struct {
+ i2c_write_fn write;
+ i2c_read_fn read;
+ } i2c_ops_t;
- ### Learning Resources
- - Tutorials and walkthroughs for beginners
- - Advanced guides for experienced practitioners
- - Case studies and real-world examples
+ /* sensor.c uses i2c_ops_t, not HAL directly */
+ static const i2c_ops_t *s_i2c;
- ### Tools
- - Development tools for embedded-testing
- - Testing and validation tools
- - Monitoring and observability tools
+ 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)