Embedded Software Testing: Complete Guide to Types, Tools & Best Practices (2026)

Robonito Team
Robonito Team
Master embedded testing in 2026

A firmware bug in a car's brake controller. A memory overflow in a cardiac pacemaker. A timing violation in an aircraft flight management system. These are not theoretical risks — they are the real consequences of inadequate embedded software testing. This guide covers everything embedded teams need: testing types, frameworks with real code, key safety standards, HIL testing, and how to build a CI/CD pipeline for embedded systems in 2026.

Originally published in 2023. Updated and expanded for 2026 with HIL testing, embedded CI/CD, safety standards, RTOS testing, and modern tooling.

By Robonito Engineering Team · Updated May 2026 · 20 min read


Quick stats

FactSource
Software defects account for 40% of automotive recalls in modern vehiclesNHTSA Vehicle Recall Database 2025
A software defect found post-production in medical devices costs 100× more to fix than pre-certificationFDA Software Guidance
ISO 26262 compliance requires test coverage of at least MC/DC (Modified Condition/Decision Coverage) for ASIL DISO 26262:2018
73% of embedded teams report that inadequate test coverage causes project delaysVDC Research Embedded Market 2025
Hardware-in-the-loop testing reduces integration defects by up to 60% vs software-only testingdSPACE Customer Study 2025

Table of Contents

  1. What makes embedded software testing different
  2. The embedded testing pyramid
  3. Unit testing for embedded C/C++
  4. Integration testing in embedded systems
  5. Hardware-in-the-loop (HIL) testing
  6. RTOS and real-time testing
  7. Static analysis and code quality
  8. Safety standards and compliance testing
  9. Embedded testing tools compared
  10. CI/CD for embedded systems
  11. Web interface testing for embedded devices
  12. Pre-release embedded testing checklist
  13. Frequently Asked Questions


Testing the web interface of your embedded device or IoT platform?

Robonito auto-generates tests for device management dashboards, configuration portals, and IoT web interfaces — running across browsers in CI with self-healing when your UI changes. Try Robonito free →



1. What makes embedded software testing different

One-sentence definition for featured snippets: Embedded software testing is the verification and validation of software running directly on hardware — microcontrollers, FPGAs, and embedded processors — under real-time constraints, with limited resources, and often without a general-purpose operating system.

Three characteristics of embedded systems make their testing fundamentally different from testing web or mobile applications:

Target hardware as the test environment — unlike web applications where the production environment (a server or browser) closely resembles the development environment, embedded software runs on hardware that may be expensive, fragile, unavailable in quantity, or not yet manufactured. Test strategies must account for when target hardware is and is not available.

Real-time constraints — many embedded systems have hard real-time requirements: a motor control interrupt must complete in 50 microseconds, a CAN bus frame must be transmitted within 1 millisecond, a safety system must respond within 10 milliseconds. Tests must verify not just that functions produce correct outputs, but that they do so within the required timing window.

Limited observability and controllability — a web application's state is fully observable through browser DevTools, logs, and databases. An embedded system running on a microcontroller may have no display, no filesystem, and only a UART output for debugging. Testing requires deliberate design of observability interfaces (test hooks, debug ports, logging infrastructure).


Embedded Software Testing Lifecycle

A typical embedded testing lifecycle includes:

  1. Requirements analysis
  2. Static analysis
  3. Unit testing
  4. Integration testing
  5. Target hardware testing
  6. HIL testing
  7. System testing
  8. Compliance validation
  9. Release certification

Testing should begin as early as possible and continue throughout the product lifecycle.

2. The embedded testing pyramid

The testing pyramid applies to embedded systems, but with important differences from web application testing:

                      ┌─────────────────────────┐
                      │    System Tests / HIL    │ ← Real hardware + simulation
                      │    (few, slow, expensive) │
                   ┌──┴─────────────────────────┴──┐
                   │     Target Integration Tests    │ ← On actual embedded board
                   │     (moderate, on hardware)     │
               ┌───┴────────────────────────────────┴───┐
               │         Host-Based Unit Tests           │ ← On development machine
               │    (many, fast, hardware-independent)   │
           ────┴────────────────────────────────────────┴────
                        Static Analysis (always running)

Host-based unit testing — the majority of embedded tests should run on a developer's workstation or CI server, not on the target hardware. This requires careful architecture: hardware-dependent code is isolated behind abstraction layers (Hardware Abstraction Layer, HAL), and unit tests mock those abstractions. Running tests on the host is 10–100× faster than running on target hardware and requires no physical device.

