The Embedded New Testament

The "Holy Bible" for embedded engineers


Project maintained by theEmbeddedGeorge Hosted on GitHub Pages — Theme by mattgraham

Unit Testing for Embedded Systems

Implementing comprehensive unit testing strategies with hardware abstraction and mock frameworks for reliable embedded software development

📋 Table of Contents

🎯 Overview

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.

Why Unit Testing is Critical in Embedded Systems

🔑 Key Concepts

Unit Testing Components

┌─────────────────────────────────────────────────────────────┐
│                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      │
└─────────────────────────────────────────────────────────────┘

Testing Strategies

🧠 Core Concepts

Hardware Abstraction Layer (HAL)

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 Implementation

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;
}

Test Framework Architecture

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;

🛠️ Implementation

Basic Test Framework

// 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;
}

Assertion Macros

// 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)

Test Case Examples

// 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;
}

🚀 Advanced Techniques

Test Fixtures and Setup

// 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 Testing

// 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

// 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
};

⚠️ Common Pitfalls

Hardware Dependency Issues

Test Design Problems

Performance Issues

✅ Best Practices

Test Design Principles

  1. Single Responsibility: Each test should verify one specific behavior
  2. Independence: Tests should not depend on each other
  3. Readability: Tests should be easy to understand and maintain
  4. Completeness: Tests should cover normal, edge, and error cases

Mock Implementation Guidelines

  1. Realistic Behavior: Mocks should behave like real hardware
  2. State Management: Properly manage mock state between tests
  3. Error Simulation: Ability to simulate hardware failures
  4. Performance: Mocks should be fast and lightweight

Test Organization

  1. Logical Grouping: Group related tests into test suites
  2. Naming Conventions: Use descriptive test names
  3. Documentation: Document test purpose and expected behavior
  4. Maintenance: Keep tests updated with code changes

💡 Interview Questions

Basic Questions

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.

Intermediate Questions

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.

Advanced Questions

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.