Testing#
C has no standard test runner; pick one. The
2026 leaders are Unity
(embedded / single-file friendly), cmocka (mocks first-class), and Criterion (modern, parametric).
For fuzzing reach for AFL++ and
libFuzzer; for memory bugs, AddressSanitizer
(-fsanitize=address) and Valgrind.
For
assert in production code, see Errors.
Unity#
Drop-in single-file test framework. The operator vendors
unity.c / unity.h into the project and writes specs as
plain C.
#include "unity.h"
#include "strutil.h"
void setUp(void) {}
void tearDown(void) {}
void test_trim_removes_trailing(void) {
char buf[] = "hi ";
trim(buf);
TEST_ASSERT_EQUAL_STRING("hi", buf);
}
void test_trim_leaves_clean(void) {
char buf[] = "hi";
trim(buf);
TEST_ASSERT_EQUAL_STRING("hi", buf);
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_trim_removes_trailing);
RUN_TEST(test_trim_leaves_clean);
return UNITY_END();
}
Build and run.
$ cc -o test_strutil test_strutil.c strutil.c unity.c
$ ./test_strutil
Useful assertions: TEST_ASSERT_EQUAL_INT,
TEST_ASSERT_EQUAL_PTR, TEST_ASSERT_NULL,
TEST_ASSERT_NOT_NULL, TEST_ASSERT_EQUAL_MEMORY,
TEST_ASSERT_TRUE, TEST_ASSERT_FALSE.
cmocka#
Heavier; first-class mocks and setup / teardown groups.
$ apt install libcmocka-dev
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
static void test_trim(void **state) {
char buf[] = " hi ";
trim(buf);
assert_string_equal(buf, "hi");
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_trim),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
$ cc -o test test.c strutil.c -lcmocka
Mocking is the differentiator; wrap a function at link time and asserts on its calls.
__wrap_malloc; /* declared via -Wl,--wrap=malloc */
/* … */
will_return(__wrap_malloc, NULL); /* force the next call to fail */
Criterion#
Modern runner; declarative tests, parametric tests, parallel execution, theory-style tests.
#include <criterion/criterion.h>
Test(strutil, trim) {
char buf[] = " hi ";
trim(buf);
cr_assert_str_eq(buf, "hi");
}
Test(strutil, len) {
cr_assert_eq(strlen("hi"), 2);
}
$ cc -o tests tests.c -lcriterion
$ ./tests
Table-driven#
Go-to structure for any function with a small input space. Works in every framework.
typedef struct {
const char *name;
const char *in;
const char *want;
} trim_case_t;
static const trim_case_t cases[] = {
{"clean", "hi", "hi"},
{"trailing", "hi ", "hi"},
{"leading", " hi", "hi"},
{"both", " hi ", "hi"},
{"empty", "", ""},
{"whitespace"," ", ""},
};
void test_trim_table(void) {
for (size_t i = 0; i < sizeof cases / sizeof cases[0]; i++) {
char buf[64];
strncpy(buf, cases[i].in, sizeof buf - 1);
buf[sizeof buf - 1] = '\0';
trim(buf);
TEST_ASSERT_EQUAL_STRING_MESSAGE(cases[i].want, buf, cases[i].name);
}
}
AddressSanitizer#
-fsanitize=address instruments allocations to detect
use-after-free, double-free, heap / stack overflows, leaks.
$ cc -O1 -g -fsanitize=address -o test test.c
$ ./test
The runtime aborts on any violation with a stack trace and a diagnosis. ASan is essential for C development; every test run in CI should be ASan-instrumented.
Companion sanitizers.
Flag |
Catches |
|---|---|
|
signed overflow, NULL deref, OOB shifts, alignment. |
|
data races (mutually exclusive with ASan). |
|
reads of uninitialised memory (Clang only). |
|
memory leaks (part of ASan). |
Valgrind#
Slower than ASan, no recompile needed. Use it when the binary is opaque (third-party blob, already built) and ASan is not an option.
$ valgrind --leak-check=full --show-leak-kinds=all ./scan
Fuzzing#
A fuzzer feeds the program randomised input looking for crashes.
libFuzzer (LLVM, in-process).
#include <stddef.h>
#include <stdint.h>
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
parse((const char*)data, size); /* must not crash */
return 0;
}
$ clang -fsanitize=fuzzer,address -o fuzz fuzz.c parse.c
$ ./fuzz -max_total_time=60
AFL++ (out-of-process, persistent mode supported).
$ afl-cc -o app main.c
$ afl-fuzz -i corpus -o out -- ./app
Benchmarks#
C has no standard benchmark harness; write a
small driver. hyperfine benchmarks the whole binary;
perf stat reports CPU counters.
$ hyperfine --warmup 3 './scan target'
$ perf stat ./scan target
Coverage#
GCC’s -fprofile-arcs -ftest-coverage (or Clang’s
-fprofile-instr-generate -fcoverage-mapping) instruments
the binary; gcov / lcov summarise.
$ cc --coverage -O0 -g -o test test.c
$ ./test
$ gcov test.c