Here's the video walkthrough, if you'd rather watch it:

Why I Started Using This Tool

I've been building an embedded C project on Linux for a while now, and I already hooked up GoogleTest with CMake. But I kept noticing that my test setup was getting more attention than my testing mindset. I wanted a tiny, clean playground where the only thing on screen is CMake, CTest, and code that's genuinely easy to test.

So I did what a lot of us do now: I wrote a detailed prompt and asked an AI to generate the code. Five math themes: arithmetic, power and absolute value, sorting, searching, statistics. Pure functions only. No globals, no malloc, no comments, each file under twenty lines. My plan was to skip the boring part and get straight to the testing.

That plan lasted about ten minutes, because once I started reading the code properly, I realized the "boring part" was actually the most educational part.

What It Does

CMake is a build generator. Instead of hand-writing Makefiles, you describe your project in a CMakeLists.txt ("here are my source files, here's my library, here's where the headers live") and CMake creates the right build files for whatever platform you're on. CTest comes bundled with it: you call enable_testing(), register each test program with add_test(), and then a single ctest command runs everything and tells you what passed.

Headers are promises

The library is split into header/source pairs. Each .h file is a promise, a list of function prototypes behind an include guard:

#ifndef ARITHMETIC_H
#define ARITHMETIC_H

int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
double divide(double dividend, double divisor);
int modulo(int dividend, int divisor);

#endif

Each .c file keeps that promise, and includes its own header so the compiler checks that the two match. The tests will do the same thing main.c does: include the header and call the functions, without caring how they work inside.

Pure functions make boring tests

Every function is pure: same input, same output, no side effects. add(2, 3) is always 5. It doesn't print, write files, or touch globals. So a test is just "call it, check the result." That's also why the prompt banned globals and malloc: both create hidden state.

The edge cases I found

The real value came from reviewing the code like a tester. Take divide and modulo:

double divide(double dividend, double divisor)
{
    return divisor == 0.0 ? 0.0 : dividend / divisor;
}

int modulo(int dividend, int divisor)
{
    return divisor == 0 ? 0 : dividend % divisor;
}

And int_power:

int int_power(int base, int exponent)
{
    int result = 1;
    for (int i = 0; i < exponent; i++)
        result *= base;
    return result;
}

Here's what I found:

  • divide and modulo return 0 on divide-by-zero. That's a design choice, not math, and it needs a test that documents it.
  • -7 % 3 is -1 in C, not 2 like in Python.
  • int_power silently returns 1 for negative exponents, because the loop never runs.
  • int_abs(INT_MIN) and big add/multiply calls are undefined behavior, not "wraparound."
  • array_min and array_max read values[0] even when the array is empty, while array_average correctly guards against it.
  • int_clamp(5, 10, 0), with the range backwards, returns 0. Nobody decided whether that's right.
  • statistics.c ended up around 31 lines, breaking the 20-line rule, and the sorting and searching modules weren't generated yet at all.

Here's the guard that array_min and array_max are missing:

return length > 0 ? (double)array_sum(values, length) / length : 0.0;

The (double) cast matters too: without it, 7 / 2 is integer division and gives 3 instead of 3.5. Every one of these is now on my list of test cases.

main.c: a demo, not a test

I also added a 19-line main.c that calls every function in the library and prints the results:

#include <stdio.h>
#include "arithmetic.h"
#include "power_abs.h"
#include "statistics.h"

int main(void)
{
    int values[] = {4, -2, 9, 4, 7};
    int length = sizeof values / sizeof values[0];

    printf("add=%d sub=%d mul=%d div=%.2f mod=%d\n",
           add(7, 3), subtract(7, 3), multiply(7, 3), divide(7, 2), modulo(7, 3));
    printf("pow=%d abs=%d min=%d max=%d clamp=%d\n",
           int_power(2, 10), int_abs(-5), int_min(3, 8), int_max(3, 8), int_clamp(15, 0, 10));
    printf("sum=%d avg=%.2f count(4)=%d min=%d max=%d\n",
           array_sum(values, length), array_average(values, length),
           count_occurrences(values, length, 4), array_min(values, length), array_max(values, length));
    return 0;
}
add=10 sub=4 mul=21 div=3.50 mod=1
pow=1024 abs=5 min=3 max=8 clamp=10
sum=22 avg=4.40 count(4)=2 min=-2 max=9

It's a handy smoke check that everything compiles and links, and it shows the "pure library, messy edges" idea in practice: the library never prints, main does. But it also showed me why a demo isn't enough. It only runs the happy path, and I have to read the output myself to know whether it's right. That's the gap CTest fills.

Where CMake and CTest come in

No tests yet, but here's the shape of the CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(MathUtils C)

set(CMAKE_C_STANDARD 99)

add_library(mathutils
    src/arithmetic.c
    src/power_abs.c
    src/statistics.c
)
target_include_directories(mathutils PUBLIC src)

add_executable(demo src/main.c)
target_link_libraries(demo PRIVATE mathutils)

enable_testing()

main.c stays out of the library list on purpose. If it were in, every test program that links the library would get a second main and fail to link.

Final Verdict

Using AI to generate the starting code was absolutely worth it. It saved me typing and gave me a clean, consistent structure in minutes. But it did not save me from thinking. The spec wasn't fully followed, and several functions have edge cases that nobody explicitly decided on.

My recommendation: if you're learning CMake and CTest, start with exactly this kind of small, pure-function library. It keeps the build system and the testing tools front and center. And if you use AI to write it, treat the output like a pull request from a new teammate: read it, question it, and turn every "hmm, what happens if…" into a test. That's where the real learning happens.