Open-source project
ThrowTheSwitch/Unity avatar
ThrowTheSwitch/Unity

Unity Test for embedded C: bit mask asserts, hand written element counts, and a natural int cast

Simple unit testing for C

5,455 stars1,143 forksCMIT

At a glance

What is it?
Unity Test is a single C file plus two headers from ThrowTheSwitch.org, aimed at microcontroller and firmware builds. This looks at what its assertions actually compare, where the defaults hand you a silent pass, and where the repository stops.
Who is it for?
Use Unity Test when your build already compiles C for a microcontroller and you need assertions that can inspect individual bits. Before committing, read the known issues page in docs/ and pin the version tag you tested against.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 14 days ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • ThrowTheSwitch/Unity5,455 ★ · +65
Star history
Repository2026-09-232026-11-14
ThrowTheSwitch/Unity5.4k5.5k

65 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

Dropping unity.c and unity.h into a build you already have

Unity Test ships its engine as a single C file and a pair of headers, which is the whole reason it fits an existing firmware toolchain without ceremony. Any compiler works, and the project names Make and CMake among the build systems it expects. The repository root shows how many ways that wiring can be approached:

code
.editorconfig
.gitattributes
.github/
.gitignore
CMakeLists.txt
LICENSE.txt
README.md
auto/
docs/
examples/
extras/
library.json
meson.build
meson_options.txt
platformio-build.py
src/
test/
unityConfig.cmake

Five numbered example folders sit under `examples/`, running from `examples/example_1/` through `examples/example_5/`, next to an `examples/unity_config.h` you can copy as a starting point. The root already carries four descriptions of the same build: CMakeLists.txt, meson.build, a platformio-build.py script, and a unityConfig.cmake for CMake consumers.

Your first test is a macro call rather than a configuration step. The basic validity group covers truthiness and forced failure:

c
    TEST_ASSERT_TRUE(condition)
    TEST_ASSERT_FALSE(condition)
    TEST_ASSERT(condition)
    TEST_ASSERT_UNLESS(condition)
    TEST_FAIL()
    TEST_FAIL_MESSAGE(message)

TEST_ASSERT is an alternate spelling of TEST_ASSERT_TRUE, and TEST_ASSERT_UNLESS is the matching alternate for TEST_ASSERT_FALSE. Everything else in the framework follows this shape: a macro that takes an expected value, an actual value, and sometimes a width or a count.

Ceedling is the build tool Unity points you at instead of writing your own

The project offers two routes. The short one is manual: copy the core file and headers into your tree, point your existing build at them, and write the test entry points yourself. The longer one is Ceedling, described as a build tool also by ThrowTheSwitch.org and offered for people who would rather leave the hard work to them.

The details you need for either route are not on the landing page. The docs/ folder contains a getting started guide, and the project also points at a change log and a known issues list in its documentation rather than reproducing them in README.md. The complete assertion list lives in docs/ at UnityAssertionsReference.md.

Two consequences follow. A green local build tells you nothing about whether Ceedling would drive the same suite in continuous integration, because the two setups are separate integrations of the same assertion macros. And if you skip the known issues page, you are trusting a framework whose own maintainers keep a running list of the things that misbehave.

Bit masks compare one register and ignore every bit you did not name

This is the part of Unity Test with no comfortable host language equivalent. When a firmware test cares about a status register and nothing else, plain equality is the wrong call, because unrelated bits are set by hardware. The bitwise group lets you name a mask instead:

c
    TEST_ASSERT_BITS(mask, expected, actual)
    TEST_ASSERT_BITS_HIGH(mask, actual)
    TEST_ASSERT_BITS_LOW(mask, actual)
    TEST_ASSERT_BIT_HIGH(bit, actual)
    TEST_ASSERT_BIT_LOW(bit, actual)

The mask forms compare the bits set high in the mask and ignore the low bits. BITS_HIGH checks that every bit named in the mask is set, and BITS_LOW checks that every bit named in the mask is clear. The single bit forms take a bit number from 0 to 31 for a 32-bit integer.

