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.

unity-test-framework-c

Overview

Disambiguation up front: this skill covers ThrowTheSwitch Unity - a C unit-testing library at throwtheswitch.org/unity and github.com/ThrowTheSwitch/Unity. It is distinct from the Unity game-engine Test Framework at docs.unity3d.com (a managed-test runner inside the Unity 3D editor). The two tools share only a name; they have unrelated origins, APIs, and consumers. If you reached this skill looking for the Unity-the-game-engine test framework, the canonical package is com.unity.test-framework documented at docs.unity3d.com and covered by qa-game/unity-test-framework.

Unity is, per throwtheswitch.org/unity, "a Unit-Testing framework written in 100% pure ANSI C" that is "carefully written and self-tested to be portable, working efficiently on tiny 8-bit microcontrollers to 64-bit powerhouses". Per the README at github.com/ThrowTheSwitch/Unity, it is "a single C file and a pair of headers, allowing it to be added to your existing build setup". The framework lets the developer "test in C without littering your source code with additional requirements" - there is no preprocessor magic, no auto-generated main, no C++ requirement.

Composes with:

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. Per the throwtheswitch page above, it runs "efficiently on tiny 8-bit microcontrollers".
  • 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

Per the Unity README:

#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();
}

Per the README: a Unity test is "just a C function that takes no arguments and returns nothing. By convention, it starts with the word 'test' or 'spec'." 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.

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 is the canonical way to attach context - failures print the message inline. The _ARRAY suffix turns any equality macro into an array-comparing one (third arg is element count).

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

Per the same README, 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).

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), so Unity's printf-based reporting reaches QEMU's stdio.

Build-time configuration

Per the Unity configuration guide (linked above), 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

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

Standalone (no Ceedling):

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, 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. Use qa-game/unity-test-framework only for game engine projects; this skill for embedded C
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 ceedling-mocks-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: