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 (notmain.c, since the test has its ownmain),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
testsbuild preset that builds only the test program, - a
defaulttest preset withoutputOnFailure(quiet when green, full Unity report when red), - a
verbosetest preset that always shows output, - a
testworkflow 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.