Ostatnio trochę eksperymentowałem z nowym frameworkiem do unit testów – Catch2. Główną różnicą od innych frameworków takich jak CppUTest czy GoogleTest jest rezygnacja z grup testowych na rzecz struktury Given-When-Then wspierającej Behavior Driven Design (BDD). Inną istotną zaletą jest fakt, iż cały framework mieści się w jednym headerze, dlatego nie ma problemów z jego integracją.
Przykładowy test
Bez zbędnego lania wody przejdźmy zatem do przykładowego kodu unit testów:
SCENARIO("Update on front sensors when robot pointed toward East", "[wall_detection]") { map::Map map; map::SingleDetectionUpdatePolicy mapUpdatePolicy(map); wall_detection::BasicDetectionPolicy detectionPolicy(mapUpdatePolicy); GIVEN("Robot pointed towards East") { values::Position position = positionFromCell(CELL, ANGLE_EAST); WHEN("Front Left sensor detects wall") { detectionPolicy.detect(wall_detection::SensorId::FrontLeft, DISTANCE_WHEN_WALL_DETECTED, position); THEN("Map is updated with East wall on a given cell") { REQUIRE(map.isWallForCell(CELL, map::WallId::East)); } } WHEN("Front Left sensor detects no wall") { detectionPolicy.detect(wall_detection::SensorId::FrontLeft, values::WALL_TOO_FAR, position); THEN("Map is updated with no East wall on a given cell") { REQUIRE(!map.isWallForCell(CELL, map::WallId::East)); } } } }Jest to test modułu wykrywania ścian do robota Micromouse. Na początku mamy inicjalizację zmiennych wykorzystywanych we wszystkich testach. Dalej mamy blok GIVEN, gdzie możemy zdefiniować warunki początkowe dla kolejnych test casów. Dla jednej początkowej inicjalizacji możemy mieć wiele bloków GIVEN. Z kolei wewnątrz każdego bloku GIVEN możemy mieć wiele bloków WHEN, a wewnątrz każdego z nich wiele THEN. Każda możliwa kombinacja GIVEN-WHEN-THEN tworzy oddzielny test case. Dzięki temu odchodzi nam problem obecny w frameworkach stosujących grupy testów – czy duplikować kod, czy zwinąć wszystko w funkcje pomocnicze przenosząc do nich część informacji potrzebnych do zrozumienia co się dzieje w danym teście.
Stosując jawnie strukturę GIVEN-WHEN-THEN nasze testy są dużo bardziej czytelne i możliwe do zrozumienia przez osoby nie znające szczegółów implementacji.
Output w konsoli
W przypadku sukcesu w konsoli mamy minimalny output:
=============================================================================== All tests passed (158 assertions in 25 test cases)Z kolei w przypadku błędu output wygląda tak:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ unit_tests is a Catch v2.5.0 host application. Run with -? for options ------------------------------------------------------------------------------- Scenario: Update on front sensors when robot pointed toward East Given: Robot pointed towards East When: Front Left sensor detects wall Then: Map is updated with East wall on a given cell ------------------------------------------------------------------------------- ../domain/wall_detection/BasicDetectionPolicyTests.cpp:44 ............................................................................... ../domain/wall_detection/BasicDetectionPolicyTests.cpp:46: FAILED: REQUIRE( !map.isWallForCell(CELL, map::WallId::East) ) with expansion: false =============================================================================== test cases: 25 | 24 passed | 1 failed assertions: 158 | 157 passed | 1 failedJak widać dzięki tekstowi wpisanemu w macra GIVEN WHEN THEN mamy bardzo dobry opis test case, który sfailował. Dodatkowo otrzymujemy standardowe informacje takie jak linijka, w której wystąpił błąd i składnia aserta, który to wychwycił.
Pozostałe funkcje
Nazwy GIVEN-WHEN-THEN to tylko aliasy dla bloku SECTION, który można dowolnie zagnieżdżać. Dzięki temu struktury testów mogą być dużo bardziej rozbudowane (żeby jednak nie poniosła nas fantazja, bo testy muszą być czytelne!). Istnieje również wsparcie dla klasycznych grup testowych w dokumentacji występujących jako Test Fixtures . Można również łączyć sekcje i grupy.
Mamy asserty(REQUIRE) wspierające wszystkie typy implementujące operator ==. Jest również opcja natychmiastowego kończenia testu z wynikiem PASS albo FAIL. Istnieje również mechanizm generowania testów z jednego szablonu, gdzie zmieniają się tylko wartości wejściowe.
Więcej o frameworku można dowiedzieć się z prezentacji autora:
Podsumowanie
Framework Catch2 bardzo mi się spodobał i przy kolejnych projektach będzie teraz moim pierwszym wyborem. Testuje go już od jakiegoś czasu przy okazji przepisywania mojego micromouse do C++ i eksperymentów z architekturą. Nie próbowałem go jeszcze uruchamiać na HW, ale mając kompilator c++14 nie powinno być z tym problemu.