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, Boost Test, Catch2 and GoogleTest stand out in terms of quality and features.
#include <cstdint>
// 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.cpp
After experiencing performance problems, we want to use a faster algorithm
#include <cstdint>
// 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.cpp
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 <cstdint>
#include <limits>
#include "catch_amalgamated.hpp"
uint64_t add_to(uint32_t n); // usually in header file
TEST_CASE("sum_to must calculate correctly", "[sum_to]") {
// setup code that runs before each section is written here
SECTION("regular use cases") {
CHECK(add_to(1) == 1); // if CHECK fails, TEST will fail, but continue
CHECK(add_to(2) == 3);
REQUIRE(add_to(25) == 325); // if REQUIRE fails, TEST fails immediately
CHECK(add_to(26) == 351);
CHECK(add_to(500000000) == 125000000250000000);
}
SECTION("corner cases") {
REQUIRE(add_to(0) == 0);
}
SECTION("slow cases") {
CHECK(add_to(4294967293) == 9223372026117357571);
CHECK(add_to(4294967294) == 9223372030412324865);
}
}
TEST_CASE("sum_to must terminate for all valid input data",
"[sum_to_edge]") {
// does not terminate for the old code version due to an overflow of the
// loop counter (the loop would terminate only at UINT_MAX + 1)
REQUIRE(add_to(std::numeric_limits<uint32_t>::max())
== 9223372034707292160);
}
Download sum_to_test.cpp
clang++ --std=c++17 before_refactoring.cpp \
sum_to_test.cpp catch_amalgamated.cpp -o run_tests
./run_tests # does not terminate; interrupt via [Ctrl]+[C]
What to do?
./run_tests --help
./run_tests --list-tests # list all available tests
./run_tests [sum_to] # or ./run_tests "sum_to must calculate correctly"
./run_tests [sum_to] --section "corner cases"
For the sake of the presentation, instead of applying the change, we use another source file.
Download after_refactoring.cpp
clang++ --std=c++17 after_refactoring.cpp \
sum_to_test.cpp catch_amalgamated.cpp -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 "old" way of using Catch2 requires no installation at all
Include a single header, compile a single C++ file and link that file
clang++ -c --std=c++17 catch_amalgamated.cpp # compile Catch2 once
clang++ -c --std=c++17 source_file.cpp
clang++ -c --std=c++17 source_file_test.cpp
# link amalgamated version of Catch2
clang++ -o your_test catch_amalgamated.o source_file.o source_file_test.o
This obviously works with any build system.
Since version 3 of Catch2, the library consists of many different files
Only inlcude what you need and use cmake
to compile your
tests
// #include "your_header.hpp"
#include <catch2/catch_test_macros.hpp>
TEST_CASE("trivial 'testcase' with v3", "[tc-tag]") {
REQUIRE(1 == 1); // use assertions here and in SECTIONs
}
Download v3_test.cpp
cmake
automatically downloads Catch2 to your
project when needed
cmake_minimum_required(VERSION 3.18)
cmake_policy(SET CMP0048 NEW)
project(sample_test VERSION 1.0)
set(CMAKE_CXX_STANDARD 20)
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.0.1 # or a later release
)
FetchContent_MakeAvailable(Catch2)
add_executable(test_v3 test_v3.cpp)
target_link_libraries(test_v3 PRIVATE Catch2::Catch2WithMain)
cmake
can be configured to make tests runnable via
make test
or ctest
cmake_minimum_required(VERSION 3.18)
cmake_policy(SET CMP0048 NEW)
project(sample_test VERSION 1.0)
set(CMAKE_CXX_STANDARD 20)
include(CTest)
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.0.1 # or a later release
)
FetchContent_MakeAvailable(Catch2)
add_executable(test_v3 test_v3.cpp)
target_link_libraries(test_v3 PRIVATE Catch2::Catch2WithMain)
add_test(NAME TestV3 COMMAND test_v3)
Download CMakeLists.txt
Comprehensive documentation can be found in Catch2's reference documentation.
The provided examples are helpful when getting started.
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.