Skip to content
Home » Embedded Systems » Embedded Systems Testing: Unit, Integration, and System Testing

Embedded Systems Testing: Unit, Integration, and System Testing

Embedded Systems Testing featured image with dark purple background, DEV TOOLS badge, TST icon in purple circle, and Unit Integration and System Testing subtitle by nerdyelectronics.com
Embedded Systems Learning Path
Part 97 of 129View Full Path →

KEY TAKEAWAYS

  • Unit tests verify individual functions in isolation; integration tests verify component interactions
  • System tests validate the complete embedded system against requirements on target hardware
  • Mock objects and hardware abstraction layers enable testing embedded code on a PC without hardware
  • Automated testing catches regressions early and builds confidence in firmware changes

Why Testing Matters in Embedded Systems

Embedded systems control cars, medical devices, industrial machines, and IoT devices. A bug in embedded firmware can cause physical harm, data loss, or expensive product recalls. Testing is not optional but a critical part of the development process.Unlike web or mobile apps where you can push a quick fix, updating firmware on deployed devices is difficult and sometimes impossible. Getting it right before deployment is essential.There are three main levels of testing:
  1. Unit Testing – Test individual functions in isolation
  2. Integration Testing – Test how modules work together
  3. System Testing – Test the complete system as a whole

Unit Testing

What is Unit Testing?

A unit test verifies that a single function or module behaves correctly in isolation. The “unit” is typically one function. You provide known inputs and check that the output matches expectations.

Example: Testing a Temperature Conversion Function

// temperature.c
float celsius_to_fahrenheit(float celsius) {
    return (celsius * 9.0f / 5.0f) + 32.0f;
}

float fahrenheit_to_celsius(float fahrenheit) {
    return (fahrenheit - 32.0f) * 5.0f / 9.0f;
}
// test_temperature.c
#include <assert.h>
#include <math.h>
#include <stdio.h>

// Helper: compare floats with tolerance
int float_equal(float a, float b) {
    return fabs(a - b) < 0.01f;
}

void test_celsius_to_fahrenheit(void) {
    assert(float_equal(celsius_to_fahrenheit(0.0f), 32.0f));
    assert(float_equal(celsius_to_fahrenheit(100.0f), 212.0f));
    assert(float_equal(celsius_to_fahrenheit(-40.0f), -40.0f));
    assert(float_equal(celsius_to_fahrenheit(37.0f), 98.6f));
    printf("PASS: celsius_to_fahrenheitn");
}

void test_fahrenheit_to_celsius(void) {
    assert(float_equal(fahrenheit_to_celsius(32.0f), 0.0f));
    assert(float_equal(fahrenheit_to_celsius(212.0f), 100.0f));
    assert(float_equal(fahrenheit_to_celsius(-40.0f), -40.0f));
    printf("PASS: fahrenheit_to_celsiusn");
}

int main(void) {
    test_celsius_to_fahrenheit();
    test_fahrenheit_to_celsius();
    printf("All tests passed!\n");
    return 0;
}

Unit Testing Frameworks for C

FrameworkDescription
UnityLightweight, popular for embedded. Single header + source file.
CUnitFeature-rich framework with test suites and reporting.
CMockaSupports mocking, good for testing with hardware abstractions.
Google TestC++ framework, works for C code too. Very popular in industry.

The Key Challenge: Hardware Dependencies

Embedded code often calls hardware-specific functions like gpio_write() or adc_read(). These do not exist on your PC where unit tests run. The solution is to use mocks or stubs: fake versions of hardware functions that return predictable values.
// mock_adc.c - Fake ADC for testing on PC
static int mock_adc_value = 0;

void mock_adc_set_value(int value) {
    mock_adc_value = value;
}

int adc_read(int channel) {
    return mock_adc_value;  // Return the preset value
}
This lets you test your logic without actual hardware.

Integration Testing

What is Integration Testing?

Integration testing verifies that multiple modules work correctly together. After individual units pass their tests, integration testing checks that they communicate and cooperate as expected.

Examples of Integration Tests

  • Sensor + UART: Read sensor data and verify it is correctly formatted and sent over UART
  • SPI driver + Flash memory: Write data to flash via SPI, read it back, and verify it matches
  • ADC + Display: Read an ADC value and verify the correct value appears on the display module
  • RTOS tasks + Queues: Verify that one task can send data through a queue and another task receives it correctly

Integration Testing on Hardware

Unlike unit tests that run on your PC, integration tests often need to run on the actual target hardware because they involve real peripherals.
// Integration test: UART loopback
// Connect TX to RX on the same UART
void test_uart_loopback(void) {
    uart_init(115200);

    uint8_t test_data[] = {0x00, 0x55, 0xAA, 0xFF};

    for (int i = 0; i < sizeof(test_data); i++) {
        uart_send_byte(test_data[i]);
        uint8_t received = uart_receive_byte_timeout(100);

        if (received != test_data[i]) {
            printf("FAIL: Sent 0x%02X, Received 0x%02X\n",
                   test_data[i], received);
            return;
        }
    }
    printf("PASS: UART loopback testn");
}

System Testing

What is System Testing?

System testing validates the complete product against its requirements. You test the whole system as a user would experience it, including hardware, firmware, and any connected systems.

Types of System Tests

Functional Testing: Does the system do what it is supposed to?
  • Does the thermostat turn on heating when temperature drops below the setpoint?
  • Does the alarm trigger when motion is detected?
  • Does the display show correct data?
Stress Testing: How does the system behave under extreme conditions?
  • Maximum sensor reading rate
  • Full memory usage
  • Maximum number of connected devices
Endurance Testing: Does the system remain stable over extended periods?
  • Run continuously for days/weeks
  • Monitor for memory leaks, crashes, or degradation
Environmental Testing: How does the system handle environmental extremes?
  • Temperature range (hot and cold)
  • Power supply variations
  • Electromagnetic interference

The Testing Pyramid

         /
        /          System Tests (few, expensive, slow)
       / ST 
      /------
     /            Integration Tests (moderate number)
    /   IT     
   /------------
  /                Unit Tests (many, cheap, fast)
 /      UT        
/------------------
  • Unit tests: Write many. They are fast, cheap, and catch most bugs early.
  • Integration tests: Write a moderate number. They catch interface issues between modules.
  • System tests: Write fewer but critical ones. They validate the complete product.

Best Practices

  1. Separate hardware-dependent and hardware-independent code. This makes unit testing much easier since you can test logic on your PC without the target hardware.
  2. Write tests alongside code, not after. If you wait until the end, you will never write them.
  3. Automate tests. Use your build system (Makefile, CMake) to run unit tests automatically on every build.
  4. Test edge cases. What happens with zero input? Maximum value? Negative numbers? NULL pointers?
  5. Test error paths. Verify your code handles failures gracefully (sensor disconnected, communication timeout, invalid data).

Summary

Testing embedded systems requires a layered approach. Unit tests catch logic bugs early and run fast on your development PC. Integration tests verify module interactions, often on real hardware. System tests validate the complete product against requirements. The more you test early (unit level), the fewer expensive bugs you find late (system level). Investing in testable code architecture, with clear separation between hardware and logic, pays off enormously throughout the life of your project.

📖 Related: Hardware Abstraction Layer (HAL) Design in C — With Examples

Leave a Reply

Your email address will not be published. Required fields are marked *