Target integration testing — a smaller set of tests that must run on real hardware to verify hardware-software interactions, timing, and device driver behaviour.

HIL / system testing — the smallest, most expensive layer. Full system validation connecting the real controller to a hardware simulator or test rig.

The common embedded testing mistake is inverting this pyramid — testing primarily on hardware because "that is what it runs on" — which creates slow, expensive, hardware-dependent test cycles.


3. Unit testing for embedded C/C++

Unit testing embedded C and C++ requires frameworks designed for resource-constrained environments. Standard web testing frameworks are not suitable — they assume a full OS, heap allocators, and C++ standard library features that may not be available on bare-metal systems.

Unity — the most widely used embedded C unit testing framework

Unity is a lightweight C testing framework designed specifically for embedded systems. It has no dependencies, fits in under 3KB of flash, and runs on any platform from Arduino to Linux.

/* test_temperature_sensor.c — Unity unit test for temperature sensor driver */
#include "unity.h"
#include "temperature_sensor.h"
#include "mock_adc.h"  /* CMock-generated mock for ADC hardware abstraction */

void setUp(void) {
    /* Called before each test — reset mock expectations */
    mock_adc_Init();
}

void tearDown(void) {
    /* Called after each test — verify all mock expectations were met */
    mock_adc_Verify();
    mock_adc_Destroy();
}

void test_TemperatureSensor_ReturnsCorrectDegreesC_ForMidrangeADCValue(void) {
    /* Arrange: mock ADC returns 512 (mid-range of 0-1023 for 3.3V reference) */
    /* ADC 512 → 1.65V → 25°C for this sensor's transfer function */
    ADC_Read_ExpectAndReturn(ADC_CHANNEL_TEMP, 512);

    /* Act */
    float temp_celsius = TemperatureSensor_ReadCelsius();

    /* Assert */
    TEST_ASSERT_FLOAT_WITHIN(0.5f, 25.0f, temp_celsius);
    /* ±0.5°C tolerance accounts for floating-point rounding */
}

void test_TemperatureSensor_ReturnsErrorCode_WhenADCReadFails(void) {
    /* Arrange: simulate ADC hardware failure */
    ADC_Read_ExpectAndReturn(ADC_CHANNEL_TEMP, ADC_ERROR);

    /* Act */
    float temp_celsius = TemperatureSensor_ReadCelsius();

    /* Assert: error sentinel value returned, not a garbage temperature */
    TEST_ASSERT_EQUAL_FLOAT(TEMPERATURE_SENSOR_ERROR, temp_celsius);
}

void test_TemperatureSensor_HandlesMaximumADCValue(void) {
    /* Boundary test: ADC saturated at 1023 (maximum 10-bit value) */
    ADC_Read_ExpectAndReturn(ADC_CHANNEL_TEMP, 1023);

    float temp_celsius = TemperatureSensor_ReadCelsius();

    /* Verify result is within physical sensor range (not NaN, not infinity) */
    TEST_ASSERT_TRUE(temp_celsius > -40.0f);   /* Below absolute minimum */
    TEST_ASSERT_TRUE(temp_celsius < 150.0f);   /* Above absolute maximum */
}

/* Unity test runner — main() calls each test function */
int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_TemperatureSensor_ReturnsCorrectDegreesC_ForMidrangeADCValue);
    RUN_TEST(test_TemperatureSensor_ReturnsErrorCode_WhenADCReadFails);
    RUN_TEST(test_TemperatureSensor_HandlesMaximumADCValue);
    return UNITY_END();
}

GoogleTest — for C++ embedded systems

// test_pid_controller.cpp — GoogleTest for PID controller
#include <gtest/gtest.h>
#include "pid_controller.h"
#include "mock_actuator.h"

class PIDControllerTest : public ::testing::Test {
protected:
    PIDController pid;
    MockActuator mock_actuator;

    void SetUp() override {
        // Standard PID tuning parameters for this system
        pid.Configure(
            /* Kp */ 2.0f,
            /* Ki */ 0.5f,
            /* Kd */ 0.1f,
            /* dt_ms */ 10,   // 10ms sample period
            /* output_min */ -100.0f,
            /* output_max */  100.0f
        );
    }
};

