googletest-embedded-arm
Author and run GoogleTest 1.17+ for embedded C++ on ARM targets - TEST() / TEST_F() / TEST_P() / TYPED_TEST(), EXPECT_* vs ASSERT_* assertions, fixtures with SetUp() / TearDown(), value-parameterised tests, GoogleMock when paired, cross-compile with arm-none-eabi-g++, run on host or under QEMU via the qemu-system-test-runner skill, --gtest_filter / --gtest_output=xml:results.xml / --gtest_shuffle / --gtest_repeat command-line flags, and XML / JSON output parsing for CI. Use when the unit-under-test is C++ (modern C++17+) and the team wants the de-facto C++ test framework instead of the C-only Unity. For C use unity-test-framework-c; for pure mocks use cmock-reference.
Install with skills.sh (any agent)
npx skills add testland/qa --skill googletest-embedded-armgoogletest-embedded-arm
Overview
GoogleTest is, per the README at github.com/google/googletest (opens in new window), "Google's C++ test framework" - an xUnit-style framework merged with GoogleMock so the two ship together. The 1.17.x branch "requires at least C++17". For embedded ARM use, the framework runs anywhere a hosted C++ standard library is available - on the host, under QEMU, and on Cortex-A Linux targets; for Cortex-M without an OS, a minimal _write / _exit semihosting stub is the bridge.
This skill wraps GoogleTest for embedded C++ work. Composes with:
When to use
How to use
The full single-test path (author to QEMU) is in Worked example below.
Authoring
Minimal test
Per google.github.io/googletest/primer.html (opens in new window):
#include <gtest/gtest.h>
#include "ringbuffer.h"
TEST(RingbufferTest, EmptyOnInit) {
Ringbuffer<int, 8> rb;
EXPECT_TRUE(rb.empty());
EXPECT_EQ(0u, rb.size());
}
TEST(RingbufferTest, PushIncrementsSize) {
Ringbuffer<int, 8> rb;
rb.push(42);
EXPECT_EQ(1u, rb.size());
EXPECT_FALSE(rb.empty());
}TEST(SuiteName, TestName) registers an independent test; the first argument is "the name of the test suite, and the second argument is the test's name within the test suite" (Primer doc).
Fixtures (TEST_F)
When several tests share setup, derive from ::testing::Test:
class RingbufferTest : public ::testing::Test {
protected:
void SetUp() override { rb.clear(); }
void TearDown() override { /* nothing */ }
Ringbuffer<int, 8> rb;
};
TEST_F(RingbufferTest, PopReturnsPushedValue) {
rb.push(7);
int v = 0;
EXPECT_TRUE(rb.pop(&v));
EXPECT_EQ(7, v);
}Per the Primer: "GoogleTest does not reuse the same test fixture for multiple tests" - each TEST_F gets a fresh instance.
Fatal vs non-fatal
| Family | On failure | Use when |
|---|---|---|
EXPECT_* (e.g. EXPECT_EQ) | Records failure; continues | Each assertion is independent; collect all failures |
ASSERT_* (e.g. ASSERT_EQ) | Records failure; aborts current function | Subsequent assertions would crash (e.g. null pointer deref) |
Per the Primer: "Usually EXPECT_* are preferred, as they allow more than one failure to be reported in a test." Use ASSERT_* only when the next line would dereference a possibly-null pointer returned from the previous check.
Value-parameterised (TEST_P), typed (TYPED_TEST), and death tests live in references/authoring-variants.md.
Worked example
One test from host to QEMU, end to end. It uses the RingbufferTest cases from Authoring above.
cmake -S . -B build-host && cmake --build build-host
./build-host/ringbuffer_test
# [==========] Running 2 tests from 1 test suite.
# [ PASSED ] 2 tests.arm-none-eabi-g++ -mcpu=cortex-m4 -mthumb -O0 -g -std=c++17 \
-DGTEST_HAS_PTHREAD=0 --specs=rdimon.specs \
-I ext/googletest/googletest/include \
test/ringbuffer_test.cpp ext/googletest/googletest/src/gtest-all.cc \
-o ringbuffer_test.elf -lrdimon
qemu-system-arm -M mps2-an385 -cpu cortex-m4 -nographic \
-semihosting-config enable=on,target=native \
-kernel ringbuffer_test.elf
echo $? # gtest RUN_ALL_TESTS() return code, propagated via semihosting exitCommand-line flags
The flags every embedded run reaches for (per the Advanced Guide (opens in new window)):
| Flag | Effect |
|---|---|
--gtest_filter=Pattern | :-separated wildcard list; supports *, ?, negative - |
--gtest_output=xml:results.xml / json:results.json | Machine-readable report for CI |
--gtest_shuffle | Random order each run - reveals inter-test dependencies |
--gtest_repeat=N | Repeat N times (-1 = infinite, for flake hunting) |
The full flag set (--gtest_break_on_failure, --gtest_catch_exceptions=0, --gtest_brief, --gtest_list_tests, and more), the XML / JSON output schema, and the CI-gate parsing pattern are in references/cross-compile-and-ci.md.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
ASSERT_EQ then ignore the abort and continue | Subsequent assertions undefined; can crash | Use EXPECT_EQ unless next line dereferences the value |
GTEST_HAS_PTHREAD=1 on bare-metal Cortex-M | Link fails: pthread symbols missing | Always -DGTEST_HAS_PTHREAD=0 on M-profile cross-builds |
| Death tests on bare-metal | No fork(); test hangs or aborts oddly | Skip death tests on M-profile; or use --gtest_filter=-*DeathTest* |
Linking gtest without gtest_main then no own main | "undefined reference to main" | Link gtest_main or write int main(int argc, char **argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); } |
| Test order dependence | --gtest_shuffle reveals; CI breaks intermittently | Each TEST_F must work in any order; reset state in SetUp |
| Optimised coverage build | -O2 collapses branches gcov can't see | -O0 -g for the coverage build per coverage-reference skill |
TEST_P without INSTANTIATE_TEST_SUITE_P | Compiles but never runs | Always pair |
| Mocking everything | Tests measure mocks not behaviour | Mock at the I/O boundary only - see cmock-reference |
Limitations
References
Cited inline. Foundational documents:
GoogleTest advanced authoring for embedded ARM
View source (opens in new window)GoogleTest advanced authoring for embedded ARM
Deep reference for the googletest-embedded-arm SKILL.md. Consult when the basic TEST() / TEST_F() cases in the SKILL need value-parameterised, typed, or death-test coverage.
Value-parameterised tests (TEST_P)
Per google.github.io/googletest/advanced.html (opens in new window):
class WrapTest : public ::testing::TestWithParam<size_t> {};
TEST_P(WrapTest, WrapAtCapacity) {
Ringbuffer<int, 4> rb;
for (size_t i = 0; i < GetParam(); ++i) rb.push(static_cast<int>(i));
EXPECT_EQ(std::min<size_t>(GetParam(), 4), rb.size());
}
INSTANTIATE_TEST_SUITE_P(Boundaries, WrapTest,
::testing::Values(0u, 1u, 3u, 4u, 5u, 100u));INSTANTIATE_TEST_SUITE_P is the modern macro (the older INSTANTIATE_TEST_CASE_P is deprecated per the Advanced Guide). A TEST_P with no INSTANTIATE_TEST_SUITE_P compiles but never runs - always pair them.
Typed tests (TYPED_TEST)
Per the Advanced Guide, typed tests run "m tests over n types" without writing m*n TESTs:
template <typename T> class IntegralWrapTest : public ::testing::Test {};
using IntegralTypes = ::testing::Types<int8_t, int16_t, int32_t, int64_t>;
TYPED_TEST_SUITE(IntegralWrapTest, IntegralTypes);
TYPED_TEST(IntegralWrapTest, ZeroFits) {
Ringbuffer<TypeParam, 8> rb;
rb.push(0);
EXPECT_EQ(1u, rb.size());
}Death tests
For "expected abort" code paths (assertions, fatal exits) per the Advanced Guide:
TEST(BufferDeathTest, NullPushAborts) {
Ringbuffer<int, 8> *rb = nullptr;
EXPECT_DEATH(rb->push(0), "");
}Death tests fork a child process ("in separate child processes" per the Advanced Guide); not all embedded targets support that. On bare-metal Cortex-M there is no fork() - skip death tests entirely, or exclude them with --gtest_filter=-*DeathTest*.
GoogleTest cross-compile, output parsing, and CI for embedded ARM
View source (opens in new window)GoogleTest cross-compile, output parsing, and CI for embedded ARM
Deep reference for the googletest-embedded-arm SKILL.md. Consult when wiring the full toolchain (host CMake, Cortex-M / Cortex-A cross-compile), reading the XML / JSON output schema, or gating CI.
Host build (fast inner loop)
CMake:
include(FetchContent)
FetchContent_Declare(googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.17.0)
FetchContent_MakeAvailable(googletest)
add_executable(ringbuffer_test test/ringbuffer_test.cpp)
target_link_libraries(ringbuffer_test PRIVATE gtest_main)
enable_testing()
add_test(NAME ringbuffer_test COMMAND ringbuffer_test)gtest_main provides a main() that calls testing::InitGoogleTest(&argc, argv) then RUN_ALL_TESTS() - which "returns 0 on success, 1 on failure" and per google.github.io/googletest/primer.html (opens in new window) "You must not ignore the return value of RUN_ALL_TESTS()".
Cortex-M cross-compile (under QEMU)
The standard recipe - uses arm-none-eabi-g++ for the toolchain, --specs=rdimon.specs to pull in the librdimon (opens in new window) semihosting library so stdout reaches QEMU:
arm-none-eabi-g++ -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard \
-O0 -g -std=c++17 \
-DGTEST_HAS_PTHREAD=0 -DGTEST_OS_LINUX=0 -DGTEST_LANG_CXX11=1 \
--specs=rdimon.specs \
-I/path/to/googletest/googletest/include \
test/ringbuffer_test.cpp /path/to/googletest/googletest/src/gtest-all.cc \
-o ringbuffer_test.elf -lrdimon-DGTEST_HAS_PTHREAD=0 is critical - bare-metal targets have no pthreads; the build fails without it. (The flag is documented in GoogleTest's port.h.) For Cortex-A Linux targets, use the standard arm-linux-gnueabihf-g++ and drop the --specs flag.
Build invariants
| Invariant | Why |
|---|---|
-O0 -g for coverage builds | gcov / llvm-cov measure post-optimisation flow; see embedded-coverage-strategy-reference |
-Wno-psabi on ARM | Suppresses noisy ABI warnings on cross-compile |
-fno-exceptions -fno-rtti if MCU build does | Match the production firmware's flags so virtual dispatch matches |
Link with -Wl,--gc-sections + compile with -ffunction-sections -fdata-sections | Keeps the .elf small enough for low-RAM Cortex-M0 simulation |
Command-line flags (full set)
Per the Advanced Guide:
| Flag | Effect |
|---|---|
--gtest_filter=Pattern | "a :-separated list of wildcard patterns" - supports *, ?, and negative - patterns |
--gtest_repeat=N | Repeats all tests N times (use -1 for infinite - useful for flake hunting) |
--gtest_shuffle | Random order each run - "reveal bad dependencies between tests" |
--gtest_output=xml:results.xml / json:results.json | Machine-readable report (the value is "xml:path" or "json:path") |
--gtest_break_on_failure | Drops into debugger on first failure |
--gtest_catch_exceptions=0 | Disables exception handling - lets debugger catch the throw |
--gtest_color=yes|no|auto | Coloured terminal output |
--gtest_brief=1 | Only failures shown |
--gtest_list_tests | List without running |
Parsing results
XML output schema
Per the Advanced Guide, the XML follows a hierarchical structure with <testsuites> / <testsuite> / <testcase> elements, with <failure> nodes nested under failing <testcase> entries:
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="7" failures="1" time="0.012">
<testsuite name="RingbufferTest" tests="2" failures="0" time="0.001">
<testcase name="EmptyOnInit" status="run" time="0.000"/>
<testcase name="PushIncrementsSize" status="run" time="0.001"/>
</testsuite>
<testsuite name="WrapTest" tests="5" failures="1" time="0.011">
<testcase name="WrapAtCapacity/4" status="run" time="0.002">
<failure message="Expected: 5, actual: 4"/>
</testcase>
</testsuite>
</testsuites>JUnit-compatible - feeds straight into GitHub Actions actions/upload-artifact + mikepenz/action-junit-report.
JSON output schema
Per the Advanced Guide: a "Proto3-compatible structure with UnitTest -> TestCase -> TestInfo -> Failure hierarchy, including timestamps and durations". Prefer JSON for custom dashboards; XML for the JUnit ecosystem.
Parsing pattern
./ringbuffer_test --gtest_output=xml:results.xml
xmlstarlet sel -t -v "count(//testcase[failure])" results.xml
# Number of failing testcases - gate on thisCI integration
jobs:
embedded-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install ARM toolchain
run: sudo apt-get install -y gcc-arm-none-eabi qemu-system-arm
- name: Configure (host build)
run: cmake -S . -B build-host -DCMAKE_BUILD_TYPE=Coverage
- name: Build + run on host
run: |
cmake --build build-host
./build-host/ringbuffer_test \
--gtest_output=xml:build-host/host-results.xml \
--gtest_shuffle
- name: Cross-compile + run under QEMU
run: |
arm-none-eabi-g++ -mcpu=cortex-m4 -mthumb -O0 -g -std=c++17 \
-DGTEST_HAS_PTHREAD=0 --specs=rdimon.specs \
-I ext/googletest/googletest/include \
test/ringbuffer_test.cpp ext/googletest/googletest/src/gtest-all.cc \
-o build-arm/ringbuffer_test.elf -lrdimon
qemu-system-arm -M mps2-an385 -cpu cortex-m4 \
-nographic -semihosting-config enable=on,target=native \
-kernel build-arm/ringbuffer_test.elf
- name: Publish JUnit
uses: mikepenz/action-junit-report@v4
with:
report_paths: 'build-host/*-results.xml'The host build gates on the JUnit XML; the QEMU build gates on QEMU's exit code (semihosting exit(RUN_ALL_TESTS()) propagates the gtest return value through QEMU).
Related skills
ceedling-build-runner
Author and run the Ceedling build system for C unit testing - the canonical build orchestration on top of Unity (assertions) + CMock (mocks) + CException (exceptions). Covers ceedling new project scaffolding, the project.yml schema (:project / :paths / :files / :defines / :flags / :tools / :test_runner / :cmock / :unity / :cexception / :gcov / :plugins), the task surface (ceedling test:all, ceedling test:{name}, ceedling test:pattern, ceedling test:path, ceedling release, ceedling clean / clobber, ceedling gcov:all, ceedling module:create, ceedling environment, ceedling dumpconfig), JUnit XML output via the report_tests_pretty_stdout / report_tests_junit_xml plugins, gcov plugin integration, host vs cross-build flow, and CI wiring. Use when a C project wants the standard ThrowTheSwitch trio bundled by one build command. For the Unity assertion API see unity-test-framework-c; for CMock semantics see cmock-reference.
cmock-reference
Pure-reference catalog of CMock and Ceedling mocking semantics for C. Defines what CMock generates from a C header (the full Expect / ExpectAndReturn / ExpectAnyArgs / ExpectWithArray / Ignore / IgnoreAndReturn / IgnoreArg_{param} / ReturnThruPtr_{param} / AddCallback / Stub / ExpectAndThrow naming family), the cmock.yml :plugins list (ignore, ignore_stateless, ignore_arg, expect_any_args, array, callback, cexception, return_thru_ptr) and what each enables, mock-suffix and mock-prefix conventions, how Unity teardown validates expectations, the resetTest mid-test verification, strict vs ignore argument-matching modes, and the trade-offs between mock / stub / spy / fake. Use as the CMock semantics reference when authoring Ceedling tests with mocks or when reading an unfamiliar mock-driven test suite.
embedded-coverage-strategy-reference
Pure-reference catalog of code-coverage strategy for embedded C/C++: the criteria hierarchy (statement / branch / decision / condition / MC/DC), the gcov toolchain (.gcno/.gcda, --coverage), the LLVM source-based toolchain (llvm-profdata / llvm-cov), host-build vs QEMU-build instrumentation, MISRA-C:2012 and DO-178C structural-coverage expectations by safety level (DAL A maps to MC/DC), and report-format choices. Use when choosing what structural-coverage level to require and wiring gcov / llvm-cov into the build; physical .gcda retrieval from hardware is in hardware-in-loop-reference, QEMU machine flags in qemu-system-test-runner, and to author the embedded tests themselves use googletest-embedded-arm or unity-test-framework-c.
hardware-in-loop-reference
Pure-reference catalog of hardware-in-the-loop (HIL) testing for embedded systems. Defines the HIL pattern (ECU-under-test + real-time plant simulator + I/O cards emulating sensors / actuators / buses), the V-cycle progression (MIL → SIL → PIL → HIL), the canonical vendor stack (NI VeriStand + PXI / CompactRIO, dSPACE SCALEXIO / MicroAutoBox, Vector CANoe + VT System, Speedgoat real-time targets), bus emulation per protocol (CAN / CAN FD / LIN / FlexRay / Automotive Ethernet / SOME-IP), fault-injection patterns (short-to-ground, open-circuit, signal corruption), DO-178C / ISO 26262 / IEC 61508 alignment, and the test-evidence chain HIL produces. Use as the HIL terminology + vendor + standard reference when scoping an embedded test rig or interpreting an automotive / aerospace / industrial HIL test report.
qemu-system-test-runner
Author and run QEMU system emulation as an embedded-test target - qemu-system-arm / qemu-system-aarch64 / qemu-system-riscv32 launching cross-compiled ELF binaries on virtual MCUs and SoCs. Covers machine selection (-M virt / mps2-an385 / mps2-an386 / mps2-an500 / mps2-an511 / mps3-an524 / lm3s6965evb / raspi3b / xilinx-zynq-a9), CPU selection (-cpu cortex-m0 / cortex-m3 / cortex-m4 / cortex-m33 / cortex-a15 / cortex-a57 / max), -kernel ELF load, -nographic + -serial stdio, ARM semihosting via -semihosting-config enable=on,target=native (so cross-compiled GoogleTest / Unity binaries print to host stdio and exit with the test return code), GDB stub via -S -gdb tcp::1234, QMP monitor via -qmp tcp:host:port for automated test orchestration, and CI wiring. Use when host-only test runs are insufficient and the team wants arch-correct (endianness / alignment / interrupt-vector) behaviour on a virtual MCU without committing to physical hardware-in-loop.
unity-test-framework-c
Author and run ThrowTheSwitch Unity (the C unit-testing library) for bare-metal and RTOS C code. Distinct from the Unity game-engine Test Framework at docs.unity3d.com: this is the ThrowTheSwitch C testing library at throwtheswitch.org/unity, a single C file plus headers that runs on 8-bit MCUs through 64-bit hosts. Anchored on the Unity assertion API and configuration macros regardless of execution environment: the TEST_ASSERT_EQUAL_* / _FLOAT / _DOUBLE / _STRING / _MEMORY / _BITS assertion families, setUp/tearDown/RUN_TEST/UNITY_BEGIN/UNITY_END semantics and the exit-code contract, the generate_test_runner.rb generator, build-time config defines (UNITY_INCLUDE_DOUBLE, UNITY_OUTPUT_CHAR, UNITY_EXCLUDE_SETJMP), and CI integration via Ceedling JUnit XML; applies to host builds, cross-builds, and QEMU-run targets alike. For QEMU machine flags, semihosting, and exit-code capture, see qemu-system-test-runner. Use when the unit-under-test is pure C and the target ranges from 8-bit AVR to Cortex-M0 to Linux ARM.