Here's the video walkthrough, if you'd rather watch it:

Why I Started Using This Tool

After the last episode, my C project could configure, build and run with a single command. I was pretty happy with myself. Then I made a "small cleanup" to my add function, ran the demo, glanced at the output, and moved on. Two days later I spotted a wrong number in that output and had no idea which change caused it.

The lesson was obvious once it hurt: running a program and looking at it isn't testing. My eyes only catch what they happen to notice. I wanted something that checks my functions automatically, tells me exactly what's wrong, and runs with the same one command I already use. That's how I ended up with Unity and CTest.

What It Does

There are three pieces, each with one job.

1. Unity, the test framework

Unity, by ThrowTheSwitch, is a tiny unit-testing library written in plain C, popular in embedded projects. My test file tests/test_arithmetic.c has two tests for add:

TEST_ASSERT_EQUAL_INT_MESSAGE(5, add(2, 3),
    "Verifies that adding 2 and 3 correctly results in 5.");

Expected value first, actual value second, and a message that only prints if the test fails. Unity also calls setUp() before and tearDown() after every test, so each test can start from a clean state. My main runs the tests with RUN_TEST and ends with return UNITY_END();, which returns the number of failures:

int main(void)
{
    UNITY_BEGIN();
    RUN_TEST(test_add_two_plus_three);
    RUN_TEST(test_add_negative_cancels_positive);
    return UNITY_END();
}

That return value turned out to be the most important line in the file.

2. CMake and CTest, the plumbing

In CMakeLists.txt, FetchContent downloads Unity from GitHub at a pinned version during configure, so I don't copy any third-party code into my repo:

include(FetchContent)
FetchContent_Declare(unity
    GIT_REPOSITORY https://github.com/ThrowTheSwitch/Unity.git
    GIT_TAG v2.6.0
)
FetchContent_MakeAvailable(unity)

Then:

  • enable_testing() turns on CTest,
  • add_executable(test_arithmetic tests/test_arithmetic.c src/arithmetic.c) builds the test program from the test file plus the code under test (not main.c, since the test has its own main),
  • target_link_libraries(test_arithmetic PRIVATE unity) links Unity and brings in its header path automatically,
  • add_test(NAME test_arithmetic COMMAND test_arithmetic) tells CTest the program exists.

CTest runs the program and checks its exit code. Zero means passed, anything else means failed. That's why return UNITY_END(); matters: it's the bridge between Unity's count and CTest's verdict.

3. Presets, the one command

In CMakePresets.json I added:

  • a tests build preset that builds only the test program,
  • a default test preset with outputOnFailure (quiet when green, full Unity report when red),
  • a verbose test preset that always shows output,
  • a test workflow that chains configure, build and test.
"testPresets": [
  { "name": "default", "configurePreset": "default", "output": { "outputOnFailure": true } },
  { "name": "verbose", "inherits": "default", "output": { "verbosity": "verbose" } }
]

Now one command does everything:

cmake --workflow --preset test
1/1 Test #1: test_arithmetic ..................   Passed    0.01 sec

100% tests passed, 0 tests failed out of 1

To prove it worked, I broke add on purpose by adding + 1. The run immediately printed the file, line number, test name, expected vs. actual, and my message:

tests/test_arithmetic.c:19:test_add_two_plus_three:FAIL: Expected 5 Was 6. Verifies that adding 2 and 3 correctly results in 5.
tests/test_arithmetic.c:25:test_add_negative_cancels_positive:FAIL: Expected 0 Was 1. Verifies that adding -4 and 4 correctly results in 0.
...
2 Tests 2 Failures 0 Ignored
FAIL

The bug that took me two days to notice took one second.

Final Verdict

If you write C with CMake, add Unity and CTest early. It's a few lines of CMake, one small test file, and a couple of presets, and you get an automatic safety net on every build. Unity is simple enough that I understood every macro I used on day one, and FetchContent means there's nothing to install by hand.

A few honest caveats. The first configure needs internet and Git to fetch Unity. CTest counts test programs, not test functions, so "1 test passed" can hide several Unity tests, and the verbose preset (cmake --workflow --preset test-verbose) is how you see them. And my setup is still a starting point: only add is tested, and arithmetic.c is compiled into two programs instead of being a proper library. Both of those are next on my list.

My biggest takeaway: never trust a test you haven't seen fail. Break your code on purpose once, watch it go red, then fix it and watch it go green. After that, you'll actually believe the green.