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ą ważną zaletą jest fakt, że 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 failed
Jak 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.
14 stycznia 2019 at 23:08
Hej!
W distortos też od jakiegoś czasu staram się dodawać testy jednostkowe (trochę się zainspirowałem Twoim blogiem i innymi które czasem przeglądam, a gdzie temat TDD się przewija). Używam właśnie Catch2 oraz trompeloeil do robienia mocków ( https://github.com/rollbear/trompeloeil ). Co ciekawe udało mi się nawet wyczarować sposób testowania najniższego poziomu (!) driverów sprzętowych. Jeszcze sam nie wiem ile jest on warty, jednak pozwolił mi on zoptymalizować niektóre drivery gdy zobaczyłem w tym teście jednostkowym że pewne operacje nie mają sensu lub można by je zrobić lepiej. Po prostu sam dostęp do poszczególnych rejestrów sprzętowych (zapis/odczyt) zamknąłem w extremalnie prostej klasie, którą dla celów testu jednostkowego zastępuję mockiem, dzięki czemu mam kontrolę nad tym jakie rejestry są odczytywane/zapisywane, w jakiej kolejności i jakimi wartościami. Oczywiście przerwania nie generują się same ani też flagi nie ustawiają się samoczynnie, niemniej jednak coś tam można sobie „zasymulować” i przetestować, „zobaczyć” jak ten kod faktycznie działa i co robi.
Generalnie nie mam doświadczenia w UT, więc sporo experymentuję z tym podejściem – zobaczymy jak się to dalej rozwinie. Jakby Ci się nudziło to możesz zerknąć do repo distortos i napisać ze 3 słowa co o tych testach sądzisz (; Sporo z nich jest raczej „nietypowych”, bo np. bardziej niż jakiś wynik testu (którego w sumie nie ma) interesuje mnie, czy jakiś template w ogóle mogę utworzyć dla wszystkich dziwnych kombinacji obsługiwanego typu (innymi słowy – czy całość się kompiluje i linkuje), ale kilka testów jest też chyba takich bardziej „normalnych”, szczególnie tych nowszych.
14 stycznia 2019 at 23:37
Cześć
Tak się składa, że właśnie bawię się Distortosem i próbuję go wkomponować w swój projekt – przepisuje micromouse do c++. Na razie dopiero uruchomiłem projekt ze static threadem i teraz próbuję uruchomić silniki, więc poznaję drivery GPIO distortosa i jak napisać swoje do TIM żeby były w miarę spójne. Na unit testy też luknę w takim razie.
Do testowania templatów może Ci się spodoba ten artykuł o compile time unit testach (http://softwarephilosophy.ninja/compile-time-unit-testing). Jeszcze nie miałem okazji tego sprawdzić w praktyce, ale może być przydatne.
15 stycznia 2019 at 08:43
> Tak się składa, że właśnie bawię się Distortosem i próbuję go wkomponować w swój projekt – przepisuje micromouse do c++.
W razie pytań/problemów/wątpliwości/sugestii/opinii/… – pisz (forma kontaktu dowolna), z chęcią pomogę, bo wiem że początki mogą być dosyć trudne.
22 marca 2020 at 08:31
jestem ciekawy w jaki sposob uruchomi pan Catch2 na mikrokontrolerze…