Code without tests (aka. legacy code) is extremely difficult to work with
Unit tests...
Essentially none, but writing unit tests ...
For writing, compiling and running unit tests, a unit testing framework is required.
There are many concurring unit testing frameworks for C.
At the time of this writing, Criterion offers the best features.
#include <stdint.h>
// Return the sum of all values from 1 to n.
uint64_t add_to(uint32_t n) {
uint64_t sum = 0;
for (uint32_t i = 1; i <= n; ++i) {
sum += i;
}
return sum;
}
Download before_refactoring.c
After experiencing performance problems, we want to use a faster algorithm
#include <stdint.h>
// Return the sum of all values from 1 to n.
uint64_t add_to(uint32_t n) {
return (uint64_t) (n / 2.0) * (n + 1);
}
Download after_refactoring.c
How can we apply the change without risking to destroy our application?
Using pencil and paper / brainstorming: define relevant test cases
25 is a possible common case.
Add 26 as an even input. 500000000 is a possible large input.
The edge cases should include 0 and 4294967295.
Ideally, also add 1, 2, 4294967293 and 4294967294.
Expected results for these are 325, 351, 125000000250000000, 0 and 9223372034707292160 as well as 1, 3, 9223372026117357571 and 9223372030412324865.
Note that 4294967295 does not terminate with the current implementation.
#include <stdint.h>
#include <criterion/criterion.h>
#include <criterion/new/assert.h>
uint64_t add_to(uint32_t n); // usually in header file
// `cr_expect` fails the test but continues the remaining assertions
// `cr_assert` leads to a failing test and terminates immediately
Test(sum_to, fast_test) {
cr_expect(eq(u64, add_to(0), 0), "Sum of 0 is 0.");
cr_expect(eq(u64, add_to(1), 1), "Sum of 1 is 1.");
cr_expect(eq(u64, add_to(2), 3), "Sum of 1..2 is 3.");
cr_expect(eq(u64, add_to(25), 325), "Sum of 1..25 is 325.");
cr_expect(eq(u64, add_to(26), 351), "Sum of 1..26 is 351.");
cr_expect(eq(u64, add_to(500000000), 125000000250000000),
"This function must work for large unsigned integers.");
}
Test(sum_to, slow) {
cr_expect(eq(u64, add_to(UINT32_MAX - 1), 9223372030412324865),
"The function must work for `UINT32_MAX - 1`. Hint: double trouble ;)");
cr_expect(eq(u64, add_to(UINT32_MAX - 2), 9223372026117357571),
"add_to(UINT32_MAX - 2). Hint: floating-point arithmetic.");
}
// does not terminate for the original code due to an overflow bug of the
// loop counter (the loop would terminate only at UINT_MAX + 1)
Test(sum_to, edge_test) {
cr_assert(eq(u64, add_to(UINT32_MAX), 9223372034707292160),
"The function must work for the largest unsigned integer.");
}
Download sum_to_test.c
clang before_refactoring.c sum_to_test.c -l criterion -o run_tests
./run_tests # does not terminate; interrupt via [Ctrl]+[C]
What to do now?
./run_tests --help
./run_tests -l # list all available tests
./run_tests --filter sum_to/fast_test
For the sake of the presentation, instead of applying the change, we use another source file.
Download after_refactoring.cclang after_refactoring.c sum_to_test.c -l criterion -o run_tests
./run_tests
Now the tests are fast, but they are failing. Fix the code, then compile and run the tests again.
The comprehensive documentation can be found on readthedocs.
The installation is trivial, if it's available in your package manager.
Otherwise, follow the setup guide for building from source.
A slightly more comprehensive example also demonstrates text fixtures.
TDD is a methodology that emphasizes writing tests before the actual code
Helps ensure that code is tested thoroughly and avoids regressions
Repeat the cycle for each new piece of functionality
Whenever you are tempted to type something into a print
statement or a debugger expression, write it as a test instead.