Chapter 27: Platform Testing
Introduction
Firmware testing presents unique challenges. The code under test runs before the operating system, often on bare metal with no file system, no networking, and no standard test harness. Traditional software testing tools – JUnit, pytest, Google Test – assume an OS environment that simply does not exist during firmware execution.
Project Mu addresses this challenge with a comprehensive testing strategy that spans multiple layers: host-based unit tests that run on the developer’s machine, integration tests that exercise firmware in QEMU, and Robot Framework tests for automated end-to-end validation. Combined with Stuart’s CI integration, these tools enable test-driven firmware development with the same rigor expected of any modern software project.
Project Mu’s Testing Philosophy
Test Pyramid for Firmware
graph TB
subgraph Pyramid["Testing Pyramid"]
E2E["End-to-End Tests<br/>(Robot Framework + QEMU)<br/>Slow, high fidelity"]
Integration["Integration Tests<br/>(QEMU boot tests, capsule tests)<br/>Medium speed"]
Unit["Host-Based Unit Tests<br/>(cmocka, HostUnitTestPkg)<br/>Fast, isolated"]
end
E2E --- Integration
Integration --- Unit
style Unit fill:#90EE90
style Integration fill:#FFD700
style E2E fill:#FF6347
Core Principles
- Test at the lowest level possible: Prefer unit tests over integration tests, and integration tests over end-to-end tests
- Run tests on the host: Avoid requiring target hardware for the majority of tests
- Automate everything: Every test should run in CI without manual intervention
- Test in isolation: Unit tests should mock external dependencies to test logic independently
- Fail fast: Catch bugs at build time, not during firmware bring-up on hardware
Host-Based Unit Testing
Overview
Host-based unit testing compiles firmware C code for the host machine (x86-64 Linux or Windows) rather than for the firmware target. This allows tests to run directly on the developer’s machine or in CI, without QEMU or hardware.
Project Mu uses the cmocka testing framework, integrated through UnitTestFrameworkPkg (formerly HostUnitTestPkg).
How It Works
flowchart LR
A[Firmware C Source] --> B[Compiled for Host<br/>x86-64 Linux/Windows]
C[Test Source<br/>cmocka tests] --> B
D[Mock Libraries] --> B
B --> E[Host Executable]
E --> F[Run Tests]
F --> G[JUnit XML Report]
The key insight is that most firmware logic – parsing, validation, state machines, calculations – is pure C code that does not depend on actual hardware. By providing mock implementations of hardware-dependent functions, we can test this logic on any machine.
Setting Up a Unit Test Module
Directory Structure
MyPkg/
Library/
MyLib/
MyLib.c # Production code
MyLib.inf # Production INF
UnitTest/
MyLibUnitTest.c # Test code
MyLibUnitTest.inf # Test INF
MyLibHostUnitTest.inf # Host-based test INF
Test INF File
[Defines]
INF_VERSION = 0x00010017
BASE_NAME = MyLibHostUnitTest
FILE_GUID = 11223344-5566-7788-99AA-BBCCDDEEFF00
MODULE_TYPE = HOST_APPLICATION
VERSION_STRING = 1.0
[Sources]
MyLibUnitTest.c
../MyLib.c
[Packages]
MdePkg/MdePkg.dec
UnitTestFrameworkPkg/UnitTestFrameworkPkg.dec
[LibraryClasses]
UnitTestLib
DebugLib
BaseLib
BaseMemoryLib
The critical setting is MODULE_TYPE = HOST_APPLICATION, which tells the build system to compile for the host rather than for UEFI.
Writing Unit Tests
Basic Test Structure
#include <stdio.h>
#include <Uefi.h>
#include <Library/UnitTestLib.h>
#include <Library/BaseLib.h>
// Include the code under test
#include "../MyLib.h"
#define UNIT_TEST_NAME "MyLib Unit Tests"
#define UNIT_TEST_VERSION "1.0"
/**
Test that ParseVersion correctly parses a valid version string.
**/
UNIT_TEST_STATUS
EFIAPI
TestParseVersionValid (
IN UNIT_TEST_CONTEXT Context
)
{
UINT32 Major = 0;
UINT32 Minor = 0;
UINT32 Patch = 0;
EFI_STATUS Status = ParseVersion("1.2.3", &Major, &Minor, &Patch);
UT_ASSERT_NOT_EFI_ERROR(Status);
UT_ASSERT_EQUAL(Major, 1);
UT_ASSERT_EQUAL(Minor, 2);
UT_ASSERT_EQUAL(Patch, 3);
return UNIT_TEST_PASSED;
}
/**
Test that ParseVersion rejects NULL input.
**/
UNIT_TEST_STATUS
EFIAPI
TestParseVersionNull (
IN UNIT_TEST_CONTEXT Context
)
{
UINT32 Major, Minor, Patch;
EFI_STATUS Status = ParseVersion(NULL, &Major, &Minor, &Patch);
UT_ASSERT_STATUS_EQUAL(Status, EFI_INVALID_PARAMETER);
return UNIT_TEST_PASSED;
}
/**
Test that ParseVersion handles malformed input.
**/
UNIT_TEST_STATUS
EFIAPI
TestParseVersionMalformed (
IN UNIT_TEST_CONTEXT Context
)
{
UINT32 Major, Minor, Patch;
EFI_STATUS Status = ParseVersion("not.a.version.string", &Major, &Minor, &Patch);
UT_ASSERT_STATUS_EQUAL(Status, EFI_INVALID_PARAMETER);
return UNIT_TEST_PASSED;
}
/**
Test that ParseVersion handles boundary values.
**/
UNIT_TEST_STATUS
EFIAPI
TestParseVersionBoundary (
IN UNIT_TEST_CONTEXT Context
)
{
UINT32 Major, Minor, Patch;
// Maximum values
EFI_STATUS Status = ParseVersion("255.255.65535", &Major, &Minor, &Patch);
UT_ASSERT_NOT_EFI_ERROR(Status);
UT_ASSERT_EQUAL(Major, 255);
UT_ASSERT_EQUAL(Minor, 255);
UT_ASSERT_EQUAL(Patch, 65535);
return UNIT_TEST_PASSED;
}
/**
Entry point for the unit test application.
**/
EFI_STATUS
EFIAPI
MyLibUnitTestEntry (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
EFI_STATUS Status;
UNIT_TEST_FRAMEWORK_HANDLE Framework;
UNIT_TEST_SUITE_HANDLE VersionTests;
// Initialize the test framework
Status = InitUnitTestFramework(
&Framework,
UNIT_TEST_NAME,
gEfiCallerBaseName,
UNIT_TEST_VERSION
);
if (EFI_ERROR(Status)) {
return Status;
}
// Create a test suite
Status = CreateUnitTestSuite(
&VersionTests,
Framework,
"Version Parsing Tests",
"MyLib.VersionParsing",
NULL, // Suite setup
NULL // Suite teardown
);
if (EFI_ERROR(Status)) {
goto EXIT;
}
// Add test cases
AddTestCase(
VersionTests,
"ParseVersion with valid input",
"ParseVersion.Valid",
TestParseVersionValid,
NULL, // Prerequisite
NULL, // Cleanup
NULL // Context
);
AddTestCase(
VersionTests,
"ParseVersion with NULL input",
"ParseVersion.Null",
TestParseVersionNull,
NULL, NULL, NULL
);
AddTestCase(
VersionTests,
"ParseVersion with malformed input",
"ParseVersion.Malformed",
TestParseVersionMalformed,
NULL, NULL, NULL
);
AddTestCase(
VersionTests,
"ParseVersion with boundary values",
"ParseVersion.Boundary",
TestParseVersionBoundary,
NULL, NULL, NULL
);
// Run all tests
Status = RunAllTestSuites(Framework);
EXIT:
if (Framework != NULL) {
FreeUnitTestFramework(Framework);
}
return Status;
}
Assertion Macros
The UnitTestLib provides a rich set of assertion macros:
| Macro | Description |
|---|---|
UT_ASSERT_TRUE(Expression) |
Assert expression is TRUE |
UT_ASSERT_FALSE(Expression) |
Assert expression is FALSE |
UT_ASSERT_EQUAL(A, B) |
Assert A equals B |
UT_ASSERT_NOT_EQUAL(A, B) |
Assert A does not equal B |
UT_ASSERT_MEM_EQUAL(A, B, Len) |
Assert memory regions are equal |
UT_ASSERT_NOT_EFI_ERROR(Status) |
Assert Status is not an error |
UT_ASSERT_STATUS_EQUAL(Status, Expected) |
Assert Status equals expected |
UT_ASSERT_NOT_NULL(Ptr) |
Assert pointer is not NULL |
UT_EXPECT_ASSERT_FAILURE(Expr, Status) |
Assert that an ASSERT fires |
UT_LOG_INFO(Format, ...) |
Log informational message |
UT_LOG_WARNING(Format, ...) |
Log warning message |
UT_LOG_ERROR(Format, ...) |
Log error message |
Mocking Dependencies
When the code under test calls functions that depend on firmware services, provide mock implementations:
#include <Library/UnitTestLib.h>
// Mock for a function that reads from SPI flash
EFI_STATUS
EFIAPI
MockSpiFlashRead (
IN UINT32 Offset,
IN UINT32 Size,
OUT UINT8 *Buffer
)
{
// Verify parameters using cmocka
check_expected(Offset);
check_expected(Size);
// Return pre-configured test data
UINT8 *MockData = (UINT8 *)mock();
CopyMem(Buffer, MockData, Size);
return (EFI_STATUS)mock();
}
// In the test case:
UNIT_TEST_STATUS
EFIAPI
TestReadConfigFromFlash (
IN UNIT_TEST_CONTEXT Context
)
{
UINT8 ExpectedData[] = {0x01, 0x02, 0x03, 0x04};
CONFIG_DATA Config;
// Set up mock expectations
expect_value(MockSpiFlashRead, Offset, CONFIG_FLASH_OFFSET);
expect_value(MockSpiFlashRead, Size, sizeof(CONFIG_DATA));
will_return(MockSpiFlashRead, ExpectedData);
will_return(MockSpiFlashRead, EFI_SUCCESS);
// Call the function under test
EFI_STATUS Status = ReadConfigFromFlash(&Config);
UT_ASSERT_NOT_EFI_ERROR(Status);
UT_ASSERT_MEM_EQUAL(&Config.RawData, ExpectedData, sizeof(ExpectedData));
return UNIT_TEST_PASSED;
}
Integration Testing with QEMU
Purpose
While unit tests verify individual functions in isolation, integration tests verify that firmware components work together correctly. QEMU provides a virtual platform where complete firmware images can be tested.
QEMU Boot Test
A basic integration test verifies that the firmware boots successfully:
#!/usr/bin/env python3
"""QEMU integration test for firmware boot."""
import subprocess
import sys
import time
def test_firmware_boot():
"""Verify firmware boots to UEFI shell."""
proc = subprocess.Popen(
[
"qemu-system-x86_64",
"-machine", "q35,smm=on",
"-drive", "if=pflash,format=raw,unit=0,file=Build/QemuQ35Pkg/DEBUG_GCC5/FV/QEMUQ35_CODE.fd",
"-drive", "if=pflash,format=raw,unit=1,file=Build/QemuQ35Pkg/DEBUG_GCC5/FV/QEMUQ35_VARS.fd",
"-m", "2048",
"-serial", "pipe:serial_log",
"-display", "none",
"-no-reboot",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
boot_successful = False
timeout = time.time() + 180 # 3-minute timeout
with open("serial_log.out", "r") as log:
while time.time() < timeout:
line = log.readline()
if not line:
time.sleep(0.1)
continue
print(line, end="")
if "Shell>" in line or "UEFI Interactive Shell" in line:
boot_successful = True
break
proc.terminate()
proc.wait(timeout=10)
if boot_successful:
print("\nPASS: Firmware booted to UEFI Shell")
return 0
else:
print("\nFAIL: Firmware did not reach UEFI Shell within timeout")
return 1
if __name__ == "__main__":
sys.exit(test_firmware_boot())
Testing Specific Features
Integration tests can target specific firmware features:
def test_secure_boot_enforcement():
"""Verify Secure Boot rejects unsigned binaries."""
# 1. Build firmware with Secure Boot enabled and keys enrolled
# 2. Create an unsigned test application
# 3. Place it on the ESP
# 4. Boot QEMU and attempt to run the unsigned application
# 5. Verify the application is rejected (security violation)
pass
def test_capsule_update():
"""Verify capsule update processing."""
# 1. Build firmware with FMP support
# 2. Create a signed test capsule
# 3. Place capsule on ESP at EFI/UpdateCapsule/
# 4. Boot QEMU and verify capsule is processed
# 5. Reboot and verify new firmware version
pass
def test_dfci_enrollment():
"""Verify DFCI enrollment via mailbox."""
# 1. Build firmware with DFCI support
# 2. Create a signed DFCI enrollment packet
# 3. Set the enrollment UEFI variable
# 4. Boot QEMU and verify enrollment is processed
# 5. Verify result variable indicates success
pass
Robot Framework for Firmware Testing
What Is Robot Framework?
Robot Framework is an open-source test automation framework that uses a keyword-driven approach. It is particularly well-suited for firmware testing because:
- Test cases are written in plain language, making them accessible to non-developers
- Custom keywords can abstract complex firmware interactions
- Built-in reporting produces detailed HTML reports
- Easy integration with CI systems
Setup
# Install Robot Framework
pip install robotframework
pip install robotframework-seriallibrary
# For QEMU management
pip install robotframework-process
Project Structure
tests/
robot/
resources/
qemu_keywords.robot # QEMU management keywords
uefi_shell_keywords.robot # UEFI shell interaction keywords
common_variables.robot # Shared variables
suites/
boot_tests.robot # Boot verification tests
secure_boot_tests.robot # Secure Boot tests
capsule_tests.robot # Capsule update tests
results/ # Test output directory
QEMU Management Keywords
*** Settings ***
Library Process
Library OperatingSystem
Library String
*** Variables ***
${QEMU_BINARY} qemu-system-x86_64
${FIRMWARE_CODE} ${BUILD_DIR}/FV/QEMUQ35_CODE.fd
${FIRMWARE_VARS} ${BUILD_DIR}/FV/QEMUQ35_VARS.fd
${QEMU_MEMORY} 2048
${BOOT_TIMEOUT} 180
*** Keywords ***
Start QEMU
[Documentation] Start a QEMU instance with the firmware under test
[Arguments] ${extra_args}=
${process}= Start Process
... ${QEMU_BINARY}
... -machine q35,smm\=on
... -drive if\=pflash,format\=raw,unit\=0,file\=${FIRMWARE_CODE}
... -drive if\=pflash,format\=raw,unit\=1,file\=${FIRMWARE_VARS}
... -m ${QEMU_MEMORY}
... -serial stdio
... -display none
... ${extra_args}
... stdout=${CURDIR}/serial.log
... stderr=STDOUT
Set Suite Variable ${QEMU_PROCESS} ${process}
RETURN ${process}
Stop QEMU
[Documentation] Terminate the QEMU instance
Terminate Process ${QEMU_PROCESS}
Wait For Boot
[Documentation] Wait for firmware to reach the UEFI Shell
[Arguments] ${timeout}=${BOOT_TIMEOUT}
${result}= Wait For Pattern In Log
... pattern=Shell>
... timeout=${timeout}
Should Be True ${result} Firmware did not boot within ${timeout}s
Wait For Pattern In Log
[Documentation] Wait for a pattern to appear in the serial log
[Arguments] ${pattern} ${timeout}=60
${deadline}= Evaluate time.time() + ${timeout} time
FOR ${i} IN RANGE 9999
${content}= Get File ${CURDIR}/serial.log
${found}= Run Keyword And Return Status
... Should Contain ${content} ${pattern}
IF ${found} RETURN ${True}
${now}= Evaluate time.time() time
IF ${now} > ${deadline} RETURN ${False}
Sleep 1s
END
RETURN ${False}
Test Suite Example
*** Settings ***
Resource ../resources/qemu_keywords.robot
Suite Setup Start QEMU
Suite Teardown Stop QEMU
*** Test Cases ***
Firmware Boots Successfully
[Documentation] Verify that firmware reaches the UEFI Shell
[Tags] boot smoke
Wait For Boot timeout=180
DXE Drivers Load Without Errors
[Documentation] Verify no DXE driver load failures in serial output
[Tags] boot drivers
Wait For Boot
${log}= Get File ${CURDIR}/serial.log
Should Not Contain ${log} ERROR: Driver failed to load
Should Not Contain ${log} ASSERT
PCI Enumeration Completes
[Documentation] Verify PCI bus enumeration completes
[Tags] boot pci
${found}= Wait For Pattern In Log
... pattern=PciBus: Discovered PCI
... timeout=120
Should Be True ${found} PCI enumeration did not complete
ACPI Tables Published
[Documentation] Verify ACPI tables are installed
[Tags] boot acpi
${found}= Wait For Pattern In Log
... pattern=InstallAcpiTable
... timeout=120
Should Be True ${found} ACPI tables not installed
Timer Interrupt Functional
[Documentation] Verify timer interrupts are firing
[Tags] boot timer
${found}= Wait For Pattern In Log
... pattern=Timer Interrupt
... timeout=120
Should Be True ${found} Timer interrupts not detected
Stuart CI Build Test Integration
stuart_ci_build
stuart_ci_build is Project Mu’s CI-focused build command that automatically discovers and runs host-based unit tests. It is the primary tool for continuous integration.
# Run CI build with tests for a specific package
stuart_ci_build -c .pytool/CISettings.py -p MyPkg
# Run CI build for all packages
stuart_ci_build -c .pytool/CISettings.py
# Run only the host-based unit tests
stuart_ci_build -c .pytool/CISettings.py -p MyPkg --FlashOnly
CI Configuration
The CI settings file configures which tests run and how:
# .pytool/CISettings.py
class CISettings(CISetupSettingsManager, CIBuildSettingsManager):
def GetActiveScopes(self):
return ("cibuild", "host-based-test")
def GetPackages(self):
return [
"MyPkg",
"MyPlatformPkg",
]
def GetPackagesPath(self):
return [
"MU_BASECORE",
"Common/MU_TIANO",
"Common/MU",
]
def GetArchitecturesSupported(self):
return ["IA32", "X64", "AARCH64"]
def GetWorkspaceRoot(self):
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CI Plugin for Unit Tests
Stuart uses a plugin system to discover and run unit tests. The HostUnitTestCompilerPlugin automatically finds INF files with MODULE_TYPE = HOST_APPLICATION and compiles/runs them:
# The plugin discovers test INFs automatically based on MODULE_TYPE
# No additional configuration is needed beyond including the test INF
# in your package's DSC file
# In your package DSC:
[Components.X64]
MyPkg/Library/MyLib/UnitTest/MyLibHostUnitTest.inf
GitHub Actions Integration
# .github/workflows/ci.yml
name: Firmware CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Dependencies
run: |
pip install -r pip-requirements.txt
stuart_setup -c .pytool/CISettings.py
- name: Update Dependencies
run: stuart_update -c .pytool/CISettings.py
- name: Build and Test
run: stuart_ci_build -c .pytool/CISettings.py -a X64
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: Build/**/HOST_APPLICATION/**/TEST_OUTPUT/
- name: Publish Test Report
if: always()
uses: dorny/test-reporter@v1
with:
name: Unit Test Results
path: Build/**/HOST_APPLICATION/**/*.xml
reporter: java-junit
Test Coverage and Reporting
Generating Coverage Reports
For host-based unit tests compiled with GCC, lcov can generate coverage reports:
# Build with coverage instrumentation
export EXTRA_CFLAGS="--coverage -fprofile-arcs -ftest-coverage"
stuart_ci_build -c .pytool/CISettings.py -p MyPkg
# Collect coverage data
lcov --capture --directory Build/ --output-file coverage.info
# Filter to only your package's source files
lcov --extract coverage.info '*/MyPkg/*' --output-file mypackage_coverage.info
# Generate HTML report
genhtml mypackage_coverage.info --output-directory coverage_report/
# View the report
# open coverage_report/index.html
Interpreting Results
Coverage metrics to monitor:
| Metric | Target | Notes |
|---|---|---|
| Line coverage | 80%+ | Percentage of code lines executed by tests |
| Branch coverage | 70%+ | Percentage of conditional branches taken |
| Function coverage | 90%+ | Percentage of functions called |
Focus on coverage of security-critical code paths:
- Input validation functions
- Authentication and authorization logic
- Flash write operations
- SMM handler entry points
- Capsule parsing code
Writing Custom Test Cases
Test Case Design Patterns
Boundary Value Testing
UNIT_TEST_STATUS
EFIAPI
TestBufferBoundaries (
IN UNIT_TEST_CONTEXT Context
)
{
EFI_STATUS Status;
// Empty buffer
Status = ProcessBuffer(NULL, 0);
UT_ASSERT_STATUS_EQUAL(Status, EFI_INVALID_PARAMETER);
// Minimum valid buffer
UINT8 MinBuffer[1] = {0};
Status = ProcessBuffer(MinBuffer, 1);
UT_ASSERT_NOT_EFI_ERROR(Status);
// Maximum valid buffer
UINT8 *MaxBuffer = AllocatePool(MAX_BUFFER_SIZE);
UT_ASSERT_NOT_NULL(MaxBuffer);
Status = ProcessBuffer(MaxBuffer, MAX_BUFFER_SIZE);
UT_ASSERT_NOT_EFI_ERROR(Status);
FreePool(MaxBuffer);
// Over-maximum buffer
Status = ProcessBuffer(MaxBuffer, MAX_BUFFER_SIZE + 1);
UT_ASSERT_STATUS_EQUAL(Status, EFI_BAD_BUFFER_SIZE);
return UNIT_TEST_PASSED;
}
State Machine Testing
UNIT_TEST_STATUS
EFIAPI
TestStateMachineTransitions (
IN UNIT_TEST_CONTEXT Context
)
{
STATE_MACHINE Sm;
// Initialize to IDLE state
InitStateMachine(&Sm);
UT_ASSERT_EQUAL(Sm.CurrentState, STATE_IDLE);
// IDLE -> PROCESSING on START event
HandleEvent(&Sm, EVENT_START);
UT_ASSERT_EQUAL(Sm.CurrentState, STATE_PROCESSING);
// PROCESSING -> COMPLETE on DONE event
HandleEvent(&Sm, EVENT_DONE);
UT_ASSERT_EQUAL(Sm.CurrentState, STATE_COMPLETE);
// COMPLETE -> IDLE on RESET event
HandleEvent(&Sm, EVENT_RESET);
UT_ASSERT_EQUAL(Sm.CurrentState, STATE_IDLE);
// Invalid transition: IDLE should ignore DONE event
HandleEvent(&Sm, EVENT_DONE);
UT_ASSERT_EQUAL(Sm.CurrentState, STATE_IDLE);
return UNIT_TEST_PASSED;
}
Error Injection Testing
UNIT_TEST_STATUS
EFIAPI
TestFlashWriteFailureHandling (
IN UNIT_TEST_CONTEXT Context
)
{
// Configure mock to simulate flash write failure
expect_any(MockSpiFlashWrite, Offset);
expect_any(MockSpiFlashWrite, Size);
expect_any(MockSpiFlashWrite, Buffer);
will_return(MockSpiFlashWrite, EFI_DEVICE_ERROR);
// Call the function under test
EFI_STATUS Status = UpdateFirmwareRegion(
TEST_REGION_OFFSET,
TestImageData,
TestImageSize
);
// Verify error is propagated correctly
UT_ASSERT_STATUS_EQUAL(Status, EFI_DEVICE_ERROR);
// Verify the original data was not corrupted
// (the function should not have partially written)
UINT8 ReadBack[TEST_IMAGE_SIZE];
MockSpiFlashReadOriginal(TEST_REGION_OFFSET, TEST_IMAGE_SIZE, ReadBack);
UT_ASSERT_MEM_EQUAL(ReadBack, OriginalData, TEST_IMAGE_SIZE);
return UNIT_TEST_PASSED;
}
Test-Driven Firmware Development
The TDD Cycle in Firmware
flowchart LR
R[Write Failing<br/>Test] --> G[Write Minimum<br/>Code to Pass]
G --> RF[Refactor]
RF --> R
style R fill:#FF6347,color:#fff
style G fill:#90EE90
style RF fill:#6495ED,color:#fff
TDD Example: Implementing a CRC32 Checker
Step 1: Write the test first
UNIT_TEST_STATUS
EFIAPI
TestCrc32KnownValue (
IN UNIT_TEST_CONTEXT Context
)
{
// "123456789" has a well-known CRC32 value
UINT8 TestData[] = "123456789";
UINT32 Crc;
EFI_STATUS Status = CalculateCrc32(TestData, 9, &Crc);
UT_ASSERT_NOT_EFI_ERROR(Status);
UT_ASSERT_EQUAL(Crc, 0xCBF43926);
return UNIT_TEST_PASSED;
}
UNIT_TEST_STATUS
EFIAPI
TestCrc32EmptyBuffer (
IN UNIT_TEST_CONTEXT Context
)
{
UINT32 Crc;
EFI_STATUS Status = CalculateCrc32(NULL, 0, &Crc);
UT_ASSERT_STATUS_EQUAL(Status, EFI_INVALID_PARAMETER);
return UNIT_TEST_PASSED;
}
Step 2: Run the test – it fails (the function does not exist yet)
Step 3: Implement the minimum code
EFI_STATUS
EFIAPI
CalculateCrc32 (
IN CONST UINT8 *Data,
IN UINTN DataSize,
OUT UINT32 *CrcOut
)
{
if (Data == NULL || DataSize == 0 || CrcOut == NULL) {
return EFI_INVALID_PARAMETER;
}
UINT32 Crc = 0xFFFFFFFF;
for (UINTN i = 0; i < DataSize; i++) {
Crc ^= Data[i];
for (int j = 0; j < 8; j++) {
if (Crc & 1) {
Crc = (Crc >> 1) ^ 0xEDB88320;
} else {
Crc >>= 1;
}
}
}
*CrcOut = Crc ^ 0xFFFFFFFF;
return EFI_SUCCESS;
}
Step 4: Run the test – it passes
Step 5: Refactor (optimize, add table-based lookup, and so on)
Benefits of TDD in Firmware
- Catches design issues early: Writing tests first forces you to think about the API before implementation
- Prevents regressions: Every bug fix comes with a test that prevents the bug from recurring
- Documentation by example: Tests serve as executable documentation showing how functions should be used
- Confidence to refactor: With a comprehensive test suite, you can restructure code knowing that tests will catch breakage
- Faster development: Debugging on the host is orders of magnitude faster than debugging on target hardware
Summary
Testing is not optional in firmware development – it is essential for producing reliable, secure, and maintainable code. Project Mu provides a comprehensive testing framework that enables firmware developers to apply the same testing discipline as any modern software project.
Key takeaways:
- Host-based unit tests with cmocka run fast on developer machines and in CI, without requiring hardware or emulators
- MODULE_TYPE = HOST_APPLICATION tells the build system to compile firmware C code for the host
- Mocking allows testing firmware logic in isolation from hardware dependencies
- QEMU integration tests verify that firmware components work together in a realistic environment
- Robot Framework enables keyword-driven end-to-end testing with detailed HTML reports
- stuart_ci_build automates test discovery and execution in continuous integration
- Test-driven development catches bugs earlier, prevents regressions, and improves code design
Invest in testing infrastructure early in your firmware project. The upfront cost pays for itself many times over in reduced debugging time, fewer field issues, and greater confidence in firmware updates.