0

I'm new to unit testing and have mostly programmed using IDEs, therefore I haven't created and/or modified makefiles before.

Now that I'm exploring Unit Testing and TDD in general; I'm not sure how to set up the development environment so that my unit tests automatically run on every build.

Please help. A general procedure to achieve this would do wonders.

I have not tried anything yet as I'm not very familiar with modifying C Make files.

mrs15
  • 15
  • 5
  • Yes, see, [Test-Driven Development in C](https://eradman.com/posts/tdd-in-c.html) and [C programming and TDD](https://stackoverflow.com/q/2574139/2472827). – Neil Nov 27 '22 at 02:46
  • So, you want an example Makefile + some unit tests ? I can provide some. – AR7CORE Nov 27 '22 at 09:07
  • Is this actually related to CMake or are you just referring to makefiles used for building C code and added a wrong tag to the question? Usually you exclude tests from the normal build btw: Tests can slow down build times and if a build server is running the test suite during nightly builds anyways, you may want the option of skipping them. Adding 2 separate targets for building & running all tests would be the way to go. In cmake you'd probably make use of ctest btw... – fabian Nov 27 '22 at 09:52

1 Answers1

0

Here is a simple Makefile I use for small TDD projects using criterion:

CC=gcc
RELEASE_CFLAGS=-ansi -pedantic -Wall -Werror -Wextra
TESTS_CFLAGS=-pedantic -Wall -Werror -Wextra
TESTS_LDFLAGS=-lcriterion

RELEASE_SRC=$(shell find src/ -type f -name '*.c')
RELEASE_OBJ=$(subst src/,obj/,$(RELEASE_SRC:.c=.o))

TESTS_SRC=$(shell find tests/src/ -type f -name '*.c')
TESTS_OBJ=$(subst src/,obj/,$(TESTS_SRC:.c=.o))
TESTS_BIN=$(subst src/,bin/,$(TESTS_SRC:.c=))

default: run-tests

obj/%.o: src/%.c
    $(CC) $(RELEASE_CFLAGS) -c $^ -o $@

tests/obj/%.o: tests/src/%.c
    $(CC) $(TESTS_CFLAGS) -c $^ -o $@

tests/bin/%: tests/obj/%.o $(RELEASE_OBJ)
    $(CC) $(TESTS_LDFLAGS) $^ -o $@

# prevent deleting object in rules chain
$(TESTS_BIN): $(RELEASE_OBJ) $(TESTS_OBJ)

run-tests: $(TESTS_BIN)
    ./$^ || true

clean:
    rm -f $(RELEASE_OBJ) $(TESTS_OBJ)

clean-all: clean
    rm -f $(TESTS_BIN)

It compiles production code with C89 (-ansi) and tests code with unspecified standard. Files in src/ are moved to obj/, same thing for tests/src/ and tests/obj/. Tests binaries (AKA test suites) depends on every source files and are included in each test binary, making them bigger but it's not a problem for small projects. If binaries size is an issue, you'll have to specify which object to include for each binary.

Directory structure is made with this command:

mkdir -p src obj tests/{src,obj,bin}

An exemple test file:

#include <criterion/criterion.h>
#include "../../src/fibo.h"

Test(fibonacci, first_term_is_0)
{
    // given
    int term_to_compute = 0;

    // when
    int result = fibonacci(term_to_compute);

    // then
    cr_assert_eq(result, 0);
}

Test(fibonacci, second_term_is_1)
{
    // given
    int term_to_compute = 1;

    // when
    int result = fibonacci(term_to_compute);

    // then
    cr_assert_eq(result, 1);
}

And the associated production code:

#include "fibo.h"

unsigned long fibonacci(unsigned int term_to_compute)
{
    return term_to_compute;
}

As you can see, production code is quite dumb and it needs more tests, because it only meets specified requirements (unit tests).

EDIT: Check the Make documentation to learn more about syntax, builtin functions, etc. If you want to learn more about TDD, YouTube has a lot to offer (live codings, explanations, TDD katas): check Robert C Martin (Uncle Bob), Continuous Delivery channel, etc.

PS: returning a long is not the best option here, you could want fixed size integers to have same result on different platforms, but the question was about "how-to TDD". If you're new to TDD, writing the given/when/then might help. Write tests first, and think about edge cases (like, specify overflow ?). I use similar setup when doing NASM TDD and testing with C.

AR7CORE
  • 268
  • 1
  • 8
  • Thank you so much! Can I also have a separate makefile for the tests directory and cd into it theough command line and make all the tests? This way (I'm assuming) I won't have to modify the release/production makefile? And what will be the process if I want to use Google Tests or Unity? – mrs15 Nov 27 '22 at 16:54
  • You can split your your rules into several Makefiles, it might help if your project is quite big, but not really useful otherwise, it's more handy to have all in one place for small projects, you can add a `release` recipe, run it with `make release` (or change the default rule), you can all also call another Makefile with `cd tests && make run`, this calls the Makefile in the `tests/` directory. To use another testing library, change the `TESTS_LDFLAGS` variable to link another library, and change included header in `tests/src/` files. – AR7CORE Nov 27 '22 at 22:23
  • You can do pretty much everything you want, the way depends of what you need. I use `run-tests` as default rule since I run it way more times than a `release-binary` one, and just typing `make` is faster, the default rule is called if you didn't specify any (just `make`). You can have any number of rules, if Makefile becomes too complex, split it, but start simple, and adapt when you need it. – AR7CORE Nov 27 '22 at 22:28