namespace cpp

C++ lernen, kennen, anwenden

Benutzer-Werkzeuge

Webseiten-Werkzeuge


lernen:doctest

doctest

Unit-Tests

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.

Die Aufgabe

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

Tests zuerst

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));
}

Roter Test (schlägt fehl)

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!

Grüner Test (ist bestanden)

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!

Neue Anforderungen

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?

CTest

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.

1)
Ich weiß nicht warum. Hängt vermutlich damit zusammen, dass wir uns an Stelle des Satzes von Pythagoras die Formel a²+b²=c² gemerkt haben. Wie oft hat deine Lehrerin darauf hingewiesen, dass die längste Seite nicht c heißen muss?
lernen/doctest.txt · Zuletzt geändert: 2020-08-23 21:23 von rrichter