Search code examples
cunit-testingcmakeidedevelopment-environment

Set Up Development Environment for Unit Testing C


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.


Solution

  • 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.