Testland
Browse all skills & agents

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.

Install with skills.sh (any agent)

npx skills add testland/qa --skill unity-test-framework-c
View source

unity-test-framework-c

Overview

This skill covers ThrowTheSwitch Unity - a C unit-testing library ("a single C file and a pair of headers") at throwtheswitch.org/unity (opens in new window) and github.com/ThrowTheSwitch/Unity (opens in new window), running on 8-bit MCUs through 64-bit hosts with no preprocessor magic, no auto-generated main, and no C++ requirement.

It is distinct from the Unity game-engine Test Framework (com.unity.test-framework, a runner inside the Unity 3D editor at docs.unity3d.com (opens in new window), covered by unity-test-framework). The two share only a name.

Composes with:

  • ceedling-build-runner - the build orchestration that calls generate_test_runner.rb and stitches Unity + CMock + the test binary.
  • cmock-reference - the CMock-generated mocks Unity asserts against.
  • qemu-system-test-runner - for running the cross-built binary on a virtual Cortex-M.
  • embedded-coverage-strategy-reference - for the gcov / llvm-cov instrumentation.

When to use

  • Unit-under-test is pure C (not C++). For C++ use googletest-embedded-arm.
  • Target may be tiny - Unity works on 8-bit AVR / PIC, 16-bit MSP430, and 32-bit Cortex-M0/M3/M4/M7/M33.
  • The team already has Ceedling, or wants the canonical ThrowTheSwitch-stack pairing of Unity + CMock + CException.
  • The MCU has limited RAM - Unity's footprint is dramatically smaller than GoogleTest's.

Authoring

Minimal test

#include "unity.h"
#include "ringbuffer.h"

void setUp(void) {
    /* runs before each test */
}

void tearDown(void) {
    /* runs after each test */
}

void test_ringbuffer_initially_empty(void) {
    ringbuffer_t rb;
    ringbuffer_init(&rb);
    TEST_ASSERT_TRUE(ringbuffer_is_empty(&rb));
    TEST_ASSERT_EQUAL_size_t(0, ringbuffer_size(&rb));
}

void test_ringbuffer_push_increments_size(void) {
    ringbuffer_t rb;
    ringbuffer_init(&rb);
    TEST_ASSERT_EQUAL_INT(0, ringbuffer_push(&rb, 42));
    TEST_ASSERT_EQUAL_size_t(1, ringbuffer_size(&rb));
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_ringbuffer_initially_empty);
    RUN_TEST(test_ringbuffer_push_increments_size);
    return UNITY_END();
}

A Unity test is a C function taking no arguments, named with a test or spec prefix. UNITY_BEGIN() initialises counters; RUN_TEST(name) invokes setUp -> test fn -> tearDown and captures the result; UNITY_END() prints the summary and returns a non-zero exit code on failure.

Assertions

The common assertions - TEST_ASSERT_TRUE(c), TEST_ASSERT_EQUAL_INT(a,b) (with _INT8 / _INT16 / _INT32 / _INT64 and _UINT / _HEX widths), TEST_ASSERT_EQUAL_FLOAT(a,b) / TEST_ASSERT_FLOAT_WITHIN(delta,a,b), TEST_ASSERT_EQUAL_STRING(a,b), TEST_ASSERT_EQUAL_MEMORY(a,b,len), TEST_ASSERT_NULL(p). Append _ARRAY to compare arrays (third arg is element count) and _MESSAGE to attach a custom failure string.

The full assertion-family table and the build-time config defines are in references/assertion-api.md.

Skipping and failing explicitly

void test_only_when_calibrated(void) {
    if (!device_calibrated()) {
        TEST_IGNORE_MESSAGE("device not calibrated; skip");
    }
    /* normal asserts */
}

TEST_IGNORE() records an "ignored" result, distinct from pass/fail in the summary.

generate_test_runner.rb

The Ruby script auto/generate_test_runner.rb scans a test file, finds every test_* and spec_* function, and emits a <file>_Runner.c that wires them up:

ruby /path/to/Unity/auto/generate_test_runner.rb \
    test/test_ringbuffer.c test/test_ringbuffer_Runner.c

Then compile test_ringbuffer.c + test_ringbuffer_Runner.c + unity.c and link. Ceedling does this implicitly - see ceedling-build-runner.

Building

Host build (fast inner loop)

gcc -Wall -O0 -g -DUNITY_INCLUDE_DOUBLE \
    -I src -I ext/Unity/src \
    src/ringbuffer.c \
    ext/Unity/src/unity.c \
    test/test_ringbuffer.c test/test_ringbuffer_Runner.c \
    -o test_ringbuffer
./test_ringbuffer

-DUNITY_INCLUDE_DOUBLE enables the _DOUBLE macros (off by default to save flash on tiny MCUs - per the Unity config docs (opens in new window)).

Cortex-M0 cross-build (under QEMU)