The consequence is a test that passes while the register is wrong. A mask written one bit short of the hardware spec reads as a passing test, and the failure only surfaces on the silicon revision where that missing bit finally gets set. Take the mask from the device header rather than from a literal in the test file, so a register layout change breaks the build instead of quietly weakening the assertion.

The default integer assert casts to whatever your target calls a natural int

TEST_ASSERT_EQUAL_INT is the macro you will type most, and the first thing to understand is that it casts to your natural integer size. That is convenient on a host build and quiet on a target whose int is narrower than the value under test. Width specific forms exist so you can pin the type, and the hexadecimal group is worth reaching for on hardware, where the failing value is a register dump and the datasheet prints it in hex:

c
    TEST_ASSERT_EQUAL_HEX(expected, actual)
    TEST_ASSERT_EQUAL_HEX8(expected, actual)
    TEST_ASSERT_EQUAL_HEX16(expected, actual)
    TEST_ASSERT_EQUAL_HEX32(expected, actual)
    TEST_ASSERT_EQUAL_HEX64(expected, actual)

Width changes the output, not only the conversion: HEX16 shows 4 nibbles. The unsigned group mirrors the signed one, from TEST_ASSERT_EQUAL_UINT down to TEST_ASSERT_EQUAL_UINT64, and TEST_ASSERT_EQUAL is simply another spelling of TEST_ASSERT_EQUAL_INT. TEST_ASSERT_INT_WITHIN, TEST_ASSERT_GREATER_THAN, and TEST_ASSERT_LESS_THAN each carry delta or threshold arguments and each has size specific variants.

Two failure modes to watch. A truncated compare on a narrow int target passes while the high bits differ, which is the same silent weakening as a short mask. And a width pinned wider than the target type forces the compiler to promote both sides, so the width you write is a claim about the hardware that you have to keep true.

Array comparisons make you supply the element count by hand

Every scalar macro has an array twin, formed by appending _ARRAY, and the call shape changes with it. You pass the expected array, the actual array, and then a third argument saying how many elements to compare:

c
    TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, actual, elements)
    TEST_ASSERT_EACH_EQUAL_INT32(expected, actual, elements)

The first form compares arrays element by element. The second, formed with _EACH_EQUAL, checks that every element equals one expected value, which is the cheap way to assert a cleared buffer without naming each position. Both forms ask you to care about the actual size of the value being checked more than the scalar versions do.

That extra argument is a footgun you carry into every array test. If the count is smaller than the real buffer, the tail is never compared and the test still passes. If the count is larger, you read past the end.

Float equality is a small percent delta rather than a bit for bit compare

Floating point comparisons in Unity Test are deliberately not exact. TEST_ASSERT_EQUAL_FLOAT and TEST_ASSERT_EQUAL_DOUBLE consider two values equal when they fall within a small percentage delta of the expected value, and the NOT_EQUAL float and double forms invert that. The explicit delta forms exist for when you want to state the tolerance yourself:

c
    TEST_ASSERT_FLOAT_WITHIN(delta, expected, actual)
    TEST_ASSERT_DOUBLE_WITHIN(delta, expected, actual)
    TEST_ASSERT_FLOAT_NOT_WITHIN(delta, expected, actual)
    TEST_ASSERT_DOUBLE_NOT_WITHIN(delta, expected, actual)

The relational forms come in pairs as well: TEST_ASSERT_LESS_THAN_FLOAT, TEST_ASSERT_LESS_THAN_DOUBLE, TEST_ASSERT_GREATER_THAN_FLOAT, and TEST_ASSERT_GREATER_THAN_DOUBLE. LESS_OR_EQUAL and GREATER_OR_EQUAL variations exist under the same rules, and that is the part that surprises people. If the two values fall inside the small percentage delta, an ordering assertion can pass while the actual value sits on the wrong side of the threshold. Set the delta to zero and you get exact comparison, at the cost of turning every last bit of noise into a failure.

