Appearance
[中文版本](/posts/c教程/zh-CN/08-调试测试与 profiling/074-Unit Testing in C)
Testing isn’t just something you do at the end, it’s how you build confidence in every line of code. Unit testing means checking small, isolated pieces (functions, modules) automatically, so you can change your code without fear.
C doesn’t come with a built-in testing framework, but it’s easy to build lightweight ones, and several excellent libraries exist if you want more power.
Let’s walk through how to design and run unit tests in plain C.
Step 1. What Is Unit Testing?
A unit test verifies a single behavior:
Given an input, does this function produce the correct output?
For example:
int add(int a, int b) { return a + b; }
void test_add(void) {
if (add(2, 3) != 5) printf("test_add failed!\n");
else printf("test_add passed!\n");
}Run this test:
int main(void) {
test_add();
}Output:
test_add passed!Simple, but powerful.
Step 2. Organizing Tests
Keep tests separate from production code. A typical layout:
src/
math.c
math.h
tests/
test_math.c
MakefileYour Makefile might build both:
all:
gcc -g -Wall -I../src ../src/math.c test_math.c -o test_mathStep 3. Writing Reusable Test Helpers
Define macros to simplify your checks.
#include <stdio.h>
#define ASSERT_EQ_INT(expected, actual) \
if ((expected) != (actual)) \
printf("FAIL: %s:%d: expected %d, got %d\n", __FILE__, __LINE__, (expected), (actual)); \
else \
printf("PASS: %s:%d\n", __FILE__, __LINE__);Now:
int add(int a, int b) { return a + b; }
void test_add(void) {
ASSERT_EQ_INT(5, add(2, 3));
ASSERT_EQ_INT(0, add(-1, 1));
}
int main(void) { test_add(); }Output:
PASS: test_math.c:10
PASS: test_math.c:11Step 4. Testing Multiple Functions
Add more test functions and call them in sequence:
void test_subtract(void) { ASSERT_EQ_INT(1, 3 - 2); }
int main(void) {
test_add();
test_subtract();
}Now your program automatically verifies both functions.
Step 5. Handling Floating-Point Comparisons
Floating-point values rarely match exactly, use a tolerance.
#include <math.h>
#define ASSERT_EQ_FLOAT(expected, actual, eps) \
if (fabs((expected) - (actual)) > (eps)) \
printf("FAIL: expected %.3f, got %.3f\n", (expected), (actual)); \
else \
printf("PASS\n");Step 6. Using Return Codes to Mark Failures
Instead of just printing results, you can make the test binary returnEXIT_FAILURE if any test fails.
int fails = 0;
#define TEST(cond) \
do { if (!(cond)) { \
printf("FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \
fails++; \
} else { \
printf("PASS: %s:%d\n", __FILE__, __LINE__); \
} } while (0)At the end:
return fails ? EXIT_FAILURE : EXIT_SUCCESS;Now your CI or shell can detect test results via$?.
Step 7. Minimal Testing Framework: TinyTest
#include <stdio.h>
#include <stdlib.h>
int tests_run = 0, tests_failed = 0;
#define TEST(name) void name(void)
#define RUN(test) do { \
printf("Running %s... ", #test); \
test(); \
tests_run++; \
printf("OK\n"); \
} while(0)
#define ASSERT_TRUE(cond) if (!(cond)) { \
printf("\n Assertion failed: %s\n", #cond); \
tests_failed++; \
return; \
}
TEST(test_addition) {
int sum = 2 + 3;
ASSERT_TRUE(sum == 5);
}
int main(void) {
RUN(test_addition);
printf("\nTests run: %d, failed: %d\n", tests_run, tests_failed);
return tests_failed ? EXIT_FAILURE : EXIT_SUCCESS;
}Output:
Running test_addition... OK
Tests run: 1, failed: 0Step 8. Using Real Testing Libraries
When your project grows, you can use frameworks like:
- Check (POSIX-compliant)
- Unity (embedded-friendly)
- CMocka
- Criterion
Example with Check:
sudo apt install check#include <check.h>
START_TEST(test_add)
{
ck_assert_int_eq(2 + 3, 5);
}
END_TESTThen compile with:
gcc test.c -lcheck -o testStep 9. Automating Tests with Makefile
Add a test target:
test:
gcc -Wall -g src/*.c tests/*.c -o tests/run_tests
./tests/run_testsNow you can just run:
make testand see your full suite’s results.
Step 10. Tiny Code: Testing a Linked List
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef struct Node {
int value;
struct Node *next;
} Node;
Node *push(Node *head, int val) {
Node *n = malloc(sizeof(Node));
n->value = val;
n->next = head;
return n;
}
void test_push(void) {
Node *head = NULL;
head = push(head, 10);
head = push(head, 20);
assert(head->value == 20);
assert(head->next->value == 10);
printf("test_push passed\n");
}
int main(void) {
test_push();
printf("All tests passed.\n");
}Output:
test_push passed
All tests passed.Why It Matters
Unit testing turns code into verifiable logic:
- Prevents regressions.
- Encourages small, clean functions.
- Makes debugging faster.
- Builds confidence before refactoring.
When you trust your tests, you can rewrite your code fearlessly.
Try It Yourself
- Write a test suite for your dynamic array implementation.
- Add
ASSERT_EQ_FLOATandASSERT_EQ_STRmacros. - Automate tests using
make test. - Add a
failscounter and colorize your results. - Use a testing library like Criterion or Unity and compare styles.
Next, you’ll learn how to add logging systems to your C programs, to record what’s happening under the hood in a controlled, readable way.