arm-none-eabi-gcc -mcpu=cortex-m0 -mthumb -O0 -g \
    -DUNITY_OUTPUT_COLOR \
    --specs=rdimon.specs \
    -I src -I ext/Unity/src \
    src/ringbuffer.c ext/Unity/src/unity.c \
    test/test_ringbuffer.c test/test_ringbuffer_Runner.c \
    -o test_ringbuffer.elf -lrdimon
qemu-system-arm -M mps2-an385 -cpu cortex-m0 \
    -nographic -semihosting-config enable=on,target=native \
    -kernel test_ringbuffer.elf

--specs=rdimon.specs provides the ARM semihosting library (developer.arm.com GNU Toolchain (opens in new window)), so Unity's printf-based reporting reaches QEMU's stdio.

Build-time configuration

Key defines (UNITY_INCLUDE_DOUBLE, UNITY_OUTPUT_CHAR, UNITY_OUTPUT_COLOR, UNITY_FIXTURE_NO_EXTRAS, UNITY_EXCLUDE_SETJMP) are tabulated in references/assertion-api.md, per the Unity configuration guide (opens in new window).

Running

Console output

Unity prints one line per result + a summary:

test/test_ringbuffer.c:34:test_ringbuffer_initially_empty:PASS
test/test_ringbuffer.c:42:test_ringbuffer_push_increments_size:PASS

-----------------------
2 Tests 0 Failures 0 Ignored
OK

The format is <file>:<line>:<test_name>:<PASS|FAIL|IGNORE>. Failures include the assertion details:

test/test_ringbuffer.c:48:test_overflow_returns_minus_one:FAIL: Expected -1 Was 0

Exit code

UNITY_END() returns the failure count (int). Use it from main:

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_x);
    return UNITY_END();   /* 0 on all-pass; non-zero on any failure */
}

CI tools that gate on exit code (CMake CTest, GitHub Actions run: step) work natively.

Parsing results

Console parsing

Unity's text output is intentionally simple - a grep -c ':FAIL:' is enough for a smoke gate:

./test_ringbuffer | tee results.txt
fails=$(grep -c ':FAIL:' results.txt || true)
[ "$fails" -eq 0 ] || exit 1

JUnit XML via Ceedling

Ceedling wraps Unity and emits a JUnit XML report at build/artifacts/test/report.xml - see ceedling-build-runner. The schema matches GoogleTest's, so the same JUnit pipeline works for both.

Custom output

Override UNITY_OUTPUT_CHAR(c) at build time:

/* unity_config.h */
#define UNITY_OUTPUT_CHAR(c)    serial_putc(c)
#define UNITY_OUTPUT_FLUSH()    serial_flush()

On bare-metal, send results over UART → host serial → CI log file.

CI integration

The standalone (no Ceedling) GitHub Actions pipeline - generate runners, build + run on host, then cross-build and run under QEMU - is in references/ci-integration.md. For Ceedling-driven projects, see ceedling-build-runner for the canonical ceedling test:all + JUnit XML flow.

Anti-patterns

Anti-patternWhy it failsFix
Hand-maintaining the test runnerDrift: a new test_* function is silently skippedRun generate_test_runner.rb in the build; or use Ceedling
TEST_ASSERT_EQUAL_INT on size_tWidth mismatch warns on 64-bit hosts, may overflow on 8-bitUse TEST_ASSERT_EQUAL_size_t or width-specific _UINT32
TEST_ASSERT_TRUE(strcmp(a,b) == 0)Failure message reports "Expected true, got false" - uselessUse TEST_ASSERT_EQUAL_STRING(a,b)
TEST_ASSERT_EQUAL_MEMORY with len=sizeof(*p) on a struct with paddingPadding bytes vary; intermittent failuresInitialise structs with memset(.., 0, sizeof) before fill, or compare fields individually
Calling RUN_TEST outside UNITY_BEGIN/UNITY_ENDAsserts work but the summary is wrongAlways bracket runs with UNITY_BEGIN / UNITY_END
Mixing the C library with the game-engine Test FrameworkBuild sees two unity.h headers; one wins randomlyDon't co-locate; keep unity-test-framework to game-engine projects
Float comparison with TEST_ASSERT_EQUAL_FLOAT and exact valuesFloating-point equality is fragileUse TEST_ASSERT_FLOAT_WITHIN(epsilon, a, b)
UNITY_EXCLUDE_SETJMP on a target that has setjmpLoses early-abort on fatal assert; tests run on after corruptionOnly exclude when the toolchain genuinely lacks setjmp

