Do pisania testów jednostkowych/modułowych w C++ potrzeba 2 rzeczy:
- framework do testów
- framework do mockowania zależności testowanej klasy.
Spośród wielu projektów, które widziałem, większość używa zestawu GoogleTest + GoogleMock. Z frameworków do testowania kiedyś popularny był Boost.Test (ktoś używał? czemu gtest go wyparł?), teraz na popularności zyskuje catch2. Ja jednak chcę się skupić na frameworku do mockowania. gmock opiera się na tym samym patencie co javowe mockito, z tym że w c++ wygląda to tak:
- jest sobie interfejs IFoo z metodami wirtualnymi, na przykład virtual void aaa() = 0;
- jest sobie klasa Foo dziedzicząca po tym interfejsie, która implementuje tę metodę: void aaa() override;
- jest sobie mock FooMock, który również implementuje tą metodę w sobie potrzebny sposób. gmock robi to za pomocą makr: MOCK_METHOD0(aaa, void());
- klasa Bar dostaje w konstruktorze wstrzyknięto zależność na interfejs IFoo (referencja/wskaźnik)
- w kodzie produkcyjnym wstrzykujemy Foo
- w kodzie testowym wstrzykujemy FooMock i to na interakcjach testowanej klasy z nim opieramy wynik testu
Jednak ma to duży minus: większość klasy projektu ma interfejsy i używa dynamicznego polimorfizmu tam gdzie w rzeczywistości nie musi tylko po to, aby kod był testowalny. A w c++ to kosztuje.
Innym podejściem jest używanie szablonów zamiast dziedziczenia. Klasa Bar wyglądałaby wtedy tak:
template <class FooType>
class Bar
{
//(...)
private:
FooType foo;
};
Kod produkcyjny:
Bar<Foo> bar;
Kod testowy:
Bar<FooMock> sut;
FooMock i Foo nie muszą dziedziczyć po wspólnej klasie.
Nie stosowałem tego podejścia, ale domyślam się, że największe minusy to: cały kod klasy w headerze (w końcu szablon), ogólna "szablonoza" projektu, potrzeba pisania getterów tylko po to, aby w teście dostać się do mocka.
Ostatnio odkryłem istnienie biblioteki FSeam (link, link). Przyznam, że nie używałem jeszcze, ale z tego co doczytałem, to jej podejście jest inne: podmienia zależności na etapie linkowania:
- jest sobie header Foo.hpp z deklaracją klasy i deklaracjami jej metod
- jest sobie plik z kodem: Foo.cpp z definicjami metod. Kompilowany do object file'u Foo.cpp.o
- jest sobie wygenerowany przez fseam plik z alternatywnymi, mockowymi definicjami metod. Kompilowany do object file'u, nazwijmy go umownie, FooMock.cpp.o
- jest sobie klasa Bar includująca normalnie Foo.hpp i mająca pole klasy Foo. Object file: Bar.cpp.o
- kod produkcyjny linkuje się z Foo.cpp.o
- kod testowy linkuje się z FooMock.cpp.o
Dodatkowy wysiłek jaki trzeba włożyć to:
- odpalanie generatora testowych implementacji
- zabawa na poziomie plików cmake'owych
Z niewątpliwych atutów, to można pisać testy legacy code'u kompletnie bez jego zmieniania.
Teraz główne pytania:
- czy ktoś używał tej biblioteki, albo innej korzystającaej z "link seam"? jakie ogólne wrażenia?
- czy to podejście jest wygodniejsze niż praca z bibliotekami typu gtest, gdzie wymagane jest dodawanie interfejsów do kodu?
- czy ktoś ma wiarygodne obliczenia performancowe na ile użycie dynamicznego polimorfizmu i "javaizacja" projektu pod testy "szkodzi" w wydajności, optymalizacji kodu przez kompilator, rozmiarze generowanych binarek, itd.?