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-cunity-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:
When to use
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.cThen 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
OKThe 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 0Exit 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 1JUnit 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-pattern | Why it fails | Fix |
|---|---|---|
| Hand-maintaining the test runner | Drift: a new test_* function is silently skipped | Run generate_test_runner.rb in the build; or use Ceedling |
TEST_ASSERT_EQUAL_INT on size_t | Width mismatch warns on 64-bit hosts, may overflow on 8-bit | Use TEST_ASSERT_EQUAL_size_t or width-specific _UINT32 |
TEST_ASSERT_TRUE(strcmp(a,b) == 0) | Failure message reports "Expected true, got false" - useless | Use TEST_ASSERT_EQUAL_STRING(a,b) |
TEST_ASSERT_EQUAL_MEMORY with len=sizeof(*p) on a struct with padding | Padding bytes vary; intermittent failures | Initialise structs with memset(.., 0, sizeof) before fill, or compare fields individually |
Calling RUN_TEST outside UNITY_BEGIN/UNITY_END | Asserts work but the summary is wrong | Always bracket runs with UNITY_BEGIN / UNITY_END |
| Mixing the C library with the game-engine Test Framework | Build sees two unity.h headers; one wins randomly | Don't co-locate; keep unity-test-framework to game-engine projects |
Float comparison with TEST_ASSERT_EQUAL_FLOAT and exact values | Floating-point equality is fragile | Use TEST_ASSERT_FLOAT_WITHIN(epsilon, a, b) |
UNITY_EXCLUDE_SETJMP on a target that has setjmp | Loses early-abort on fatal assert; tests run on after corruption | Only exclude when the toolchain genuinely lacks setjmp |
Limitations
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:
| Family | Macros |
|---|---|
| Basic validity | TEST_ASSERT_TRUE(c), TEST_ASSERT_FALSE(c), TEST_ASSERT(c), TEST_FAIL(), TEST_IGNORE() |
| Equality, integer | TEST_ASSERT_EQUAL_INT(a,b), with width variants _INT8 / _INT16 / _INT32 / _INT64; _UINT family analogous |
| Equality, hex | TEST_ASSERT_EQUAL_HEX(a,b), with _HEX8 / _HEX16 / _HEX32 / _HEX64 width variants |
| Equality, float | TEST_ASSERT_EQUAL_FLOAT(a,b), TEST_ASSERT_EQUAL_DOUBLE(a,b), plus delta variants TEST_ASSERT_FLOAT_WITHIN(delta,a,b) |
| String / memory | TEST_ASSERT_EQUAL_STRING(a,b), TEST_ASSERT_EQUAL_MEMORY(a,b,len) |
| Pointer | TEST_ASSERT_NULL(p), TEST_ASSERT_NOT_NULL(p) |
| Range | TEST_ASSERT_WITHIN(delta,a,b), TEST_ASSERT_GREATER_THAN(a,b), TEST_ASSERT_LESS_THAN(a,b) |
| Bitwise | TEST_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:
| Define | Effect |
|---|---|
UNITY_INCLUDE_DOUBLE | Enables _DOUBLE assertions |
UNITY_FLOAT_PRECISION | Default delta for _WITHIN float comparison |
UNITY_OUTPUT_CHAR(c) | Redirect output (default: putchar) - set to a UART putc for bare-metal |
UNITY_OUTPUT_COLOR | ANSI colour codes in output |
UNITY_FIXTURE_NO_EXTRAS | Slim build for very small MCUs |
UNITY_EXCLUDE_SETJMP | If 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.logFor 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.