Zum Schreiben von Programmen gehört auch das Testen, ob sie das tun, was sie tun sollen. Unit-Tests überprüfen Programmbausteine. Funktioniert die geschriebene Funktion oder Bibliothek? Erfüllt sie die Anforderungen? Erfüllt sie diese auch nach einem Umschreiben, bei dem ein besserer Lösungsweg gefunden wurde? Reagiert sie auf falsche Eingaben? Reagiert sie wie vorgeschrieben? Die nur aus einer Headerdatei bestehende C++-Bibliothek doctest ermöglicht es auf einfache Weise, Unit-Tests durchzuführen.
Nehmen wir ein einfaches Programm:
#include <iostream> #include "pythagorean.hpp" int main() { std::cout << is_pythagorean_triple(3,4,5) << "\n"; }
Bei einem pythagoräischen Tripel ist das Quadrat der größten (natürlichen) Zahl gleich der Summe der Quadrate der anderen beiden (natürlichen) Zahlen: 3² + 4² = 5². Der Name kommt vom Satz des Pythagoras, ein Dreieck mit den Seitenlängen 3, 4 und 5 (in willkürlichen Einheiten) ist rechtwinklig.
Das Projekt wird in CMakeLists.txt
für CMake beschrieben:
cmake_minimum_required(VERSION 3.18) project(Example) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) add_executable(programm src/main.cpp) target_include_directories(programm PRIVATE include) target_sources(programm PRIVATE src/pythagorean.cpp)
Wir werden es mehrfach übersetzen und testen müssen. Der Header pythagorean.hpp
#pragma once bool is_pythagorean_triple(int a, int b, int c);
befindet sich im Unterverzeichnis include
, die anderen Quellen im Unterverzeichnis src
. Nach dem Kommandos
mkdir build cd build cmake -G "MinGW Makefiles" .. cmake --build . programm
sollte als Ausgabe 1
(für true
) erscheinen. Fehlt nur noch pythgorean.cpp
…
Halt! Zuvor sollte festgelegt werden, was die fehlende Funktion tun soll. Dafür bereiten wir eine Test-Umgebung vor. Wir erweitern CMakeLists.txt
:
add_executable(test_programm doctest/doctest_main.cpp) target_include_directories(test_programm PRIVATE include doctest/include) target_sources(test_programm PRIVATE src/pythagorean.cpp src/pythagorean.test.cpp)
Das Hauptprogramm doctest_main.cpp
für Tests mit doctest besteht nur aus zwei Zeilen:
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include "doctest.h"
Es schließt nur den von der Webseite zu holenden Header doctest.h
ein. Ein erster Unit-Test wird in pythagorean.test.cpp
erstellt. (Ich bevorzuge hier die Trennung zwischen Testcode und zu testenden Quellen.)
#include "pythagorean.hpp" #include "doctest.h" TEST_CASE("known triples") { CHECK(true == is_pythagorean_triple(3,4,5)); CHECK(true == is_pythagorean_triple(5,4,3)); }
Eine erste Implementierung (wir überspringen die nullte Näherung return false
, weil zu albern)
#include "pythagorean.hpp" bool is_pythagorean_triple(int a, int b, int c) { return a*a + b*b == c*c; }
genügt zwar der ersten Anforderung. Die zweite schlägt beim Aufruf des Testprogramms fehl1). Die Beanstandung wird klar und deutlich ausgegeben:
...>test_programm.exe [doctest] doctest version is "2.4.0" [doctest] run with "--help" for options =============================================================================== ...\pythagorean.test.cpp:4: TEST CASE: known triples ...\pythagorean.test.cpp:7: ERROR: CHECK( true == is_pythagorean_triple(5,4,3) ) is NOT correct! values: CHECK( true == false ) =============================================================================== [doctest] test cases: 1 | 0 passed | 1 failed | 0 skipped [doctest] assertions: 2 | 1 passed | 1 failed | [doctest] Status: FAILURE!
Erst durch Umordnen der übernommenen Werte
#include <algorithm> #include "pythagorean.hpp" bool is_pythagorean_triple(int a, int b, int c) { if (a > c) std::swap(a,c); if (b > c) std::swap(b,c); return a*a + b*b == c*c; }
wird dieser Test bestanden:
[doctest] test cases: 1 | 1 passed | 0 failed | 0 skipped [doctest] assertions: 2 | 2 passed | 0 failed | [doctest] Status: SUCCESS!
Auch das andere mögliche Ergebnis muss eine boolesche Funktion erfüllen, was sie glücklicherweise tut. Der Test bleibt grün:
TEST_CASE("not pythagorean triples") { CHECK(false == is_pythagorean_triple(1,1,2)); CHECK(false == is_pythagorean_triple(5,4,6)); }
Weil (3,4,5) als das pythagoräische Tripel mit den kleinsten Zahlen bekannt ist, müssen wir aber auch die Null ausschließen: Ein Dreieck, bei dem mindestens eine Seite die Länge 0 hat, ist kein Dreieck. Das ist noch nicht geschehen. Ebenso haben negative ganze Zahlen keinen Sinn: Längen sind niemals negativ.
TEST_CASE("pythagorean triples contain only positive numbers") { CHECK(false == is_pythagorean_triple(0,1,1)); CHECK(false == is_pythagorean_triple(-3,4,5)); CHECK(false == is_pythagorean_triple(-3,-4,-5)); }
Dieser Test wird erst mit einer weiteren Änderung grün:
#include <algorithm> #include "pythagorean.hpp" bool is_pythagorean_triple(int a, int b, int c) { if (a < 1 || b < 1 || c < 1) return false; if (a > c) std::swap(a,c); if (b > c) std::swap(b,c); return a*a + b*b == c*c; }
[doctest] test cases: 3 | 3 passed | 0 failed | 0 skipped [doctest] assertions: 7 | 7 passed | 0 failed | [doctest] Status: SUCCESS!
Sollten weitere Forderungen kommen (nur teilerfremde Zahlen?), sind wir gewappnet. Nebenbei gibt es unendlich viele weitere pythagoräische Tripel, z.B. (5,12,13)… Was sollte passieren, wenn die Quadrate größer werden als der Zahlenbereich für int
-Werte?
Unabhängig davon, ob wir doctest
, das ähnlich zu handhabende Catch2, ein anderes Test-Framework nutzen oder eigene Testprogramme schreiben, können wir diese in CMakeLists.txt
registrieren
enable_testing() add_test(test_pyth test_programm)
und dann mit einem der Befehle
cmake --build . --target test ctest ctest -V
ausführen lassen:
Test project .../build Start 1: test_pyth 1/1 Test #1: test_pyth ........................ Passed 0.01 sec 100% tests passed, 0 tests failed out of 1 Total Test time (real) = 0.03 sec
Die Option -V
(mit großem V!) steht für verbose = ausführlich, nützlich vor allem, wenn ein Test scheitert.