…one of the most highly regarded and expertly designed C++ library projects in the world. Dieses Zitat stammt von  Herb Sutter und Andrei Alexandrescu. Boost Bibliotheken erfüllen die höchsten Ansprüche und müssen einen strengen Peer Review Prozess durchlaufen. Zudem entsprechen die Namenskonventionen jenen der Standardbibliothek, was Anwendungscode gut lesbar macht. Boost hat großen Einfluss auf C++ Standards: Viele der Bibliotheken wurden bereits in die Standardbibliothek aufgenommen.

Wegen der oben genannten Eigenschaften ist das Unit Test Framework Boost.test einer näheren Betrachtung wert.

Ein erster Unit Test

Der folgende Code verwendet die Header-only Variante der Bibliothek. Weitere Varianten beschreibt die Dokumentation von Boost.test.

#define BOOST_TEST_MODULE First Test         // <- 1
#include <boost/test/included/unit_test.hpp> // <- 2
#include <string>

BOOST_AUTO_TEST_CASE(hello_world)            // <- 3
{
    std::string sut("Hello World");
    BOOST_TEST(sut.length() == 11);          // <- 4
    BOOST_TEST(sut[0] == 'h');               // <- 5
}
  1. Definiert den Namen des Testprogramm
  2. Inkludiert die Header-only Variante von Boost.test
  3. Startet die Definition des Tests hello_world
  4. Prüft die Länge der Zeichenkette “Hello World”
  5. Prüft, ob der erste Buchstabe der Zeichenkette ‘h’ ist.

Beim Ausführen des Tests sieht man, dass der Vergleich im Makro BOOST_TEST kein einfaches Anwenden des Vergleichsoperators für char ist:. Der Test Report enthält sowohl die Bedingung, wie sie im Code steht, als auch die konkreten Werte.

> g++ first_test.cpp -o first_test && ./first_test
Running 1 test case...
first_test.cpp(9): error: in "hello_world": check sut[0] == 'h' has failed ['H' != 'h']

*** 1 failure is detected in the test module "First Test"

Organisation von Tests in Testsuites

Man kann Tests hierarchisch in Test Suites organisieren. Dazu verwendet man etwa die Makros BOOST_AUTO_TEST_SUITE(name) und BOOST_AUTO_TEST_SUITE_END(). Alle Tests, die man zwischen den beiden Makros definiert, gehören implizit zur Suite. Im Gegensatz zu Google Test muss man also den Namen der Suite nicht für jeden Test wiederholen und damit das DRY-Prinzip verletzen. Test Suites lassen sich beliebig tief verschachteln. Zudem hat jede Suite ihren eigenen Namensraum, was hilft Namenskollisionen zu vermeiden.

Prüfen von Testbedingungen

Das einführende Beispiel zeigt die grundlegende Verwendung des Makros BOOST_TEST. Aber das Makro kann aber viel mehr:

  • Vergleichen von Containern
  • Vergleichen C-Zeichenketten (char*) werden wie std::string behandelt
  • Bitweises Vergleichen
  • Man kann Hinweise ausgeben lassen, die bei der Fehleranalyse helfen
  • Man kann die Toleranz für den Vergleich von Fließkommazahlen festlegen

Ist eine Bedingung nicht erfüllt, markiert Boost.test den Test als fehlgeschlagen und fährt mit der Ausführung fort. Möchte man den Test sofort beenden, wenn eine Bedingung nicht erfüllt ist, verwendet man das Makro BOOST_TEST_REQUIRE. Damit kann man etwa einen Zeiger prüfen, bevor man ihn dereferenziert. Das Makro BOOST_TEST_WARN gibt bei nicht erfüllter Bedingung eine Warnung aus ohne den Test als fehlgeschlagen zu markieren. Die folgenden Beispiele zeigen die Verwendung des Makros:

BOOST_AUTO_TEST_CASE(container_comparison)
{
  std::vector<int> a{1,2,3};
  std::vector<int> b{1,2,4};
  BOOST_TEST(a == b, boost::test_tools::per_element());
}
-> conditions_examples.cpp(10): error: in "container_comparison": check a == b has failed
Mismatch at position 2: 3 != 4.

BOOST_AUTO_TEST_CASE(lexicographic_comparison)
{
  const char* a = "Hello World!";
  std::string b = "Hello world!";
  BOOST_TEST(a == b);
}
-> conditions_examples.cpp(17): error: in "lexicographic_comparison": check a == b has failed [Hello World! != Hello world!]

BOOST_AUTO_TEST_CASE(float_comparison)
{
  auto a = 1.0f;
  auto b = 1.0f - 0.001;
  BOOST_TEST(a == b, boost::test_tools::tolerance(0.01));
}
-> OK

BOOST_AUTO_TEST_CASE(bitwise_comparison)
{
  int a = 0xFF;
  int b = 0xAE;
  BOOST_TEST_REQUIRE(a == b, boost::test_tools::bitwise());
}
-> conditions_examples.cpp(31): fatal error: in "bitwise_comparison": critical check a == b has failed [255 != 174]. Bitwise comparison failed
Mismatch at position 0
Mismatch at position 4
Mismatch at position 6


BOOST_AUTO_TEST_CASE(additional_information)
{
  BOOST_TEST(3 * 3 == 6,
    "Multiplication ain't addition (3*3 == " << (3*3) << ")"); 
}
-> conditions_examples.cpp(38): error: in "additional_information": Multiplication ain't addition (3*3 == 9)