TEST_F(PIDControllerTest, ProportionalTerm_ProducesCorrectOutput_ForPositiveError) {
    float setpoint = 50.0f;
    float measured = 40.0f;  // Error = 10.0
    float expected_proportional = 2.0f * 10.0f;  // Kp * error

    // Single step with zero integral and derivative history
    float output = pid.Compute(setpoint, measured);

    EXPECT_NEAR(expected_proportional, output, 0.01f);
}

TEST_F(PIDControllerTest, OutputClamped_WhenIntegralWindupOccurs) {
    // Drive integrator to maximum
    for (int i = 0; i < 1000; i++) {
        pid.Compute(100.0f, 0.0f);  // Large sustained error
    }
    float output = pid.Compute(100.0f, 0.0f);

    // Output must be clamped, not exceeding max
    EXPECT_LE(output, 100.0f);
    EXPECT_GE(output, -100.0f);
}

TEST_F(PIDControllerTest, IntegratorResets_WhenResetCalled) {
    // Accumulate integrator state
    pid.Compute(100.0f, 0.0f);
    pid.Compute(100.0f, 0.0f);

    pid.Reset();  // Reset should clear all state

    // After reset, output should be purely proportional
    float output = pid.Compute(50.0f, 40.0f);  // Error = 10
    EXPECT_NEAR(20.0f, output, 0.1f);  // Only Kp * error
}

Hardware Abstraction Layer — the key to testable embedded code

/* hal_adc.h — Hardware Abstraction Layer interface */
/* Production: implemented by hal_adc_stm32f4.c */
/* Testing:    implemented by mock_adc.c (generated by CMock) */

#ifndef HAL_ADC_H
#define HAL_ADC_H

#include <stdint.h>

#define ADC_ERROR    (-1)
#define ADC_CHANNEL_TEMP   0
#define ADC_CHANNEL_VBAT   1

/**
 * @brief Read ADC channel
 * @param channel ADC channel number
 * @return 12-bit ADC value (0-4095), or ADC_ERROR on failure
 */
int32_t ADC_Read(uint8_t channel);

/**
 * @brief Initialize ADC hardware
 * @return 0 on success, negative error code on failure
 */
int32_t ADC_Init(void);

#endif /* HAL_ADC_H */

/* The production implementation (hal_adc_stm32f4.c) uses STM32 HAL:
   int32_t ADC_Read(uint8_t channel) {
       ADC_ChannelConfTypeDef sConfig = {0};
       sConfig.Channel = adc_channel_map[channel];
       HAL_ADC_ConfigChannel(&hadc1, &sConfig);
       HAL_ADC_Start(&hadc1);
       HAL_ADC_PollForConversion(&hadc1, 10);
       return HAL_ADC_GetValue(&hadc1);
   }

   The mock implementation (generated by CMock from the header):
   int32_t ADC_Read(uint8_t channel) {
       // Returns whatever value the test specified with ADC_Read_ExpectAndReturn()
   }
*/

4. Integration testing in embedded systems

Integration testing in embedded systems verifies that software modules communicate correctly across their interfaces — function call APIs, shared memory regions, message queues, interrupt handlers, and hardware registers.

Software integration testing on host

/* test_sensor_to_logger_integration.c */
/* Tests the integration between temperature sensor module and data logger module */
/* Runs on host — both modules compiled for x86, hardware mocked */

#include "unity.h"
#include "temperature_sensor.h"
#include "data_logger.h"
#include "mock_adc.h"
#include "mock_flash_storage.h"

void test_TemperatureReading_LoggedCorrectly_WhenAboveThreshold(void) {
    /* Arrange: temperature above warning threshold (30°C) */
    ADC_Read_ExpectAndReturn(ADC_CHANNEL_TEMP, 614);  // → 30.5°C

    /* Expect logger to write one entry with correct data */
    LogEntry_t expected_entry = {
        .sensor_id   = SENSOR_TEMPERATURE,
        .value_x100  = 3050,  // 30.50°C stored as integer × 100
        .severity    = LOG_WARNING,
        .timestamp   = 0,     // Will be set by logger — ignore in comparison
    };
    FlashStorage_Write_ExpectWithArrayAndReturn(
        0,                    // Any address
        &expected_entry,
        sizeof(LogEntry_t),
        1                     // Return success
    );

    /* Act: run the sensor-to-logger pipeline */
    float temp = TemperatureSensor_ReadCelsius();
    DataLogger_LogSensorReading(SENSOR_TEMPERATURE, temp);

    /* Assert: Unity verifies CMock expectations were all called */
}

5. Hardware-in-the-loop (HIL) testing

