Unit testing C code

Jun 6, 2023 by Thibault Debatty | 5246 views

C Secure Software Development

https://cylab.be/blog/271/unit-testing-c-code

Writing unit tests is considered a good development habit for numerous reasons. Indeed, unit tests guarantee that the code works as expected, and they prevent developers from accidentally breaking things. Finally, they allow to see how the program is improving with each new commit, and they can be used as documentation to show how the program should be used by others. In this blog post, we show a simple way to implement unit tests when you are writing C code.

shutterstock_2261903725.jpg

The main idea is to:

  1. write your tests in a separate test file and
  2. add a recipe to your Makefile to build and run the test

Finally, gcov will be used to compute the code coverage of the test.

To illustrate this, we will use a simple implementation of a counter. All the source code can be found on the following GitLab repository: https://gitlab.cylab.be/cylab/examples/c-unit-testing

Example code : a simple counter

So, for this example we will test the implementation of a simple counter. The main point is that the code of the counter is kept separated from the main app source code. So we will have a counter.h with something like:

/*
 * A very simple counter.
 * https://cylab.be/blog/271/unit-testing-c-code
 */

struct Counter;

/**
 * Instantiate a new counter.
 */
struct Counter *counter(void);

/**
 * Increment the value of the counter.
 */
void increment(struct Counter *);

/**
 * Get the current value of the counter.
 */
int value(struct Counter *);

And a counter.c with something similar to:

/**
 * A simple counter.
 * https://cylab.be/blog/271/unit-testing-c-code
 */

#include <stdlib.h>

#include "counter.h"

struct Counter {
    int value;
};

struct Counter * counter(void)
{
    struct Counter* c = malloc(sizeof(struct Counter));
    c->value = 0;

    return c;
}

void increment(struct Counter* c)
{
    c->value++;
}

int value(struct Counter* c)
{
    return c->value;
}

And the main method should go in a dedicated file, like app.c:

/**
 * Example app, that uses a counter.
 * https://cylab.be/blog/271/unit-testing-c-code
 */
#include "counter.h"

int main(int argc, char **argv)
{
    // do someting with the counter...
}

Test file

Now, to test our counter, we will create a dedicated test file test.c:

#include "counter.h"
#include <assert.h>

int main(int argc, char **argv)
{
    struct Counter* c = counter();
    assert(c);

    assert(value(c) == 0);

    increment(c);
    assert(value(c) == 1);
    return 0;
}

As you can see, this file:

  • has its own main method;
  • includes "counter.h" and <assert.h>;
  • uses assert() to check the working of the counter.

Makefile

So to run the test, we have to compile and run test.c, which we will do by adding some lines to the Makefile:

CC=gcc
CFLAGS=-std=c99 -Wall

# additional flags for gcov
TESTFLAGS=-fprofile-arcs -ftest-coverage

test: test.c counter.h counter.c
	# build the test
	$(CC) $(CFLAGS) $(TESTFLAGS) -o test test.c counter.c

	# run the test, which will generate test-counter.gcna and test-counter.gcno
	./test

	# compute how test is covering counter.c
	gcov -c -p test-counter

clean:
	rm -f *.o test *.gcov *.gcda *.gcno

As you can see, to compile the test, we use additional gcc flags profile-arcs and test-coverage. With these flags, when test is executed, it will produce 2 additional files (.gcna and .gcno) that can be used by gcov to compute the code coverage of the test.

We can now run the test with make test:

c-unit-test-gcov.png

Gcov produces a short summary on screen, and also a report called counter.c.gcov in our example, that show how many times each line has been executed…

c-coverage-gcov.png

GitLab

Once you have written your tests, you can add them to your .gitlab-ci.yaml:

test:
    image: gcc:11
    script:
        - make test

This blog post is licensed under CC BY-SA 4.0

This website uses cookies. More information about the use of cookies is available in the cookies policy.
Accept