Switch Theme

Writing Unit Tests in C

dev c

4/11/2025

thumbnail

12 min. read

Introduction

We all love C, right? You can basically write anything that you want. But that comes at the cost of you trying to debug everything. So, I’ll explain my process of writing unit tests in C, utilizing the CTest Framework with CMake.

The code used in this example is borrowed from the cmdfx library.

Prerequisites

Let’s assume your file tree looks something like this:

|CMakeLists.txt
|README.md
|LICENSE
|test/
├── CMakeLists.txt
├── src/
│   ├── test1.c
│   ├── test2.c
│   ├── test3_main.c
│   ├── test3_others.c
│   └── test.h
|include/
└── library.h

In your root CMakeLists.txt, get started with this code:

option(TESTING "Build tests for ${PROJECT_NAME}" ON)
if (TESTING)
    enable_testing()
    add_subdirectory(test)
endif()

This will allow you to start configuring tests when you run cmake .. in the test directory.

Now, in your test/CMakeLists.txt, you can add the following code:

cmake_minimum_required(VERSION 3.16)

include(CTest)

# Tests
function(add_test_executable name)
    set(TEST_NAME "test-${name}")

    # Create the test executable
    add_executable("${TEST_NAME}" "src/${name}.c" "src/test.h")
    target_include_directories("${TEST_NAME}" PRIVATE "${PROJECT_SOURCE_DIR}/include")

    # Link the library to the test executable
    add_dependencies("${TEST_NAME}" "${PROJECT_NAME}")

    # Set the test executable name
    string(REPLACE "_" "/" TEST_EXECUTABLE_NAME ${name}) # Replace underscores with slashes in name
    add_test(NAME ${TEST_EXECUTABLE_NAME} COMMAND "${TEST_NAME}") # Add test command

    # Set the test executable properties if needed
    if (CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
        # Add coverage flags for GCC/Clang
        target_compile_options("${TEST_NAME}" PRIVATE --coverage -lm -w) # Link with math library and suppress warnings
        target_link_options("${TEST_NAME}" PRIVATE --coverage)
    endif()

    # Set the output directory for the test executable
    set_target_properties(${TEST_NAME} PROPERTIES
        RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/tests/
    )
endfunction()

# Find Test Files
file(GLOB TESTS "src/*.c")

foreach(TEST_FILE ${TESTS})
    get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE)

    # Call the preceding function to add the test executable
    add_test_executable(${TEST_NAME})
endforeach()

This code will create a function that you can call to add test executables. You can also add the --coverage flag to your tests if you want to.

Writing Tests

test.h

test.h is a header file that contains assertion functions that can be used to check the results of your tests. It also contains a function to run all tests.

Here’s a modified example of test.h that includes the assertion functions. Now, I usually use a as my actual value instead of my expected value, so feel free to modify this to fit what you usually like to code with:

#ifndef PROJECT_TEST_H
#define PROJECT_TEST_H

// Define the test result strings
#define PASS "PASS"
#define FAIL "FAIL"

#include <stdio.h>
#include <string.h>
#include <math.h>

// Initialize the test counter
static int count = 1;

int assertTrue(int condition) {
    printf("#%d: %s\n", count, condition ? PASS : FAIL);
    if (!condition) {
        printf("#%d: had true, wanted false\n", count);
        count++;
        return 1;
    }

    count++;
    return 0;
}

int assertFalse(int condition) {
    printf("#%d: %s\n", count, !condition ? PASS : FAIL);
    if (condition) {
        printf("#%d: had false, wanted true\n", count);
        count++;
        return 1;
    }

    count++;
    return 0;
}

int assertNull(void* ptr) {
    printf("#%d: %s\n", count, ptr == 0 ? PASS : FAIL);
    if (ptr != 0) {
        printf("#%d: why is <%p> not null\n", count, ptr);
        count++;
        return 1;
    }

    count++;
    return 0;
}

int assertNotNull(void* ptr) {
    printf("#%d: %s\n", count, ptr != 0 ? PASS : FAIL);
    if (ptr == 0) {
        printf("#%d: this pointer shouldn't be null bro\n", count);
        count++;
        return 1;
    }

    count++;
    return 0;
}

int assertEquals(int a, int b) {
    printf("#%d: %s\n", count, a == b ? PASS : FAIL);
    if (a != b) {
        printf("#%d: had <%d>, wanted <%d>\n", count, a, b);
        count++;
        return 1;
    }

    count++;
    return 0;
}

