| name | embedded-tdd |
| description | Use when implementing any driver, sensor handler, or firmware component - requires HAL Mock test first, then hardware verification test |
Embedded Test-Driven Development
Overview
Write HAL Mock test first. Watch it fail. Write minimal HAL implementation. Run hardware verification test.
Core principle: Test the driver logic WITHOUT hardware, then verify WITH hardware.
Announce at start: "I'm using the embedded-tdd skill to write HAL Mock tests before implementation."
The Iron Law
NO FIRMWARE CODE WITHOUT FAILING HAL MOCK TEST FIRST
Write code before HAL Mock test? Delete it. Start over.
No exceptions:
- Don't keep untested code as "reference"
- Don't "adapt" existing code while writing tests
- Don't look at untested implementation
- Delete means delete
Two-Phase Testing Strategy
Phase 1: HAL Mock Tests (Unit Tests)
Test driver logic WITHOUT real hardware:
- Mock
spi_transmit() → verify command sequence
- Mock
i2c_read() → verify data parsing
- Mock
gpio_set_level() → verify timing logic
Purpose: Prove driver logic is correct regardless of hardware behavior.
Phase 2: Hardware Verification Tests (Integration Tests)
Test WITH real hardware:
- Real SPI → actual LCD response
- Real I2C → actual sensor data
- Compile → Flash → Monitor → Evidence
Purpose: Prove driver works with actual hardware.
The RED-GREEN-REFACTOR Cycle (Embedded)
digraph embedded_tdd {
"Write HAL Mock test" [shape=box];
"Run test: FAIL?" [shape=diamond];
"STOP: Test must fail first" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
"Write minimal implementation" [shape=box];
"Run test: PASS?" [shape=diamond];
"Refactor implementation" [shape=box];
"Run Hardware test" [shape=box];
"Hardware test: PASS?" [shape=diamond];
"Invoke embedded-verification" [shape=doublecircle];
"Write HAL Mock test" -> "Run test: FAIL?";
"Run test: FAIL?" -> "STOP: Test must fail first" [label="no - test wrong"];
"Run test: FAIL?" -> "Write minimal implementation" [label="yes - RED verified"];
"Write minimal implementation" -> "Run test: PASS?";
"Run test: PASS?" -> "Write minimal implementation" [label="no - more code needed"];
"Run test: PASS?" -> "Refactor implementation" [label="yes - GREEN"];
"Refactor implementation" -> "Run Hardware test";
"Run Hardware test" -> "Hardware test: PASS?";
"Hardware test: PASS?" -> "Run Hardware test" [label="no - debug"];
"Hardware test: PASS?" -> "Invoke embedded-verification" [label="yes"];
}
HAL Mocking Strategy
What to Mock
| Component | HAL Functions to Mock |
|---|
| SPI LCD | spi_init(), spi_transmit(), spi_receive() |
| I2C Sensor | i2c_init(), i2c_read(), i2c_write() |
| GPIO Control | gpio_init(), gpio_set_level(), gpio_get_level() |
| UART Comm | uart_init(), uart_send(), uart_receive() |
What NOT to Mock
| Layer | What NOT to Mock |
|---|
| Driver logic | Command sequences, data parsing, timing calculations |
| Application | Business logic, state machines |
| Platform SDK | ESP-IDF internals (use their test framework) |
HAL Mock Implementation
Example: LCD Driver HAL Mock
static uint8_t mock_spi_buffer[256];
static int mock_spi_buffer_len = 0;
static bool mock_spi_initialized = false;
void mock_spi_init(spi_config_t *config) {
mock_spi_initialized = true;
mock_spi_buffer_len = 0;
}
void mock_spi_transmit(uint8_t *data, size_t len) {
if (!mock_spi_initialized) {
TEST_FAIL("SPI not initialized before transmit");
}
memcpy(mock_spi_buffer + mock_spi_buffer_len, data, len);
mock_spi_buffer_len += len;
}
uint8_t *mock_spi_get_buffer(void) {
return mock_spi_buffer;
}
int mock_spi_get_buffer_len(void) {
return mock_spi_buffer_len;
}
void mock_spi_reset(void) {
mock_spi_buffer_len = 0;
}
Example: HAL Mock Test
TEST_CASE("lcd_init_sends_reset_command") {
mock_spi_reset();
lcd_config_t config = {
.spi_host = SPI2_HOST,
.mosi_pin = 11,
.sck_pin = 12,
.cs_pin = 10,
.dc_pin = 9,
.rst_pin = 8
};
lcd_init(&config);
uint8_t *buffer = mock_spi_get_buffer();
TEST_ASSERT_EQUAL_UINT8(0x01, buffer[0]);
}
TEST_CASE("lcd_fill_sends_correct_command_sequence") {
mock_spi_reset();
lcd_init(&mock_config);
lcd_fill_rect(0, 0, 240, 240, COLOR_RED);
uint8_t *buffer = mock_spi_get_buffer();
TEST_ASSERT_EQUAL_UINT8(0x2A, buffer[0]);
TEST_ASSERT_EQUAL_UINT8(0x2B, buffer[4]);
TEST_ASSERT_EQUAL_UINT8(0x2C, buffer[8]);
}
RED Phase: Write Failing HAL Mock Test
write test/test_lcd_hal.c
idf.py build
Verify RED:
- Test MUST fail
- Error MUST be "not implemented" or "undefined reference"
- If test passes immediately: Test is wrong (testing mock, not logic)
GREEN Phase: Write Minimal Implementation
void lcd_init(lcd_config_t *config) {
spi_init(config->spi_host, config->mosi_pin, config->sck_pin);
gpio_init(config->cs_pin, GPIO_OUTPUT);
gpio_init(config->dc_pin, GPIO_OUTPUT);
gpio_init(config->rst_pin, GPIO_OUTPUT);
gpio_set_level(config->cs_pin, 0);
gpio_set_level(config->dc_pin, 0);
spi_transmit(config->spi_host, 0x01);
gpio_set_level(config->cs_pin, 1);
}
idf.py build
REFACTOR Phase: Clean Implementation
After HAL Mock tests pass:
- Extract constants to header
- Add error handling
- Optimize buffer usage
- Document timing constraints
Phase 2: Hardware Verification Test
TEST_CASE("lcd_hardware_init_succeeds") {
lcd_config_t config = {
.spi_host = SPI2_HOST,
.mosi_pin = 11,
.sck_pin = 12,
.cs_pin = 10,
.dc_pin = 9,
.rst_pin = 8
};
esp_err_t ret = lcd_init(&config);
TEST_ASSERT_EQUAL(ESP_OK, ret);
uint32_t lcd_id = lcd_read_id();
TEST_ASSERT_EQUAL(LCD_ID_ST7789, lcd_id);
}
TEST_CASE("lcd_hardware_fill_visible") {
lcd_fill_rect(0, 0, 240, 240, COLOR_RED);
}
idf.py -p COM3 flash monitor
Common Rationalizations
| Excuse | Reality |
|---|
| "No hardware available" | Write HAL Mock tests first. Hardware test when available. |
| "Sensor too simple to test" | I2C address errors cost hours. HAL Mock takes 5 min. |
| "Already tested on breadboard" | Ad-hoc ≠ systematic. HAL Mock tests are repeatable. |
| "Mock doesn't prove hardware works" | Mock proves logic. Hardware test proves integration. |
| "I'll test after implementing" | That's not TDD. Write test FIRST. |
| "Keep old code as reference" | You'll adapt it. Delete means delete. |
| "This is just a small change" | Small changes need tests too. Regression happens. |
Red Flags - STOP
If you think:
- "I'll write the test after implementing"
- "This is too simple for HAL Mock"
- "Mock test passes immediately (should verify)"
- "Skip hardware test because mock passed"
- "Keep old code as reference while writing tests"
- "Adapt existing implementation instead of deleting"
STOP. You are rationalizing. Follow the Iron Law.
Anti-Patterns (See testing-anti-patterns.md)
| Anti-Pattern | Fix |
|---|
| Testing mock behavior | Test driver logic, not mock internals |
| Test-only methods in HAL | Move to test utilities |
| Mock without understanding | Understand HAL interface before mocking |
| Incomplete mocks | Mock complete HAL interface |
| Hardware tests as afterthought | Hardware tests are Phase 2 of TDD |
Checklist (Create TodoWrite)
Integration
Previous skill: embedded-brainstorming (hardware design approved)
Next skill: embedded-verification (evidence collection)
After hardware test passes: "Hardware tests pass. I'm invoking the embedded-verification skill to collect build-flash-monitor evidence."
Reference Documents
| Reference | Content |
|---|
references/hal-mocking.md | HAL Mock patterns and examples |
references/chips.md | Chip-specific HAL interfaces |
references/hardware-interfaces.md | SPI/I2C/UART specifications |