A unit test refers to an automated test that verifies a clearly defined software unit in isolation. Such a unit is typically a single module, function, or class. The goal is to verify the behavior of this unit independently of the rest of the system by comparing defined inputs with expected outcomes. Unit tests are intentionally small, quick to execute, and designed to clearly attribute errors to a specific code unit. Unit tests are a fundamental component of modern Embedded Software Development.
Content
Unit Tests for Embedded Systems
In the context of embedded systems, the term takes on a specific meaning. A unit test for embedded systems describes the isolated testing of firmware components, which is usually performed not on the target hardware, but on a development machine or build server (off-target). This decoupling is necessary because Embedded Software often interacts directly with hardware that is not always available during development or whose use would slow down testing.
What is a „unit“
A unit is a self-contained implementation unit, typically a single module or class, often represented by a .cor .cpp-file. This unit will be embedded in a test environment that allows for its isolated execution.
A test case describes a concrete test scenario that either passes or fails. It consists of an initialization (setup) and one or more checks. These checks are formulated as test assertions, i.e., statements about expected states or return values.
Multiple test cases are combined into a test suite, which runs as an executable program and represents the entirety of the tests.
The unit being tested is referred to as the Code Under Test (CUT). Its direct dependencies are called Depended on Components (DOC), while indirect dependencies are called Transitively Depended on Components (TDOC). This distinction is relevant in the embedded domain because hardware accesses often occur across multiple abstraction layers.
Unit Tests Without a Framework
Besides established frameworks, a pragmatic approach exists in the embedded sector: framework-lose Unit Tests, often realized as simple .c-Files with direct assertions.
A typical example is the use of assert() from the standard library:
int main(void)
{
CanConfig config = {0};
CanUnit unit = {0};
bool result = CAN_Init(&config, &unit, 500000U, 2000000U);
assert(result == true);
assert(config.ctrl_baudrate == 500000U);
assert(config.data_baudrate == 2000000U);
assert(config.fd_enabled == true);
assert(unit.initialized == true);
assert(unit.active_ctrl_baudrate == 500000U);
assert(unit.active_data_baudrate == 2000000U);
assert(unit.fd_active == true);
return 0;
}
This approach completely dispenses with testing frameworks, test runners, or mocking infrastructure. The tests are compiled and executed as standalone programs.
Furthermore, assertions are used within functions to safeguard preconditions. These are defensive checks in the code. For example, they verify if passed parameters are valid and stop execution early in case of violations. These checks are part of the implementation and address the correct usage of the interface by the caller. Both cannot be sharply separated: assertions can be defensive checks or unit tests.
In an embedded context, this approach is particularly relevant where traditional unit testing infrastructures are difficult to implement – for example, with direct access to hardware registers or with heavily hardware-coupled logic. Here, a simple, controlled test setup can help verify behavior, at least in specific instances.
Unit Tests in Functional Safety
In the context of functional safety, unit tests are not just a development tool but a component of a structured verification concept. They serve to verify the correct implementation of software units against their specified requirements and are typically anchored in the left-hand branch of the V-model.
In the V-model, development proceeds along specification levels (system, software architecture, modules), while verification takes place in a mirrored fashion on the right side. Unit tests are assigned to the lowest level of verification and check whether individual software units correctly fulfill their specified functions. They are directly related to low-level requirements.
A central criterion in this environment is the Code Coverage, So, code coverage by tests. This is not an end in itself, but rather proof that relevant parts of the implementation have actually been executed and verified.
Important metrics include:
- Statement Coverage describes what percentage of the instructions in the code were executed at least once. It provides a basic statement on whether the code was reached at all.
- Branch Coverage goes one step further and considers decision structures. It measures whether all possible branches, for example in
IforSwitch-constructs, went through.
Unit Test Tools
In the embedded environment, a distinction is made between certified (qualifiable) tools for regulated industries and free frameworks for development.
Certified / Qualifiable Tools (DO-178C, ISO 26262)
These tools are designed for functional safety and support requirements such as coverage, traceability, and audit trails.
- TESSY (Razorcat)
Automates the entire unit testing process, including test design, execution, reporting, and coverage and traceability. .
Certified for standards such as ISO 26262 and IEC 61508. . - QA Systems Cantata
Unit and integration testing tool for C/C++ with a focus on automated test execution, reporting, and integration into embedded toolchains. . - Parasoft C/C++test
Combines unit testing, static analysis, Code Coverage and Requirements Traceability. TÜV-certified for safety-critical applications and supports standards such as ISO 26262 and DO-178C. . - VectorCAST (Overview)
Industrial tool for automated unit testing and coverage in safety-critical environments, frequently used in the avionics and automotive sectors.
Free Unit Test Frameworks
These tools are not certified, but they are technically widespread and form the basis of many embedded test strategies.
- GoogleTest
Standard in the C++ environment with good integration and mocking support. For safety-critical projects, additional tools for coverage and traceability are necessary. . - CppUTest
Lightweight framework specifically for embedded systems, with a focus on small footprints and easy integration. - Unity / Ceedling
Widely used in the C environment, especially through automatic mock generation (CMock) and simple toolchain integration.
Summary
The importance of unit tests is particularly high in the embedded sector, as systems are often operated under restrictive conditions, have long life cycles, and are difficult to access in case of errors. Unit tests make it possible to detect errors early in the development process and ensure the maintainability of the software throughout its entire lifespan. They also form the foundation for modern development approaches such as Test-Driven Development, where tests actively guide the implementation process.
Overall, unit tests – particularly in the embedded context – represent a key tool for systematically, reproducibly, and hardware-independent verification of software.
Zurück zum Glossar