int assertFloatEquals(float a, float b) {
    // use fabs for float comparison because of precision issues
    printf("#%d: %s\n", count, fabs(a - b) < 0.0001f ? PASS : FAIL);
    if (fabs(a - b) >= 0.0001f) {
        printf("#%d: had <%f>, wanted <%f>\n", count, a, b);
        count++;
        return 1;
    }

    count++;
    return 0;
}

int assertDoubleEquals(double a, double b) {
    // use fabs for double comparison because of precision issues
    printf("#%d: %s\n", count, fabs(a - b) < 0.0001 ? PASS : FAIL);
    if (fabs(a - b) >= 0.0001) {
        printf("#%d: had <%f>, wanted <%f>\n", count, a, b);
        count++;
        return 1;
    }

    count++;
    return 0;
}

int assertNotEquals(int a, int b) {
    printf("#%d: %s\n", count, a != b ? PASS : FAIL);
    if (a == b) {
        printf("#%d: had <%d>, didn't want <%d>\n", count, a, b);
        count++;
        return 1;
    }

    count++;
    return 0;
}

int assertGreaterThan(int a, int b) {
    printf("#%d: %s\n", count, a > b ? PASS : FAIL);
    if (a <= b) {
        printf("#%d: wanted <%d> to be greater than <%d>\n", count, a, b);
        count++;
        return 1;
    }

    count++;
    return 0;
}

int assertLessThan(int a, int b) {
    printf("#%d: %s\n", count, a < b ? PASS : FAIL);
    if (a >= b) {
        printf("#%d: wanted <%d> to be less than <%d>\n", count, a, b);
        count++;
        return 1;
    }

    count++;
    return 0;
}

int assertIn(double value, double min, double max) {
    printf("#%d: %s\n", count, value >= min && value <= max ? PASS : FAIL);
    if (value < min || value > max) {
        printf("#%d: expected <%f> to be in range <%f> to <%f>\n", count, value, min, max);
        count++;
        return 1;
    }

    count++;
    return 0;
}

int assertPointersMatch(void* a, void* b) {
    printf("#%d: %s\n", count, a == b ? PASS : FAIL);
    if (a != b) {
        printf("#%d: expected pointers <%p> and <%p> to match\n", count, a, b);
        count++;
        return 1;
    }

    count++;
    return 0;
}

int assertStringsMatch(char* a, char* b) {
    if (a == 0 || b == 0) {
        printf("#%d: %s\n", count, a == b ? PASS : FAIL);
        if (a != b) {
            printf("#%d: expected strings <%s> and <%s> to match\n", count, a, b);
            count++;
            return 1;
        }
    }

    printf("#%d: %s\n", count, strcmp(a, b) == 0 ? PASS : FAIL);
    if (strcmp(a, b) != 0) {
        printf("#%d: expected strings <%s> and <%s> to match\n", count, a, b);
        count++;
        return 1;
    }

    count++;
    return 0;
}

#endif

Other Test Files

For this example, I’m going to modify an example from the CmdFX testing suite. Now, you can create your test files.

#include <stdio.h>

#include "cmdfx/core/sprites.h"
#include "test.h"

int main() {
    int r = 0;

    CmdFX_Sprite* sprite = Sprite_createFilled(5, 5, '&', 0, 0);
    Sprite_draw(4, 4, sprite);

    r |= assertEquals(sprite->x, 4);
    r |= assertEquals(sprite->y, 4);

    Sprite_moveTo(sprite, 2, 2);

    r |= assertEquals(sprite->x, 2);
    r |= assertEquals(sprite->y, 2);

    Sprite_moveBy(sprite, 1, 1);

    r |= assertEquals(sprite->x, 3);
    r |= assertEquals(sprite->y, 3);

    return r;
}

This test file creates a sprite and moves it around. It uses the assertion functions to check if the sprite’s position is correct after each move. You can create as many test files as you want, and they will all be compiled into separate executables. You can also use the assert functions in any of your test files.

Running Tests

To run the tests, you can use the following command:

ctest --output-on-failure

This will run all the tests and show the output for any failed tests. You can also use the --coverage flag to generate a coverage report.

Conclusion

Writing unit tests in C can be a bit tricky, but with the right tools and techniques, it can be done easily. The CTest framework with CMake makes it easy to create and run tests, and the assertion functions in test.h make it easy to check the results of your tests. I hope this guide helps you get started with writing unit tests in C!