Jako programista systemów wbudowanych, często stawałem przed wyzwaniem zintegrowania skutecznych testów jednostkowych w środowisku, gdzie zależności sprzętowe są nieodłączną częścią procesu deweloperskiego. W tym artykule chcę podzielić się moimi doświadczeniami i technikami, które pomogły mi przezwyciężyć te wyzwania. Skupię się na wykorzystaniu mocków i wstrzykiwaniu zależności, demonstrując, jak te metody mogą znacząco ułatwić proces testowania jednostkowego w systemach wbudowanych. Ten artykuł jest zbiorem moich przemyśleń i praktycznych porad, które mam nadzieję, będą pomocne dla innych programistów w tej dynamicznie rozwijającej się dziedzinie.
Kiedy zaczynałem karierę jako programista wbudowanych systemów, często słyszałem, że testowanie jednostkowe jest fantastyczne, ale nie w świecie systemów wbudowanych. Było mi trudno zaakceptować to stwierdzenie. Byłem zdeterminowany, aby zakwestionować status quo. Zastanawiałem się. dlaczego my, w świecie systemów wbudowanych, nie możemy przyjąć wszystkich najlepszych praktyk stosowanych na całym świecie?
Więc wziąłem pierwszy lepszy framework do testowania jednostkowego i napisałem mój inauguracyjny przypadek testowy. Co za ekscytująca przygoda! Uruchomiłem go i...
Okazało się, że nie przemyślałem zależności sprzętowych, które istniały w projekcie. Jak to pokonać? I o tym jest ten artykuł.
Czym jest mock?
Mock to symulowany obiekt, który naśladuje zachowanie rzeczywistego obiektu w kontrolowany sposób. Jest używany w testowaniu oprogramowania do zastępowania zależności i symulowania ich zachowania podczas testów jednostkowych. Mocki są często używane, gdy rzeczywiste zależności są trudne do ustawienia lub interakcji podczas testowania.
Pozwalają programistom izolować testowany kod i skupić się na konkretnych scenariuszach bez polegania na faktycznej implementacji zależności.
Mocki można tworzyć ręcznie lub za pomocą frameworków do mockowania, takich jak GMock.
Dependency injection
Mówimy o wstrzykiwaniu zależności, gdy chcemy wskazać, że moduł może łatwo wymienić część kodu, z którego korzysta.
Ale dlaczego i kiedy chcielibyśmy to zrobić?
Jednym z przykładów jest sytuacja, gdy potrzebujemy zmienić zachowanie klasy, która jest używana w innej wersji sprzętowej niż wcześniej. Innym powodem może być użycie mocka zamiast produkcyjnego modułu do testów jednostkowych.
Spójrzmy na poniższy prosty przykład:
Ponieważ moduł "Testable code" wykorzystuje "Production module", umożliwia to łatwe wstrzykiwanie, co skutkuje czystszym i bardziej łatwym do utrzymania kodem. Poprzez stworzenie osobnego pliku wykonywalnego z różnymi zależnościami wstrzykniętymi do "Kodu testowalnego", możemy skutecznie używać go do celów testowych.
Podejście to poprawia nasz przykład, jak pokazano poniżej:
Runtime dependency injection
Ale zastanówmy się, jak możemy zaimplementować wstrzykiwanie zależności w sposób luźny, dostosowany do codziennych potrzeb biznesowych.
Przedstawię Ci dwie metody.
Pierwsza metoda polega na stosowaniu wstrzykiwania zależności w czasie wykonywania programu. Można to osiągnąć, przekazując obiekt oparty na polimorfizmie przez referencję podczas tworzenia naszego "Testable code". Poniżej znajdziesz przykład, który może być użyty w kodzie produkcyjnym:
Compile-time dependency injection
W przypadku wstrzykiwania zależności w czasie kompilacji zadaniem konsolidatora (linkera) jest połączenie odpowiedniej implementacji.
Ważne jest, aby znać system budowania, z którego korzystasz, ale warto spróbować tego podejścia. W ten sposób nie musisz modyfikować swojego API, aby umożliwić wstrzykiwanie modułu.
Można to osiągnąć podczas kompilacji (a dokładniej, podczas konsolidacji).
Oto bardzo prosty przykład, który rozwinę w następnej sekcji:
Dzięki oddzieleniu deklaracji funkcji od definicji w klasie Dependency, mamy możliwość skompilowania oddzielnego pliku z alternatywną definicją tych funkcji. W następnej sekcji wyjaśnię, jak działa ten proces.
Mockowanie przy pomcy compile-time dependency injection
Teraz, korzystając z GMock, pokażę Ci, jak wstrzykiwać mocki zamiast naszej "produkcyjnej" zależności.
Proszę, spójrz:
Przyjrzyjmy się bliżej dostarczonemu kodowi. Zaimplementowaliśmy wzorzec singleton w klasie DependencyMock, aby uniknąć jawnej instancji klasy mock. Zamiast tego, ustalamy nasze oczekiwania co do tego, które funkcje powinny być wywoływane i z jakimi parametrami. To podejście wymaga od nas jawnego wyczyszczenia i zweryfikowania oczekiwania mocka.
W typowym scenariuszu byłoby to zrobione w destruktorze mocka, ale ponieważ używamy wzorca singleton, musimy sobie z tym poradzić w fazie końcowej (tear down) naszego zestawu testowego, tak jak to:
Jak możemy ustalić oczekiwania względem konstruktora? W przypadku testowym "WhenCreatingFooThenDependencyIsCreated" jest oczywiste, że spodziewamy się również utworzenia obiektu "Dependency".
Przyjrzyjmy się całemu procesowi, aby uzyskać bardziej kompleksowe zrozumienie, jak to działa.
Warto zauważyć, że używając makra "MOCK_METHOD", generujemy funkcję członkowską, na której możemy ustawić nasze oczekiwania, takie jak:
Dobrze... ale jak możemy być pewni, że nasza klasa Dependency została utworzona? Właśnie omówiliśmy klasę DependencyMock. To tutaj dzieje się magia. Dzięki wstrzykiwaniu zależności w czasie kompilacji mamy możliwość zdefiniowania naszej klasy Dependency w dowolny sposób, jaki chcemy.
A w celu mockowania, możemy również przekierować wywołanie do naszego obiektu mock, jak pokazano w pliku dependency_mock.cpp:
Dzięki tej linii, za każdym razem, gdy tworzymy obiekt Dependency podczas testów jednostkowych, faktycznie wywołujemy DependencyMock.
To jest powód, dla którego wszystko może funkcjonować bez negatywnego wpływu na API.
Podsumowanie
Testowanie jednostkowe w świecie systemów wbudowanych może być wyzwaniem ze względu na zależności sprzętowe. Jednak istnieją techniki, takie jak używanie mocków i wstrzykiwanie zależności, które mogą pomóc pokonać te wyzwania. Mocki pozwalają programistom na symulowanie rzeczywistych obiektów podczas testowania, podczas gdy wstrzykiwanie zależności umożliwia łatwą wymianę części kodu. Frameworki takie jak GMock mogą być używane do mockowania z wstrzykiwaniem zależności w czasie kompilacji.
Referencje
#GoodByte #EmbeddedSoftware #DependencyInjectionInEmbedded