Limitations

  • No native parameterised tests. Unity has no TEST_P equivalent. Loop with a fixture struct and call TEST_ASSERT_* inside; report the iteration via _MESSAGE suffix.
  • No native test discovery. Without generate_test_runner.rb (or Ceedling), each test must be manually listed in main.
  • Single-threaded. Unity's failure-recovery uses setjmp/ longjmp; concurrent tests in the same process collide. RTOS tests should run tests serially on the test thread.
  • No GoogleMock-style matchers. Mocks live in CMock; matcher expressivity is per cmock-reference.
  • TEST_ASSERT_EQUAL_FLOAT precision is configurable but global. UNITY_FLOAT_PRECISION applies to every float compare in the suite; per-test precision needs _WITHIN.
  • 8-bit targets benefit from UNITY_FIXTURE_NO_EXTRAS. Default builds include features (per-test color, fixture hooks) that bloat tiny MCUs.

References

Cited inline. Foundational documents:

Unity assertion API and build-time config

View source (opens in new window)

Unity assertion API and build-time config

Assertion families

Per the Unity README:

FamilyMacros
Basic validityTEST_ASSERT_TRUE(c), TEST_ASSERT_FALSE(c), TEST_ASSERT(c), TEST_FAIL(), TEST_IGNORE()
Equality, integerTEST_ASSERT_EQUAL_INT(a,b), with width variants _INT8 / _INT16 / _INT32 / _INT64; _UINT family analogous
Equality, hexTEST_ASSERT_EQUAL_HEX(a,b), with _HEX8 / _HEX16 / _HEX32 / _HEX64 width variants
Equality, floatTEST_ASSERT_EQUAL_FLOAT(a,b), TEST_ASSERT_EQUAL_DOUBLE(a,b), plus delta variants TEST_ASSERT_FLOAT_WITHIN(delta,a,b)
String / memoryTEST_ASSERT_EQUAL_STRING(a,b), TEST_ASSERT_EQUAL_MEMORY(a,b,len)
PointerTEST_ASSERT_NULL(p), TEST_ASSERT_NOT_NULL(p)
RangeTEST_ASSERT_WITHIN(delta,a,b), TEST_ASSERT_GREATER_THAN(a,b), TEST_ASSERT_LESS_THAN(a,b)
BitwiseTEST_ASSERT_BITS(mask,expected,actual), TEST_ASSERT_BIT_HIGH(n,x), TEST_ASSERT_BIT_LOW(n,x)
Array"Append _ARRAY or _EACH_EQUAL to most macros" per the README - e.g. TEST_ASSERT_EQUAL_INT_ARRAY(expected, actual, num)
Message variant"All assertions support _MESSAGE variants" - e.g. TEST_ASSERT_EQUAL_INT_MESSAGE(a,b,"frame count") adds a custom failure string

The _MESSAGE suffix attaches context - failures print the message inline. The _ARRAY suffix turns any equality macro into an array-comparing one (third arg is element count).

Build-time configuration

Per the Unity Configuration Guide (opens in new window), key defines:

DefineEffect
UNITY_INCLUDE_DOUBLEEnables _DOUBLE assertions
UNITY_FLOAT_PRECISIONDefault delta for _WITHIN float comparison
UNITY_OUTPUT_CHAR(c)Redirect output (default: putchar) - set to a UART putc for bare-metal
UNITY_OUTPUT_COLORANSI colour codes in output
UNITY_FIXTURE_NO_EXTRASSlim build for very small MCUs
UNITY_EXCLUDE_SETJMPIf toolchain has no setjmp - Unity falls back to a longjmp-free mode but loses the early-abort-on-fatal-assert behaviour

Unity CI integration (standalone, no Ceedling)

View source (opens in new window)

Unity CI integration (standalone, no Ceedling)

Generate runners, build + run on host, then cross-build and run under QEMU:

jobs:
  unity-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - name: Install toolchain
        run: sudo apt-get install -y gcc-arm-none-eabi qemu-system-arm ruby
      - name: Generate runners
        run: |
          for t in test/test_*.c; do
            ruby ext/Unity/auto/generate_test_runner.rb "$t" "${t%.c}_Runner.c"
          done
      - name: Build + run on host
        run: |
          gcc -O0 -g -DUNITY_INCLUDE_DOUBLE \
              -I src -I ext/Unity/src \
              src/*.c ext/Unity/src/unity.c \
              test/test_*.c test/*_Runner.c \
              -o build/unity_host
          ./build/unity_host | tee build/host.log
          ! grep -q ':FAIL:' build/host.log
      - name: Cross-build + QEMU run
        run: |
          arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -O0 -g \
              --specs=rdimon.specs \
              -I src -I ext/Unity/src \
              src/*.c ext/Unity/src/unity.c \
              test/test_*.c test/*_Runner.c \
              -o build/unity_arm.elf -lrdimon
          qemu-system-arm -M mps2-an385 -cpu cortex-m4 \
              -nographic -semihosting-config enable=on,target=native \
              -kernel build/unity_arm.elf | tee build/arm.log
          ! grep -q ':FAIL:' build/arm.log

For Ceedling-driven projects, use ceedling-build-runner for the canonical ceedling test:all + JUnit XML flow.

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.

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.

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.