Table of Contents
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:- Unit Testing – Test individual functions in isolation
- Integration Testing – Test how modules work together
- 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
| Framework | Description |
|---|---|
| Unity | Lightweight, popular for embedded. Single header + source file. |
| CUnit | Feature-rich framework with test suites and reporting. |
| CMocka | Supports mocking, good for testing with hardware abstractions. |
| Google Test | C++ framework, works for C code too. Very popular in industry. |
The Key Challenge: Hardware Dependencies
Embedded code often calls hardware-specific functions likegpio_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?
- Maximum sensor reading rate
- Full memory usage
- Maximum number of connected devices
- Run continuously for days/weeks
- Monitor for memory leaks, crashes, or degradation
- 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
- 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.
- Write tests alongside code, not after. If you wait until the end, you will never write them.
- Automate tests. Use your build system (Makefile, CMake) to run unit tests automatically on every build.
- Test edge cases. What happens with zero input? Maximum value? Negative numbers? NULL pointers?
- 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

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.