Hardware-in-the-loop testing connects the real embedded controller to a real-time simulator that models the physical plant (engine, battery, motor, vehicle dynamics). The controller receives realistic, time-accurate sensor inputs and sends actuator commands that the simulator responds to.

Why HIL is essential for safety-critical systems:

Software-only simulation on a host machine cannot replicate the timing accuracy that safety-critical control algorithms require. A motor control algorithm that runs correctly in simulation at 1× speed may produce instability at the 10kHz interrupt rate of real hardware. HIL testing catches these timing-dependent issues by running the actual controller software at actual execution speed.

HIL test architecture

┌─────────────────────────────────────────────────────────┐
│                   HIL Test System                        │
│                                                          │
│  ┌─────────────────┐         ┌──────────────────────┐   │
│  │  Real-time       │ I/O    │  Embedded Controller  │   │
│  │  Simulator       │◄──────►│  (Device Under Test)  │   │
│  │  (dSPACE/        │ boards │  Running actual        │   │
│  │   NI VeriStand)  │        │  production firmware   │   │
│  │                  │        │                        │   │
│  │ Models:          │        │ Inputs received:       │   │
│  │ - Engine model   │        │ - CAN messages         │   │
│  │ - Vehicle dyn.   │        │ - Analog sensor signals│   │
│  │ - Battery model  │        │ - Digital I/O          │   │
│  │ - Fault inject.  │        │                        │   │
│  └─────────────────┘         └──────────────────────┘   │
│           │                                               │
│           ▼                                               │
│  ┌────────────────────────┐                              │
│  │  Test Automation Layer  │                              │
│  │  (Python / MATLAB)      │                              │
│  │  - Define test sequences│                              │
│  │  - Inject fault conditions│                            │
│  │  - Capture responses    │                              │
│  │  - Verify timing        │                              │
│  └────────────────────────┘                              │
└─────────────────────────────────────────────────────────┘
## HIL test script — Python automation layer (dSPACE/ETAS/National Instruments)
## Tests emergency braking system response to sudden obstacle detection

import hil_interface as hil
import time

def test_emergency_braking_activates_within_50ms():
    """
    Regulatory requirement: Emergency braking must activate
    within 50ms of obstacle detection signal assertion.
    ISO 26262 ASIL-C requirement.
    """
    ## Set up initial conditions — vehicle at 80 km/h on flat road
    hil.set_signal("vehicle_speed_kmh", 80.0)
    hil.set_signal("road_gradient_deg", 0.0)
    hil.set_signal("brake_pedal_position_pct", 0.0)

    ## Wait for system to reach steady state
    time.sleep(0.1)

    ## Record precise timestamp of obstacle detection signal assertion
    t_obstacle_detected = hil.get_timestamp()
    hil.set_signal("obstacle_detected", True)

    ## Poll for brake actuator response
    brake_activated = False
    t_brake_activated = None

    for _ in range(100):  ## Poll for up to 100ms
        time.sleep(0.001)  ## 1ms polling interval
        brake_pressure = hil.get_signal("brake_pressure_bar")
        if brake_pressure > 5.0:  ## Threshold for "activated"
            brake_activated = True
            t_brake_activated = hil.get_timestamp()
            break

    ## Assert: braking activated
    assert brake_activated, "Emergency braking did not activate after obstacle detection"

    ## Assert: response time within 50ms requirement
    response_time_ms = (t_brake_activated - t_obstacle_detected) * 1000
    assert response_time_ms <= 50.0, \
        f"Emergency braking response time {response_time_ms:.1f}ms exceeds 50ms requirement"

    print(f"✅ Emergency braking response: {response_time_ms:.1f}ms (requirement: ≤50ms)")

6. RTOS and real-time testing

Real-Time Operating System (RTOS) testing adds another dimension beyond function correctness: timing correctness. An embedded system running FreeRTOS, Zephyr, VxWorks, or QNX must meet deterministic timing requirements — tasks must complete within their deadline, context switches must take predictable time, and interrupt latency must remain bounded.

Timing violation detection

/* Watchdog timer pattern for detecting timing violations in RTOS tasks */
/* Used when formal timing analysis tools are not available */

#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"

/* Maximum allowed execution time for motor control task: 500 microseconds */
#define MOTOR_TASK_DEADLINE_US    500U
#define TIMER_PERIOD_US           600U  /* Expires if task exceeds 500us */

static TimerHandle_t deadline_timer;
static volatile bool deadline_violated = false;