Neben BOOST_TEST gibt es die aus anderen xUnit Frameworks bekannten Makros zum Definieren von Testbedingungen. Hier findet man eine Zusammenfassung aller Makros zum Prüfen von Bedingungen.

Ausnahmebehandlung

Wenn ein Test eine Ausnahme nicht behandel, tut dies das Framework und markiert den Test als fehlgeschlagen. Um zu prüfen, ob eine Funktion oder eine Methode eine Ausnahme auslöst, kann man folgende Makros verwenden:

  • BOOST_<level>_THROW: Prüft ob eine Ausnahme eines bestimmten Typs ausgelöst wird.
  • BOOST_<level>_EXCEPTION: Prüft, ob eine Ausnahme eines bestimmten Typs ausgelöst wird und inspiziert die Ausnahme unter Verwendung eines benutzerdefinierten Prädikats.

Test Fixtures

Test Fixtures ermöglichen, Funktionen zum Vorbereiten und Aufräumen von Testumgebungen zu kapseln und in mehreren Test wiederzuverwenden. Das Framework erlaubt die Verwendung von Fixtures für einzelene Tests, Suites und Modulen. Die einfachste Form der Fixture ist eine Klasse in deren Konstruktor man die Umgebung vorbereitet und in deren Desktruktor man die Testumgebung aufräumt. Diese Methode eignet sich jedoch nicht für Fixtures, die beim Aufräumen Ausnahemen auslösen können, weil man in Destruktoren keine Ausnahemen auslösen soll. Daher kann man zusätzlich zum Konstruktor die Methoden setup() und teardown() in Fixtures definieren. Das folgende Beispiel zeigt die Verwendung einer Test Fixture:

struct my_fixture {
    void setup() {
        std::cerr << "Setup" << std::endl;
    }

    void teardown() {
        std::cerr << "Teardown" << std::endl;
    }
};

BOOST_FIXTURE_TEST_CASE(fixture_test, my_fixture) {}

Parametrisierte Tests

Parametrisierte oder datengesteuerte Tests erlauben das Ausführen eines Tests mit vielen unterschiedlichen Daten. Boost.test definiert eine einfache Schnittstelle um Datensätze zu definieren. Diese kann man mit vordefinierten Operationen, miteinander kombinieren. Zudem bietet Boost.test vorgefertigte Datengeneratoren. Das folgende Beispiel zeigt, wie man alle möglichen Paare aus zwei Listen erzeugt:

const std::vector<std::string> ranks
{"Ace", "King", "Queen", "Jack"};

const std::vector<std::string> suits
{"Diamons", "Clubs", "Hearts", "Spades"};

BOOST_DATA_TEST_CASE(
    playing_cards,
    boost::unit_test::data::make(ranks)*suits,
    rank,
    suit)
{
    std::cerr << "rank: '" << rank 
              << "', suit: '" << suit << "'" << std::endl;
}

Reporting

Standardmäßig gibt Boost.test die Ergebnisse in einem von Menschen lesbarem Format aus. Um die Ergebnisse in der Build-Umgebung automatisch zu verarbeiten, kann man entweder das proprietäre XML-Format von Boost.test oder jenes von JUnit verwenden. Weil das JUnit-Format von vielen Softwarewerkzeugen wie etwa Jenkins interpretiert werden kann, lässt sich Boost.test schnell in bestehende Build-Umgebungen einbinden.

Logging

Damit Boost.test benutzerdefinierte Datentypen in lesbarer Form ausgeben kann, muss man einen der folgenden Operatoren definieren. Die zweite Variante ermöglicht die Definitionen eines Ausgabeformats, das nur für Unit Tests verwedent wird.

std::ostream& operator<<(std::ostream&, const UserDefinedType&)
std::ostream& boost_test_print_type(std::ostream&, const UserDefinedType&)

Dekoratoren

Durch Hinzufügen von Dekoratoren zu Tests und Test Suites kann man diese mit Informationen anreichern oder deren Verhalten beeinflussen. So kann man etwa Abhängigkeiten zwischen Tests definieren, sodass das Framework einen Test nicht ausführt, wenn dessen Vorgänger nicht erfolgreich war. Mit dem Dekorator label kann man zu einem Test beliebig viele Markierungen hinzufügen.

Der Dekorator timeout ermöglicht die maximale Laufzeit eines Tests festzulegen. Das Framework bricht den Test bei Überschreiten der Zeit automatisch ab. Wie bei fast allen Konzepten von Boost.test ist auch das Konzept der Dekoratoren durch den Benutzer erweiterbar. Möchte man etwa Test abhängig von der Testumgebung aktivieren oder deaktivieren, so kann man das mit Hilfe von Dekoratoren auf elegante Art erreichen.

Mocking Bibliothek Turtle

Leider hat Boost noch keine Mocking Bibliothek. Allerdings gibt es mit Turtle eine hervorragende Lösung, die für den Einsatz mit Boost.test entwickelt wurde.

Zusammenfassung

Boost.test ist eine hervorragende Unit Test Bibliothek, die den Namenskonventionen des C++ Standards folgt. Wegen sauberer Konzepte und vieler Erweiterungspunkte kann man Boost.test sehr gut an die Anforderungen des jeweiligen Projekts anpassen.

Das Konzept zum Erzeugen von Test Suites ist sehr mächtig und verletzt im Gegensatz zu Google Test nicht das DRY-Prinzip. Wenn man auf der Suche nach einem Unit Test Framework für C++ ist, sollte man auf jeden Fall Boost.test in Betracht ziehen!