The "Holy Bible" for embedded engineers
Implementing comprehensive unit testing strategies with hardware abstraction and mock frameworks for reliable embedded software development
Unit testing in embedded systems involves testing individual software components in isolation, using hardware abstraction layers and mock objects to simulate hardware interactions. This approach ensures code reliability, facilitates refactoring, and provides confidence in software quality before integration testing.
┌─────────────────────────────────────────────────────────────┐
│ Unit Testing Components │
├─────────────────────────────────────────────────────────────┤
│ Test Framework │ Test runner, assertions, reporting │
│ Hardware Abstraction│ Interface layer for hardware access │
│ Mock Objects │ Simulated hardware and dependencies │
│ Test Doubles │ Stubs, fakes, and test implementations│
│ Test Fixtures │ Setup and teardown for test cases │
│ Assertions │ Validation of expected behavior │
└─────────────────────────────────────────────────────────────┘
The HAL provides a consistent interface between software and hardware:
// Hardware interface definition
typedef struct {
void (*gpio_write)(uint8_t pin, bool value);
bool (*gpio_read)(uint8_t pin);
void (*uart_send)(uint8_t data);
uint8_t (*uart_receive)(void);
void (*delay_ms)(uint32_t ms);
} hardware_interface_t;
// Hardware abstraction functions
void gpio_write(uint8_t pin, bool value);
bool gpio_read(uint8_t pin);
void uart_send(uint8_t data);
uint8_t uart_receive(void);
void delay_ms(uint32_t ms);
// Global hardware interface pointer
extern hardware_interface_t *current_hardware;
Mock hardware simulates real hardware behavior for testing:
// Mock hardware state
typedef struct {
uint8_t gpio_state[32];
uint8_t uart_tx_buffer[256];
uint8_t uart_rx_buffer[256];
uint32_t uart_tx_index;
uint32_t uart_rx_index;
uint32_t uart_rx_count;
uint32_t delay_calls;
} mock_hardware_state_t;
static mock_hardware_state_t mock_state = {0};
// Mock hardware functions
static void mock_gpio_write(uint8_t pin, bool value) {
if (pin < 32) {
mock_state.gpio_state[pin] = value;
}
}
static bool mock_gpio_read(uint8_t pin) {
if (pin < 32) {
return mock_state.gpio_state[pin];
}
return false;
}
static void mock_uart_send(uint8_t data) {
if (mock_state.uart_tx_index < 256) {
mock_state.uart_tx_buffer[mock_state.uart_tx_index++] = data;
}
}
static uint8_t mock_uart_receive(void) {
if (mock_state.uart_rx_index < mock_state.uart_rx_count) {
return mock_state.uart_rx_buffer[mock_state.uart_rx_index++];
}
return 0xFF; // No data available
}
static void mock_delay_ms(uint32_t ms) {
mock_state.delay_calls += ms;
}
A lightweight test framework for embedded systems:
// Test result enumeration
typedef enum {
TEST_PASS,
TEST_FAIL,
TEST_SKIP
} test_result_t;
// Test function type
typedef test_result_t (*test_function_t)(void);
// Test case structure
typedef struct {
const char *name;
const char *description;
test_function_t test_func;
bool enabled;
} test_case_t;
// Test suite structure
typedef struct {
const char *name;
test_case_t *test_cases;
uint32_t test_count;
} test_suite_t;
// Test results
typedef struct {
uint32_t total_tests;
uint32_t passed_tests;
uint32_t failed_tests;
uint32_t skipped_tests;
} test_results_t;
// Test framework implementation
#define MAX_TEST_CASES 100
#define MAX_TEST_SUITES 20
test_case_t test_cases[MAX_TEST_CASES];
test_suite_t test_suites[MAX_TEST_SUITES];
uint32_t test_case_count = 0;
uint32_t test_suite_count = 0;
// Register a test case
uint32_t register_test_case(const char *name, const char *desc,
test_function_t func) {
if (test_case_count >= MAX_TEST_CASES) {
return UINT32_MAX; // Error
}
test_cases[test_case_count].name = name;
test_cases[test_case_count].description = desc;
test_cases[test_case_count].test_func = func;
test_cases[test_case_count].enabled = true;
return test_case_count++;
}
// Register a test suite
uint32_t register_test_suite(const char *name, test_case_t *cases,
uint32_t count) {
if (test_suite_count >= MAX_TEST_SUITES) {
return UINT32_MAX; // Error
}
test_suites[test_suite_count].name = name;
test_suites[test_suite_count].test_cases = cases;
test_suites[test_suite_count].test_count = count;
return test_suite_count++;
}
// Run all tests
test_results_t run_all_tests(void) {
test_results_t results = {0};
printf("=== Running Unit Tests ===\n");
for (uint32_t i = 0; i < test_case_count; i++) {
if (test_cases[i].enabled) {
printf("Running: %s - %s\n",
test_cases[i].name,
test_cases[i].description);
test_result_t result = test_cases[i].test_func();
switch (result) {
case TEST_PASS:
printf(" ✓ PASS\n");
results.passed_tests++;
break;
case TEST_FAIL:
printf(" ✗ FAIL\n");
results.failed_tests++;
break;
case TEST_SKIP:
printf(" - SKIP\n");
results.skipped_tests++;
break;
}
results.total_tests++;
}
}
return results;
}
// Basic assertion macros
#define ASSERT_TRUE(condition) \
do { \
if (!(condition)) { \
printf("Assertion failed: %s is not true\n", #condition); \
return TEST_FAIL; \
} \
} while(0)
#define ASSERT_FALSE(condition) \
do { \
if (condition) { \
printf("Assertion failed: %s is not false\n", #condition); \
return TEST_FAIL; \
} \
} while(0)
#define ASSERT_EQUAL(expected, actual) \
do { \
if ((expected) != (actual)) { \
printf("Assertion failed: expected %d, got %d\n", \
(int)(expected), (int)(actual)); \
return TEST_FAIL; \
} \
} while(0)
#define ASSERT_STRING_EQUAL(expected, actual) \
do { \
if (strcmp((expected), (actual)) != 0) { \
printf("Assertion failed: expected '%s', got '%s'\n", \
(expected), (actual)); \
return TEST_FAIL; \
} \
} while(0)
#define ASSERT_NULL(pointer) \
do { \
if ((pointer) != NULL) { \
printf("Assertion failed: pointer is not NULL\n"); \
return TEST_FAIL; \
} \
} while(0)
#define ASSERT_NOT_NULL(pointer) \
do { \
if ((pointer) == NULL) { \
printf("Assertion failed: pointer is NULL\n"); \
return TEST_FAIL; \
} \
} while(0)
// Example test case for GPIO functionality
test_result_t test_gpio_write_read(void) {
// Setup: Configure mock hardware
mock_hardware_init();
// Test: Write to GPIO pin
uint8_t test_pin = 5;
bool test_value = true;
gpio_write(test_pin, test_value);
// Verify: Read back the value
bool read_value = gpio_read(test_pin);
ASSERT_EQUAL(test_value, read_value);
// Test: Write false value
gpio_write(test_pin, false);
read_value = gpio_read(test_pin);
ASSERT_FALSE(read_value);
return TEST_PASS;
}
// Example test case for UART functionality
test_result_t test_uart_send_receive(void) {
// Setup: Prepare test data
uint8_t test_data[] = {0x55, 0xAA, 0x12, 0x34};
uint32_t test_length = sizeof(test_data);
// Set up mock UART receive buffer
mock_uart_set_rx_data(test_data, test_length);
// Test: Send data
for (uint32_t i = 0; i < test_length; i++) {
uart_send(test_data[i]);
}
// Verify: Check sent data
uint8_t *sent_data;
uint32_t sent_length;
mock_uart_get_tx_data(&sent_length);
ASSERT_EQUAL(test_length, sent_length);
// Test: Receive data
for (uint32_t i = 0; i < test_length; i++) {
uint8_t received = uart_receive();
ASSERT_EQUAL(test_data[i], received);
}
return TEST_PASS;
}
// Test fixture structure
typedef struct {
void (*setup)(void);
void (*teardown)(void);
const char *name;
} test_fixture_t;
// Test fixture implementation
typedef struct {
test_fixture_t *fixture;
test_case_t *test_case;
} test_with_fixture_t;
// Run test with fixture
test_result_t run_test_with_fixture(test_with_fixture_t *test) {
test_result_t result;
// Setup
if (test->fixture && test->fixture->setup) {
test->fixture->setup();
}
// Run test
result = test->test_case->test_func();
// Teardown
if (test->fixture && test->fixture->teardown) {
test->fixture->teardown();
}
return result;
}
// Example fixture for hardware testing
void hardware_test_setup(void) {
mock_hardware_init();
// Initialize any other test dependencies
}
void hardware_test_teardown(void) {
mock_hardware_reset();
// Clean up test resources
}
test_fixture_t hardware_fixture = {
.name = "Hardware Test Fixture",
.setup = hardware_test_setup,
.teardown = hardware_test_teardown
};
// Parameterized test structure
typedef struct {
const char *test_name;
void *test_data;
test_result_t (*test_func)(void *data);
} parameterized_test_t;
// Example parameterized test for different GPIO pins
typedef struct {
uint8_t pin;
bool value;
const char *description;
} gpio_test_params_t;
gpio_test_params_t gpio_test_cases[] = {
{0, true, "GPIO 0 - High"},
{1, false, "GPIO 1 - Low"},
{15, true, "GPIO 15 - High"},
{31, false, "GPIO 31 - Low"}
};
test_result_t test_gpio_parameterized(void *data) {
gpio_test_params_t *params = (gpio_test_params_t *)data;
// Test the specific GPIO configuration
gpio_write(params->pin, params->value);
bool read_value = gpio_read(params->pin);
ASSERT_EQUAL(params->value, read_value);
return TEST_PASS;
}
// Run parameterized tests
void run_parameterized_tests(void) {
uint32_t test_count = sizeof(gpio_test_cases) / sizeof(gpio_test_cases[0]);
for (uint32_t i = 0; i < test_count; i++) {
printf("Running parameterized test: %s\n",
gpio_test_cases[i].description);
test_result_t result = test_gpio_parameterized(&gpio_test_cases[i]);
if (result == TEST_PASS) {
printf(" ✓ PASS\n");
} else {
printf(" ✗ FAIL\n");
}
}
}
// Mock state management functions
void mock_hardware_init(void) {
memset(&mock_state, 0, sizeof(mock_state));
}
void mock_uart_set_rx_data(const uint8_t *data, uint32_t length) {
if (length <= 256) {
memcpy(mock_state.uart_rx_buffer, data, length);
mock_state.uart_rx_count = length;
mock_state.uart_rx_index = 0;
}
}
uint8_t* mock_uart_get_tx_data(uint32_t *length) {
if (length) {
*length = mock_state.uart_tx_index;
}
return mock_state.uart_tx_buffer;
}
void mock_hardware_reset(void) {
mock_hardware_init();
}
// Mock hardware interface
hardware_interface_t mock_hardware = {
.gpio_write = mock_gpio_write,
.gpio_read = mock_gpio_read,
.uart_send = mock_uart_send,
.uart_receive = mock_uart_receive,
.delay_ms = mock_delay_ms
};
Q: What is the purpose of unit testing in embedded systems? A: Unit testing verifies individual software components work correctly in isolation, catches bugs early, enables safe refactoring, provides documentation, and ensures code quality before integration testing.
Q: What is a hardware abstraction layer and why is it important for testing? A: A HAL provides a consistent interface between software and hardware, allowing the same code to work with different hardware implementations and enabling easy substitution of real hardware with mocks during testing.
Q: How would you implement a mock for a UART peripheral? A: Create a mock that simulates UART behavior by maintaining internal buffers for transmit and receive data, implementing the same interface as the real UART, and providing functions to set up test data and verify transmitted data.
Q: What are the challenges of unit testing interrupt service routines? A: ISRs have timing constraints, may access hardware directly, can be difficult to trigger in tests, and require careful mocking of interrupt sources and hardware state.
Q: How would you design a test framework for a multi-core embedded system? A: Use shared memory for test coordination, implement thread-safe test execution, use hardware features for synchronization, and design tests that can run independently on different cores.
Q: How do you ensure unit tests don’t interfere with real-time constraints? A: Use lightweight mocks, minimize test overhead, run comprehensive tests only during development, use hardware features when available, and design tests to complete quickly.
Next Steps: Explore Hardware-in-the-Loop Testing for integrated testing or Performance Profiling for optimization analysis.