| name | c-testing |
| description | C testing patterns: Unity framework (TEST_ASSERT_*), CMocka mocking with __wrap_ and linker flag, Valgrind leak checking, AddressSanitizer, CMake CTest integration, test organization, and running tests with sanitizers. Use when writing or reviewing C tests. |
C Testing
When to Activate
- Writing C unit tests with Unity or CMocka
- Setting up CMake CTest integration
- Running tests with AddressSanitizer or Valgrind
- Mocking C functions for isolation testing
- Adding test coverage to a C library that currently has no automated tests and needs to be validated before a release
- Replacing a slow Valgrind CI step with a fast AddressSanitizer build that catches the same heap and stack errors
- Isolating a C module that calls a network or filesystem function by wrapping it with CMocka
__wrap_ mocks
- Wiring CTest into a CMake build so
cmake --build build && ctest gives a single pass/fail result in CI
Unity — Minimal Unit Testing
Unity is a single-file C test framework — no dependencies.
#include "unity.h"
#include "queue.h"
void setUp(void) { }
void tearDown(void) { }
void test_queue_push_and_pop(void) {
Queue *q = queue_new(4);
TEST_ASSERT_NOT_NULL(q);
int val = 42;
TEST_ASSERT_EQUAL(ERR_OK, queue_push(q, &val, sizeof(val)));
TEST_ASSERT_EQUAL(1, (int)queue_size(q));
int out = 0;
TEST_ASSERT_EQUAL(ERR_OK, queue_pop(q, &out, sizeof(out)));
TEST_ASSERT_EQUAL_INT(42, out);
TEST_ASSERT_EQUAL(0, (int)queue_size(q));
queue_free(q);
}
void test_queue_overflow(void) {
Queue *q = queue_new(1);
int a = 1, b = 2;
TEST_ASSERT_EQUAL(ERR_OK, queue_push(q, &a, sizeof(a)));
TEST_ASSERT_EQUAL(ERR_OVERFLOW, queue_push(q, &b, sizeof(b)));
queue_free(q);
}
void test_queue_null_arg(void) {
TEST_ASSERT_EQUAL(ERR_INVALID, queue_push(NULL, NULL, 0));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_queue_push_and_pop);
RUN_TEST(test_queue_overflow);
RUN_TEST(test_queue_null_arg);
return UNITY_END();
}
Unity Assertion Reference
TEST_ASSERT_EQUAL(expected, actual)
TEST_ASSERT_EQUAL_INT(expected, actual)
TEST_ASSERT_EQUAL_UINT(expected, actual)
TEST_ASSERT_EQUAL_STRING(expected, actual)
TEST_ASSERT_EQUAL_FLOAT(expected, actual, delta)
TEST_ASSERT_EQUAL_MEMORY(expected, actual, len)
TEST_ASSERT_EQUAL_INT_ARRAY(expected, actual, n)
TEST_ASSERT_NULL(ptr)
TEST_ASSERT_NOT_NULL(ptr)
TEST_ASSERT_TRUE(condition)
TEST_ASSERT_FALSE(condition)
TEST_FAIL_MESSAGE("explicit failure reason")
TEST_IGNORE_MESSAGE("skipped — known flake")
CMocka — Mocking with __wrap_
CMocka enables function mocking via the --wrap linker flag.
#include <stdarg.h>
#include <setjmp.h>
#include <cmocka.h>
#include "http_client.h"
int __wrap_http_get(const char *url, char *out, size_t max) {
check_expected(url);
const char *resp = mock_ptr_type(const char *);
strncpy(out, resp, max - 1);
out[max - 1] = '\0';
return mock_type(int);
}
static void test_fetch_user_success(void **state) {
(void)state;
expect_string(__wrap_http_get, url, "https://api.example.com/users/1");
will_return(__wrap_http_get, "{\"name\":\"Alice\",\"age\":30}");
will_return(__wrap_http_get, 200);
User user;
int rc = fetch_user(1, &user);
assert_int_equal(0, rc);
assert_string_equal("Alice", user.name);
assert_int_equal(30, user.age);
}
static void test_fetch_user_network_error(void **state) {
(void)state;
expect_any(__wrap_http_get, url);
will_return(__wrap_http_get, );
will_return(__wrap_http_get, );
User user;
rc = fetch_user(, &user);
assert_int_equal(ERR_IO, rc);
}
{
{
cmocka_unit_test(test_fetch_user_success),
cmocka_unit_test(test_fetch_user_network_error),
};
cmocka_run_group_tests(tests, , );
}
CMake with --wrap linker flag
add_executable(test_http_client
tests/test_http_client.c
src/http_client.c
)
target_include_directories(test_http_client PRIVATE src)
target_link_libraries(test_http_client cmocka)
target_link_options(test_http_client PRIVATE
-Wl,--wrap,http_get # Linux (ld)
)
# macOS: use -Wl,-interposable,_http_get or compile with -DUNIT_TEST and #ifdef
AddressSanitizer — Fast Compile-Time Detection
Catches: heap-use-after-free, stack-buffer-overflow, use-after-return, use-after-scope, undefined behavior.
CFLAGS_TEST = -fsanitize=address,undefined -g -O1 -fno-omit-frame-pointer
test: build/test_queue
ASAN_OPTIONS=detect_leaks=1 ./build/test_queue
build/test_queue: tests/test_queue.c src/queue.c unity/unity.c
$(CC) $(CFLAGS_TEST) -Isrc -Iunity $^ -o $@
ASan outputs colorized stack traces with file:line references. Exit code is non-zero on any error.
Valgrind — Deep Runtime Analysis
Catches: memory leaks, invalid reads/writes, uninitialized memory, overlapping memcpy.
valgrind \
--leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
--error-exitcode=1 \
./build/test_queue
Key output categories:
definitely lost — classic leak (must fix)
indirectly lost — pointer to leaked memory (fix root)
possibly lost — pointer arithmetic made it look lost (review)
still reachable — freed at exit, not a bug
CMake CTest Integration
cmake_minimum_required(VERSION 3.20)
project(mylib C)
set(CMAKE_C_STANDARD 11)
add_compile_options(-Wall -Wextra -Wpedantic -Werror)
enable_testing()
# Library under test
add_library(mylib src/queue.c src/user.c)
target_include_directories(mylib PUBLIC src)
# Unity (vendored)
add_library(unity unity/unity.c)
target_include_directories(unity PUBLIC unity)
# Test binary — queue
add_executable(test_queue tests/test_queue.c)
target_link_libraries(test_queue mylib unity)
target_compile_options(test_queue PRIVATE
-fsanitize=address,undefined -g)
target_link_options(test_queue PRIVATE
-fsanitize=address,undefined)
add_test(NAME QueueTests COMMAND test_queue)
# Test binary — user
add_executable(test_user tests/test_user.c)
target_link_libraries(test_user mylib unity)
target_compile_options(test_user PRIVATE
-fsanitize=address,undefined -g)
target_link_options(test_user PRIVATE
-fsanitize=address,undefined)
add_test(NAME UserTests COMMAND test_user)
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
ctest --test-dir build -V
ctest --test-dir build --output-on-failure
Project Structure
project/
src/
queue.c
queue.h
user.c
user.h
tests/
test_queue.c
test_user.c
unity/
unity.c
unity.h
unity_internals.h
CMakeLists.txt
Makefile # optional, for simple projects
Strategy: ASan in CI, Valgrind Locally
| Tool | Speed | Detection | Usage |
|---|
| AddressSanitizer | Fast (~2×) | Most heap/stack/UB errors | CI and local |
| Valgrind | Slow (~20×) | Full memory analysis + leaks | Local debugging |
Use ASan by default in CI (fast feedback). Run Valgrind locally when you suspect a leak that ASan misses, or for production pre-release verification.