Integration Test: Module Interaction on Real or Simulated HW
Learn how integration testing validates firmware module interaction on real and simulated hardware, catching timing and interface bugs unit tests miss.

Contents & prerequisites
Why Integration Testing Matters in Embedded Systems
The gap between unit tests and production behavior
Unit tests validate a single module in isolation: a CRC routine, a ring buffer, a state-machine transition table. You mock out everything the module touches—HAL calls, peer modules, interrupt sources—and assert that given inputs produce expected outputs. This is necessary but insufficient. A UART driver that passes every unit test with a mocked DMA controller can still deadlock in production because the real DMA completion interrupt fires before the driver finishes updating its internal state, a race that no mock ever modeled.
Integration testing sits one layer up: it exercises two or more real modules together—often on real or simulated hardware—so that the actual interfaces, actual timing, and actual shared resources are in play. The difference isn't academic. Unit tests answer "does this module do what I designed it to do?" Integration tests answer "do these modules, running together with real clocks and real memory, do what the system needs?" Those are different questions, and a codebase can score 100% unit test coverage while still failing the second one completely.
Catching interface mismatches and timing issues early
Most integration defects fall into a handful of recurring categories: mismatched assumptions about byte ordering or struct padding across a shared buffer, a producer module that writes faster than a consumer can drain, an interrupt handler that assumes it owns a peripheral that another module also configures, or two modules that each assume they initialize first. None of these show up in isolated unit tests because the second party to the bug doesn't exist in that test's world.
Timing issues are the worst subset because they're often not reproducible on demand. A sensor-fusion task that reads an I2C sensor and a SPI IMU in the same control loop might work correctly at 100 Hz in the lab and intermittently miss a deadline at 1 kHz in the field because bus arbitration delays stack up differently under load. Integration testing, run under realistic timing and realistic data rates, is the first place these interactions become visible—well before a customer sees a watchdog reset in the field.
Cost of defects found in integration vs. field failures
The classic defect-cost curve still holds in firmware: a bug caught in integration testing costs an engineer an afternoon with a logic analyzer. The same bug caught after deployment costs a field recall, a CAPA investigation, and possibly a safety review if the product is regulated. Integration testing is the last automated checkpoint before hardware-in-the-loop qualification and field trials, so investment here has an outsized payoff relative to its cost—especially for firmware where a fix after shipment means a field update campaign, not a silent deploy.
Test Environment Strategy: Real Hardware vs. Simulation
Real hardware testing: coverage, realism, and constraints
Running integration tests on the actual target board gives you the real silicon, real clock jitter, real analog behavior, and real peripheral quirks that no model fully captures—errata, marginal timing on bus transactions, power-rail sequencing effects. The cost is throughput: real hardware is slower to provision, harder to parallelize (you need N boards for N concurrent test runs), and prone to flaky failures caused by probe contacts, thermal drift, or board-to-board variation that have nothing to do with the firmware under test.
Simulated environments: repeatability, cost, and scalability
Simulation—whether a full instruction-set simulator, a QEMU-based emulation of your SoC, or a lighter "swap the HAL" build that runs on the host OS—trades realism for repeatability and scale. You can run thousands of integration test permutations overnight on commodity CI runners, inject fault conditions that are hard to force on real silicon (a corrupted DMA descriptor, a bus timeout), and get deterministic, bisectable failures. What you lose is confidence that the simulation models the real electrical and timing behavior accurately; a simulator that doesn't model bus arbitration latency will never catch the bug described above.
Hybrid approaches: when to use each and transition paths
The practical answer most mature embedded teams converge on is a funnel: broad, fast integration test suites run in simulation on every commit, and a smaller, curated subset of scenarios—especially timing-sensitive and peripheral-heavy ones—run nightly or pre-release on a bank of real target boards (a hardware-in-the-loop, or HIL, rig). Treat simulation failures as a fast filter and real-hardware failures as the ones that block a release. Teams running sprint-based development often map this directly onto their Agile methodology, sprint, backlog, Scrum, Kanban workflow: simulated integration tests gate every pull request in the sprint, while HIL runs are scheduled as part of the sprint's definition of done.
Designing Integration Tests for Module Interfaces
Identifying critical module boundaries and data flows
Start from the system's architecture diagram, not the code. Every arrow between boxes—driver to middleware, middleware to application task, task to task via a queue—is a candidate boundary. Prioritize boundaries that cross execution contexts (ISR to task, task to task via RTOS primitives) and boundaries that cross hardware domains (digital logic to analog front end, MCU to external bus device), since these are where assumptions diverge most often.
Test case structure: setup, stimulus, observation, verification
A well-formed integration test has four explicit phases: setup (bring the modules under test to a known state—power-on reset, specific configuration registers, a seeded queue); stimulus (inject the event: a simulated sensor sample, a bus transaction, an interrupt); observation (capture the resulting state—output buffer contents, GPIO toggles, log output, elapsed time); verification (assert the observation matches the expected contract, including timing bounds, not just final values).
// Integration test: sensor task hands data to comms task via queue
void test_sensor_to_comms_handoff(void) {
sensor_task_init();
comms_task_init();
inject_i2c_sample(0x42, sample_payload, sizeof(sample_payload));
uint32_t t0 = test_clock_now();
TEST_ASSERT_QUEUE_RECEIVES(comms_rx_queue, EXPECTED_FRAME, 5 /*ms*/);
uint32_t latency = test_clock_now() - t0;
TEST_ASSERT_LESS_OR_EQUAL(5, latency); // contract: <=5 ms handoff
}
Handling asynchronous events and interrupt interactions
Asynchronous boundaries need tests that deliberately vary event ordering: fire the interrupt before the consuming task has reached its blocking wait, fire it mid-processing, fire two interrupt sources back-to-back to probe priority inversion. If your RTOS or HAL exposes a way to single-step or deterministically interleave contexts in simulation, use it—on real hardware you can approximate this by adding deliberate jitter or load tasks that shift scheduling windows.
State machine validation across modules
When two modules each run their own state machine but must stay synchronized (e.g., a charger state machine and a battery-gauge state machine), write tests that drive both through every legal transition pair, not just the happy path. The cross product of states is usually small enough to enumerate explicitly, and this is exactly where integration bugs cluster—one module reaching a state the other didn't expect it to reach yet.
Hardware Simulation and Test Harnesses
Mock objects and stub modules for unavailable components
Early in development, not all hardware exists. Stub the missing peripheral with a model that replicates its documented register-level behavior—enough to exercise the driver's state machine and error paths, not a full behavioral simulation. Keep stubs under version control alongside the modules they replace, and retire them in favor of real hardware tests as boards become available; a stub that's never replaced becomes a false sense of coverage.
Signal injection and monitoring on real platforms
On real boards, a test harness typically needs a way to inject signals (a programmable power supply to brown out a rail, a signal generator feeding a sensor input, a second MCU emulating a bus peer) and a way to monitor outcomes (logic analyzer capture, UART log capture, GPIO toggle timestamps). Automating both ends—injection and capture—is what turns a bench test into a repeatable integration test rather than a one-off debug session.
Creating reproducible test scenarios across hardware variants
If your product ships multiple hardware revisions, parameterize test scenarios by board variant rather than duplicating test code. A timing contract that holds at 48 MHz on rev A silicon may not hold at 24 MHz on a cost-reduced rev B; the test should assert against the variant's documented timing budget, not a single hardcoded constant.
Logging and traceability for integration test results
Every integration test run should emit a log that ties a failure back to firmware build hash, hardware revision, and test harness configuration. Without this, an intermittent failure reported three weeks later is nearly impossible to reproduce.
Protocol and Bus-Level Integration Testing
Testing module communication over standard interfaces
For modules talking over I2C, SPI, UART, CAN, or USB, integration tests should validate against the protocol specification, not just against the other module's current implementation—otherwise you risk two buggy implementations agreeing with each other. Capture actual bus traffic with a protocol analyzer and compare it against the documented frame format in your bus specification, not just against expected end-to-end results.
Timing and latency validation between components
Define explicit timing contracts for every bus interaction—maximum response latency, minimum inter-frame gap, clock-stretching limits—and look these numbers up in the relevant peripheral datasheet or bus specification rather than guessing. Assert them in automated tests the same way you assert data correctness; a response that's correct but three bus cycles late can be just as broken as wrong data if the consumer times out.
Error handling and recovery in message passing
Deliberately inject malformed frames, NACKs, bus timeouts, and truncated transfers, then verify the receiving module recovers—resets its parser state, raises the correct error flag, doesn't leak a buffer. Error-path integration tests are the ones teams skip under deadline pressure and the ones that most reliably predict field reliability.
Bus contention and arbitration scenarios
Where multiple modules share a bus (several tasks issuing I2C transactions through one mutex-protected driver, or several CAN nodes contending for arbitration), build tests that force contention deliberately—back-to-back requests from two tasks, simultaneous CAN transmission attempts—and verify the arbitration or mutex logic behaves as specified under load, not just in the uncontended case that's easy to test by accident.
Continuous Integration and Test Automation
Automating integration test execution in the CI/CD pipeline
Simulated integration tests belong in the same CI pipeline as unit tests, triggered on every commit or pull request. HIL-based tests, being slower and resource-constrained by the number of physical boards available, are typically scheduled as a gated stage—nightly builds, or a pre-merge gate for release branches—so they don't become the bottleneck for everyday development velocity.
Test result aggregation and reporting
A single integration suite can span dozens of scenarios across multiple boards and simulation targets; aggregate results into a dashboard that shows pass/fail by module boundary and by hardware variant, not just a single pipeline green/red. Engineers need to see at a glance whether a failure is isolated to one board revision or systemic.
Managing test data and fixture versioning
Stimulus data (sample sensor captures, bus traffic recordings, fault-injection scripts) and expected-output fixtures should be versioned alongside the firmware they test, with explicit compatibility tags when a protocol or data format changes—otherwise a fixture update silently invalidates historical test results and nobody notices until a regression slips through.
Regression detection across firmware builds
Track integration test timing results (not just pass/fail) across builds. A test that still passes but whose measured latency has crept from 2 ms to 4 ms is an early warning that a module is drifting toward its contract boundary, often well before it becomes an outright failure.
Common Pitfalls and Debugging Integration Failures
Timing-dependent bugs that only appear in integration
The telltale sign is a test that fails roughly one run in twenty, or fails only under specific build optimization levels. These are almost always races: two modules touching shared state without adequate synchronization, or an interrupt handler whose execution time occasionally overruns its budget under cache-miss conditions. Reproducing them reliably often requires adding controlled jitter (extra NOPs, deliberate scheduling delays) to widen the race window rather than narrow it.
Resource contention and initialization order issues
A large fraction of "intermittent" integration failures trace back to initialization order: module B's init function assumes module A has already configured a clock or GPIO mux, and this holds by accident in the current link order until a build change reorders it. Make initialization dependencies explicit—return codes, init-stage barriers—rather than relying on call-order convention.
Debugging techniques: hardware probes, logs, and emulation traces
For real-hardware failures, a logic analyzer or oscilloscope capturing the actual bus traffic at the moment of failure is worth more than any amount of log analysis—timestamps in firmware logs are themselves subject to the scheduling jitter you're trying to diagnose. For simulation failures, instruction-trace or emulation-trace capture lets you replay the exact failing sequence deterministically, which is the single biggest debugging advantage simulation holds over real hardware.
When to escalate from integration test to live field testing
Integration testing, however thorough, cannot model every real-world condition—RF interference, thermal extremes, connector wear, power-supply noise from the actual end-product enclosure. Once a module interaction passes integration testing cleanly across simulation and HIL, the next checkpoint is field or environmental testing under real operating conditions; treat integration testing as necessary, not sufficient, for release sign-off.
Related reading
- Software Architecture: Monolith, Layered, Microkernel — interfaces between modules.
- CAN FD: Faster Data Phase, Up to 64 Byte Payload — bus-level integration and payload checks.
Learning
Sign in to track your progress.
Evidence
Public projects engineers linked to Integration Test: Module Interaction on Real or Simulated HW.
No engineer has linked a project to this topic yet. Built something that proves it? Add the project and tag it with firmware-software-integration-test-module-interaction-on-real-or-sim — it then shows here and on your public profile.