static void DeadlineTimerCallback(TimerHandle_t xTimer) {
    /* This callback fires if the motor control task ran too long */
    deadline_violated = true;
    /* In production: trigger safety shutdown */
    /* In testing: record violation for test assertion */
}

void MotorControlTask(void *pvParameters) {
    TickType_t xLastWakeTime = xTaskGetTickCount();

    for (;;) {
        /* Start deadline monitoring */
        deadline_violated = false;
        xTimerStart(deadline_timer, 0);

        /* Critical control loop — must complete within 500us */
        uint32_t adc_raw = ADC_Read(ADC_CHANNEL_CURRENT_SENSE);
        float current_amps = ConvertAdcToAmps(adc_raw);
        float pwm_duty = PID_Compute(&motor_pid, current_setpoint, current_amps);
        PWM_SetDuty(PWM_CHANNEL_MOTOR, pwm_duty);

        /* Stop deadline monitoring */
        xTimerStop(deadline_timer, 0);

        /* TEST ASSERTION: No deadline violation occurred */
        configASSERT(!deadline_violated);

        /* Wait for next 1ms period */
        vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(1));
    }
}

7. Static analysis and code quality

Static analysis examines source code without executing it, finding defects, undefined behaviour, MISRA C/C++ violations, and security vulnerabilities. For safety-critical embedded systems, static analysis is not optional — it is mandated by ISO 26262, DO-178C, and IEC 61508.

MISRA C compliance — common violations and fixes

/* MISRA C:2012 — common violations in embedded code */

/* ❌ Rule 14.4: Controlling expression of if/while must be essentially Boolean */
int sensor_value = ADC_Read(ADC_CHANNEL_1);
if (sensor_value) {  /* MISRA violation: integer used as Boolean */
    ProcessSensorData(sensor_value);
}

/* ✅ MISRA compliant */
int32_t sensor_value = ADC_Read(ADC_CHANNEL_1);
if (sensor_value > 0) {  /* Explicit comparison */
    ProcessSensorData(sensor_value);
}

/* ❌ Rule 17.7: Return value of non-void function shall be used */
memcpy(dest_buffer, src_buffer, BUFFER_SIZE);  /* Return value discarded */

/* ✅ MISRA compliant */
void *result = memcpy(dest_buffer, src_buffer, BUFFER_SIZE);
if (result == NULL) {
    HandleCopyError();
}

/* ❌ Rule 12.1: Pointer arithmetic increases maintenance risk */
uint8_t *ptr = buffer;
*(ptr + 5) = 0xFF;  /* Pointer arithmetic without bounds check */

/* ✅ MISRA compliant — use index notation with bounds check */
if (5U < BUFFER_SIZE) {
    buffer[5] = 0xFFU;
}

Code coverage for safety-critical systems

ISO 26262 ASIL-D requires Modified Condition/Decision Coverage (MC/DC) — every condition in every decision must independently affect the outcome. Standard branch coverage (used in most web testing) is insufficient for safety-critical certification.

## Makefile — code coverage with gcov for embedded code (host compilation)
CFLAGS += --coverage -O0 -g
LDFLAGS += --coverage

coverage: test
	lcov --capture --directory . --output-file coverage.info
	lcov --remove coverage.info '/usr/*' '*/test/*' --output-file coverage_filtered.info
	genhtml coverage_filtered.info --output-directory coverage_report
	@echo "Coverage report: coverage_report/index.html"
	## For ASIL compliance: verify MC/DC coverage requirements met
	## Commercial tools (LDRA, VectorCAST) provide MC/DC measurement

8. Safety standards and compliance testing

ISO 26262 — Automotive functional safety

ISO 26262 defines Automotive Safety Integrity Levels (ASIL A through D) based on severity, exposure, and controllability of hazardous events. Higher ASIL ratings require more rigorous testing:

ASIL LevelApplication exampleRequired coverageUnit testingHIL required
ASIL AWindshield wipersStatement coverageRecommendedNo
ASIL BPower windows, sunroofBranch coverageRequiredRecommended
ASIL CElectric power steeringMC/DC coverageRequiredRequired
ASIL DBrake-by-wire, airbagsMC/DC + fault injectionRequiredRequired

DO-178C — Aviation software

DO-178C classifies airborne software by Design Assurance Level (DAL):

DALFailure conditionImpactTesting requirements
ACatastrophicLoss of aircraftMC/DC, structural coverage, independent review
BHazardousSerious injuryDecision coverage, reviews
CMajorMinor injury or aircraft issueStatement coverage
DMinorInconvenienceTesting per plan
ENo safety effectNoneNo requirements

