Skip to content

[中文版本](/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
Makefile

Your Makefile might build both:

all:
    gcc -g -Wall -I../src ../src/math.c test_math.c -o test_math

Step 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:11

Step 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: 0

Step 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_TEST

Then compile with:

gcc test.c -lcheck -o test

Step 9. Automating Tests with Makefile ​

Add a test target:

test:
    gcc -Wall -g src/*.c tests/*.c -o tests/run_tests
    ./tests/run_tests

Now you can just run:

make test

and 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 ​

  1. Write a test suite for your dynamic array implementation.
  2. AddASSERT_EQ_FLOAT andASSERT_EQ_STR macros.
  3. Automate tests usingmake test.
  4. Add afails counter and colorize your results.
  5. 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.

本站总访问 — 次