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

-fsanitize=undefined

signed overflow, NULL deref, OOB shifts, alignment.

-fsanitize=thread

data races (mutually exclusive with ASan).

-fsanitize=memory

reads of uninitialised memory (Clang only).

-fsanitize=leak

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

References#