IEC 61508 — Industrial functional safety

IEC 61508 defines Safety Integrity Levels (SIL 1–4) for industrial systems including process control, manufacturing automation, and energy systems. SIL 4 (highest) requires fault tolerance, diverse redundancy, and formal verification methods in addition to comprehensive testing.


Common Challenges in Embedded Software Testing

Embedded teams frequently encounter:

  • Limited hardware availability
  • Real-time timing verification
  • Hardware-software dependency issues
  • Incomplete observability
  • Certification requirements
  • Long hardware test cycles

Modern CI pipelines using host testing, simulation, and HIL help reduce these challenges.

Embedded Software Testing vs Traditional Software Testing

AspectEmbedded Software TestingTraditional Software Testing
EnvironmentHardware dependentBrowser, desktop, cloud
Timing ConstraintsCriticalUsually less critical
Resource LimitsMemory and CPU constrainedGenerally abundant resources
Hardware InteractionRequiredRare
Safety StandardsISO 26262, DO-178CUsually not required
Testing CostHigherLower

9. Embedded testing tools compared

Unit testing frameworks

FrameworkLanguageTarget deploymentStandards supportBest for
UnityCBare metal + RTOSMISRA friendlyMost embedded C projects
CppUTestC/C++Host + targetC++ embedded projects
GoogleTestC++Host (primarily)Modern C++ embedded
CpputestC/C++Host + targetLegacy C++ projects
CTest (CMake)AnyHost + CICMake-based projects

Commercial embedded test automation

ToolTypeStandardsBest for
VectorCASTUnit test + coverageISO 26262, DO-178C, IEC 61508Automotive, aerospace
LDRAStatic analysis + testingAll major safety standardsAviation, medical
Parasoft C/C++testUnit test + staticISO 26262, MISRAAutomotive, industrial
Polyspace (MathWorks)Static analysisISO 26262, MISRAAutomotive

HIL simulation platforms

PlatformProviderBest for
dSPACEdSPACEAutomotive, complex plant models
NI VeriStandNational InstrumentsMulti-domain, scalable
Wind River SimicsWind RiverProcessor simulation, virtual prototypes
QEMUOpen sourceProcessor emulation, CI integration
RenodeAntmicroOpen-source embedded system simulation

Open-source simulation for CI

## QEMU — run embedded tests in CI without physical hardware
## Example: test ARM Cortex-M firmware in GitHub Actions