String compares stop at len, and MESSAGE variants take one more argument

The string group comes in two families, and the difference matters when a buffer is not null terminated. The plain form compares two null terminated strings and fails if any character differs or if the lengths differ:

c
    TEST_ASSERT_EQUAL_STRING(expected, actual)
    TEST_ASSERT_EQUAL_STRING_LEN(expected, actual, len)
    TEST_ASSERT_EQUAL_STRING_MESSAGE(expected, actual, message)

The LEN form compares two strings but stops comparing after len characters, which is what you want when the tail of the buffer is padding you do not control. The MESSAGE form outputs a custom message on failure, and that pattern is not isolated: the same extra argument turns up across the other assertion groups, so a message is a convention rather than a single special case.

Release tags move slower than the master branch, so pin what you tested

The version history is uneven in a way that matters for firmware. v2.6.0 landed on 2024-03-10, the next tag v2.6.1 on 2025-01-01, and v2.7.0 on 2026-07-16. The master branch received a push on 2026-10-31, which is more recent than the newest tag, so the working tree and the latest release are not the same code.

The project is not archived, so there is no signal of abandonment, but the gap between v2.6.0 and v2.6.1 is long enough that you should assume any given tag can sit untouched for more than a year.

The practical move is to vendor the core file and headers into your tree instead of tracking a branch, and to record which tag the copy came from. If you do follow master, the change log in docs/ is the place to read before a release, and the known issues page is the place to read after one fails you.

Editorial conclusion

Use Unity Test when your build already compiles C for a microcontroller and you need assertions that can inspect individual bits. Before committing, read the known issues page in docs/ and pin the version tag you tested against.

Frequently asked questions

Does Unity Test need Ceedling to run a test suite?

No. The core is a single C file and a pair of headers, and you may use any compiler and most existing build systems, including Make and CMake. Ceedling is offered as a build tool also by ThrowTheSwitch.org for people who would rather leave that work to someone else.

How does Unity Test compare a status register without failing on unrelated bits?

TEST_ASSERT_BITS takes an integer mask and compares only the bits set high in it, ignoring low bits. TEST_ASSERT_BITS_HIGH and TEST_ASSERT_BITS_LOW check that the masked bits are all set or all clear, and TEST_ASSERT_BIT_HIGH and TEST_ASSERT_BIT_LOW take a single bit from 0 to 31.

Why can Unity Test integer asserts pass on a narrow microcontroller target?

TEST_ASSERT_EQUAL_INT casts both values to your natural integer size, so anything wider than your target's int is truncated before the comparison. Use a width specific form such as TEST_ASSERT_EQUAL_INT32 or TEST_ASSERT_EQUAL_UINT32 when the width is the point of the test.

What happens if I pass too few elements to a Unity Test array assertion?

Only the elements you name get compared. A count smaller than the real buffer leaves the tail uncompared and the test passes anyway, while a count larger than the buffer reads past the end.

Is Unity Test float equality exact?

No. TEST_ASSERT_EQUAL_FLOAT and TEST_ASSERT_EQUAL_DOUBLE treat two values as equal when they sit within a small percentage delta of the expected value. Use TEST_ASSERT_FLOAT_WITHIN or TEST_ASSERT_DOUBLE_WITHIN to state the delta yourself.

What version of Unity Test should an embedded project pin?

The newest tag is v2.7.0 from 2026-07-16, after v2.6.1 from 2025-01-01 and v2.6.0 from 2024-03-10. The master branch received a push on 2026-10-31, so the branch and the latest tag are not the same code.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. ThrowTheSwitch/Unity on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/throwtheswitch-unity.svg)](https://hysenlabs.com/projects/throwtheswitch-unity)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/throwtheswitch-unity.svg)](https://hysenlabs.com/projects/throwtheswitch-unity)

Was this useful?