- name: Build firmware for STM32
  run: |
    arm-none-eabi-gcc \
      -mcpu=cortex-m4 \
      -mthumb \
      -DTESTING \
      -o firmware_test.elf \
      src/*.c test/*.c

- name: Run unit tests in QEMU (ARM emulation)
  run: |
    qemu-system-arm \
      -machine lm3s6965evb \
      -kernel firmware_test.elf \
      -semihosting-config enable=on,target=native \
      -nographic \
      -serial stdio \
      -monitor null | tee test_output.txt
    grep -q "OK" test_output.txt || exit 1

10. CI/CD for embedded systems

Integrating embedded software testing into CI/CD pipelines reduces the time between a defect introduction and its detection. The architecture uses host-based tests for speed and QEMU simulation for target verification, reserving physical hardware for pre-release integration.

## .github/workflows/embedded-ci.yml
name: Embedded Software CI

on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  ## Stage 1: Host-based unit tests (fast, no hardware needed)
  unit-tests-host:
    name: Unit Tests (Host x86_64)
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4

      - name: Install build tools
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc g++ cmake gcovr
          pip install gcovr --break-system-packages

      - name: Build unit tests for host (x86_64)
        run: |
          cmake -B build_host \
            -DTARGET=host \
            -DBUILD_TESTS=ON \
            -DCMAKE_C_FLAGS="--coverage -O0"
          cmake --build build_host

      - name: Run unit tests
        run: |
          cd build_host
          ctest --output-on-failure --timeout 60

      - name: Generate coverage report
        run: |
          gcovr --root . \
            --html-details coverage/index.html \
            --xml coverage/coverage.xml \
            --exclude '*/test/*' \
            --exclude '*/third_party/*'

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

  ## Stage 2: Target simulation tests (QEMU)
  target-simulation:
    name: Target Tests (QEMU ARM)
    runs-on: ubuntu-latest
    needs: unit-tests-host
    steps:
      - uses: actions/checkout@v4

      - name: Install cross-compilation toolchain + QEMU
        run: |
          sudo apt-get install -y \
            gcc-arm-none-eabi \
            binutils-arm-none-eabi \
            qemu-system-arm

      - name: Build firmware for ARM Cortex-M target
        run: |
          cmake -B build_arm \
            -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake \
            -DTARGET=stm32f4 \
            -DBUILD_TESTS=ON
          cmake --build build_arm

      - name: Run tests in QEMU ARM emulation
        run: |
          timeout 120 qemu-system-arm \
            -machine lm3s6965evb \
            -kernel build_arm/firmware_tests.elf \
            -semihosting-config enable=on,target=native \
            -nographic \
            -serial stdio 2>&1 | tee qemu_output.txt

          ## Verify tests passed
          grep -q "PASS\|OK" qemu_output.txt && \
          ! grep -q "FAIL\|ASSERT" qemu_output.txt

  ## Stage 3: Static analysis (MISRA, undefined behaviour)
  static-analysis:
    name: Static Analysis (cppcheck + MISRA)
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: sudo apt-get install -y cppcheck
      - name: Run cppcheck with MISRA rules
        run: |
          cppcheck \
            --enable=all \
            --std=c11 \
            --platform=arm32-wchar_t4 \
            --suppress=missingInclude \
            --error-exitcode=1 \
            --xml \
            src/ 2> cppcheck_results.xml
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: static-analysis-results
          path: cppcheck_results.xml

11. Web interface testing for embedded devices

Modern embedded systems increasingly expose web-based management interfaces — device configuration portals, IoT dashboards, fleet management consoles, and remote monitoring tools. These web interfaces require the same rigorous testing as any web application.

This is where Robonito provides direct value to embedded teams: automated AI-powered testing of the web interfaces that accompany embedded devices, without requiring any scripting.

Common embedded device web interfaces that need testing:

  • Device configuration portals — network settings, firmware updates, parameter configuration
  • IoT dashboard UIs — sensor data visualisation, alert management, device fleet monitoring
  • Industrial HMI web panels — process control interfaces, alarm management, historian views
  • Medical device management systems — device configuration, patient data review, calibration interfaces
// Playwright test for IoT device management dashboard
// (Embedded team testing their web interface, not the firmware)
import { test, expect } from '@playwright/test';

test.describe('IoT Device Management Dashboard', () => {

  test.beforeEach(async ({ page }) => {
    await page.goto('https://iot-dashboard.yourcompany.com');
    await page.getByLabel('Username').fill('admin');
    await page.getByLabel('Password').fill(process.env.DASHBOARD_ADMIN_PASSWORD!);
    await page.getByRole('button', { name: 'Sign in' }).click();
    await expect(page).toHaveURL(/\/dashboard/);
  });

  test('device list shows all registered devices with correct status', async ({ page }) => {
    await page.getByRole('link', { name: 'Devices' }).click();

    // All registered test devices should appear
    await expect(page.getByTestId('device-count')).not.toHaveText('0');

    // Status indicators should be accurate
    const onlineDevices = page.getByTestId('status-online');
    const offlineDevices = page.getByTestId('status-offline');

    await expect(onlineDevices.first()).toBeVisible();
    // Verify status badge accessibility
    await expect(onlineDevices.first()).toHaveAttribute('aria-label', /online/i);
  });

  test('firmware update workflow completes without error', async ({ page }) => {
    await page.getByRole('link', { name: 'Firmware' }).click();
    await page.getByRole('button', { name: 'Upload firmware' }).click();

    // Select test firmware file
    const fileInput = page.locator('input[type="file"]');
    await fileInput.setInputFiles('./test-fixtures/firmware-v2.2.0-test.bin');

    await expect(page.getByTestId('firmware-file-name')).toHaveText('firmware-v2.2.0-test.bin');
    await expect(page.getByTestId('firmware-file-size')).not.toHaveText('0 bytes');

    // Confirm upload (do not trigger actual deployment in test)
    await expect(page.getByRole('button', { name: 'Deploy to devices' })).toBeVisible();
  });
});

Robonito automates this web interface testing without scripting — recording the management portal interactions, generating test cases, and running them across browsers in CI automatically.


12. Pre-release embedded testing checklist

Software testing (host-based)

  • Unit test suite achieving required coverage level for target ASIL/DAL
  • All MISRA C/C++ violations resolved or formally justified
  • Static analysis completed with no critical findings
  • Memory analysis completed (no leaks, no buffer overflows)
  • All boundary values tested for sensor inputs, register values, and communication parameters
  • Error handling tested for all hardware failure modes

Target and integration testing

  • All software modules tested on target hardware (not just host simulation)
  • Interrupt latency measured and verified within requirements
  • Stack usage measured — minimum 20% headroom below stack size
  • Timing-critical tasks verified to meet deadlines under worst-case load
  • Communication protocol conformance tested (CAN, SPI, I2C, UART, Ethernet)
  • Flash and RAM usage within allocation budget

HIL testing (safety-critical systems)

  • All normal operating scenarios validated at representative timing
  • Fault injection tests completed for all single-point failures
  • Response time requirements verified under simulation
  • System behaviour verified during hardware reset and power cycle sequences
  • Communication loss scenarios tested and safe states verified

Standards compliance

  • Required code coverage level achieved and documented
  • Traceability matrix linking requirements → test cases → results complete
  • All safety requirements traced to at least one passing test
  • Test reports generated in format required by certification authority
  • Tool qualification evidence available for all testing tools (DO-330 / ISO 26262 Part 8)

Web interface (for devices with management UI)

  • Device management web UI tested cross-browser (Chrome + Safari minimum)
  • Configuration changes reflected correctly in device behaviour
  • Firmware upload workflow tested end-to-end in staging
  • Authentication and authorisation verified for all user roles
  • Mobile responsiveness verified for field service tablet use cases

Common Embedded Software Testing Interview Questions

What is the difference between HIL and SIL testing?

Software-in-the-Loop (SIL) testing executes the software against simulated models, while Hardware-in-the-Loop (HIL) testing runs the actual controller hardware against a real-time simulator.

Why is MC/DC coverage important?

MC/DC coverage verifies that each condition in a decision independently affects the outcome. It is required by standards such as ISO 26262 ASIL D and DO-178C Level A.

Which embedded testing framework is most widely used?

Unity remains one of the most widely adopted open-source frameworks for embedded C testing because of its small footprint and portability.

Frequently Asked Questions

What is embedded software testing?

Embedded software testing is the verification and validation of software running directly on hardware — microcontrollers, FPGAs, and embedded processors — under real-time constraints, with limited memory and processing resources. It differs from application software testing by requiring tests that run on or simulate the target hardware, account for timing constraints, and verify hardware-software interactions.

What are the main types of embedded software testing?

Unit testing (functions in isolation, typically on host), integration testing (module interactions), hardware-in-the-loop (HIL) testing (software against simulated hardware), target-based testing (on actual hardware), performance and timing testing (real-time constraint verification), and static analysis (code quality without execution).

What standards govern embedded software testing?

ISO 26262 for automotive, DO-178C for aviation, IEC 61508 for industrial systems, IEC 62443 for industrial cybersecurity, and MISRA C/C++ for safety-critical coding standards. The applicable standard depends on the industry and safety integrity level of the system.

What tools are used for embedded unit testing?

Unity and CppUTest for C, GoogleTest for C++, VectorCAST for commercial C/C++ test automation with certification evidence, LDRA for code coverage and MISRA compliance, gcov/lcov for open-source coverage measurement. QEMU provides free processor emulation for CI without physical hardware.

How do you integrate embedded testing into CI/CD?

Host-based unit tests run on every commit (no hardware required, fast). QEMU or Renode simulation runs on every PR (ARM emulation without physical devices). Physical hardware tests run pre-release only (slow, expensive, limited availability). Static analysis runs alongside unit tests. This tiered approach balances thoroughness against pipeline speed.

How is Robonito relevant to embedded software testing?

Robonito specifically tests the web-based management interfaces that accompany modern embedded devices — IoT dashboards, device configuration portals, industrial HMI web panels. It does not test firmware or microcontroller code (that is the domain of Unity, VectorCAST, and HIL tools). For embedded teams building web interfaces alongside their hardware, Robonito provides no-code automated testing of that web layer.


External references



Testing the web dashboard or configuration portal of your embedded device?

Robonito automates the web interface layer of your embedded system — IoT dashboards, device management portals, and industrial HMI web panels — with no-code test generation, cross-browser execution, and self-healing when your UI changes. Start free at Robonito.com →



Automate your QA — no code required

Stop writing test scripts. Start shipping with confidence.

Join thousands of QA teams using Robonito to automate testing in minutes — not months.