TDD w systemach embedded - wszystkie wpisy

W idealnym świecie nie testujemy szczegółów implementacyjnych jakimi są funkcje statyczne. To samo tyczy się prywatnych pól i metod klasy w językach obiektowych. Zamiast tego piszemy testy dla publicznego API i z pomocą odpowiednich mocków jesteśmy w stanie zaobserwować całe zachowanie testowanego modułu z zewnątrz. Rzeczywistość często nie jest taka różowa i musimy często jakoś dostać się do tego ukrytego kodu.

W dalszej części artykułu zastanowimy się dlaczego tak się dzieje i jak radzić sobie z takimi przypadkami. Jednak kiedy jesteśmy postawieni przed problemem testowania prywatnych funkcji, najpierw zadajmy sobie jedno ważne pytanie. Czy to nie jest to oznaka jakiś problemów architektonicznych?

Być może testowany moduł ma zbyt dużo odpowiedzialności i powinien zostać rozbity na kilka mniejszych. Warto również zastanowić się, czy nie próbujemy za mocno uzależnić testów od konkretnej implementacji. Takie testy nabijają coverage, ale czynią wprowadzanie późniejszych zmian prawdziwą udręką. I tutaj wracamy do teorii – działanie każdej funkcji prywatnej powinno być widoczne na zewnątrz. Nie obchodzi nas jak to jest wewnątrz zrealizowane, ale czy zachowanie jest poprawne.

Kiedy testujemy funkcje prywatne?

Jak już wcześniej wspomniałem, istnieją przypadki, kiedy nie uciekniemy od testowania funkcji statycznych. Najczęściej są to:

  • Code coverage – pisanie testów dla coverage nie jest dobrą praktyką, o czym już kiedyś pisałem. Jeżeli piszemy w TDD, wysokie pokrycie będzie efektem ubocznym dobrze napisanych testów. Jednak czasem zdarza się, że w projekcie wymagane jest utrzymanie coverage na odpowiednim poziomie. Może to być spowodowane np. potrzebą certyfikacji w systemach safety-critical. W takim wypadku, aby uzyskać pokrycie rzędu 95%, zwykle nie uciekniemy od testowania funkcji statycznych.
  • Legacy code – jeżeli mamy do czynienia z istniejącą bazą kodu bez testów, często pojedynczy plik zawiera dużo różnych funkcji, często statycznych. Nie ma również możliwości podejrzenia wszystkich zmian z zewnątrz przy pomocy mocków. Najczęściej docelowo chcemy zmodyfikować taki kod, aby był łatwiejszy w utrzymaniu. W tym celu potrzebujemy testów sprawdzających, czy niczego nie zepsujemy wprowadzając zmiany.
  • Wewnętrzny stan trudny do ustawienia z zewnątrz – dobrym przykładem może być tutaj maszyna stanów. Po inicjalizacji znajduje się ona w jakimś początkowym stanie i zanim będziemy mogli wykonać testy dla innego stanu, musimy go ustawić. Jeżeli korzystamy tylko z prywatnego API może to wymagać wielu wywołań i podawania specyficznych danych. Zamiast tego dużo łatwiej jest po prostu wpisać stan do zmiennej statycznej.

Pora przejść do sposobów na uzyskanie dostępu do zmiennych i funkcji statycznych.

Modyfikator PRIVATE

Najprostszym sposobem na uzyskanie dostępu do prywatnych symboli jest zdefiniowanie własnego modyfikatora PRIVATE:

Dzięki temu prostemu zabiegowi symbole produkcyjne z modyfikatorem PRIVATE będą lokalne w docelowej aplikacji, ale dostępne w unit testach, jeśli użyjemy deklaracji potrzebnego symbolu jako extern:

Tego sposobu używam już od bardzo długiego czasu ze względu na jego prostotę. Posiada on jednak kilka wad:

  • ingeruje w kod produkcyjny – każdą zmienną i funkcję, którą chcemy mieć dostępną w unit testach musimy zadeklarować jako PRIVATE zarówno w headerze, jak i w pliku źródłowym. Wymaga to zmian w istniejącym kodzie produkcyjnym. Szczególnie w systemach legacy może zdarzyć się, że kod produkcyjny jest zamrożony i nie możemy zrobić nawet takiej prostej zmiany.
  • możliwe konflikty nazw – w unit testach lokalne symbole z różnych plików mogą mieć takie same nazwy powodując błędy kompilacji. Jest to bardzo rzadki przypadek, ale jednak możliwy.
  • metoda może być nadużywana przez lenistwo – dzięki swojej prostocie, możemy stosować tą metodę nawet tam, gdzie nie jest potrzebna.

Funkcje testowe w pliku produkcyjnym

Kolejnym sposobem wykorzystującym preprocesor i ingerującym w plik produkcyjny jest dodanie na końcu pliku produkcyjnego funkcji kompilowanych tylko w unit teście dających dostęp do prywatnych symboli. Może to wyglądać następująco:

W unit testach możemy dodać też header z definicjami testowych funkcji i używać je w unit testach. To rozwiązanie również ingeruje w kod produkcyjny. Funkcji testowych może być całkiem sporo i zaśmiecają one plik źródłowy.

Include pliku źródłowego

Kolejny sposób umożliwia testowanie nie ruszając w ogóle pliku produkcyjnego. Niestety jest też strasznie brzydki i niektórym dopisanie czegoś takiego do swojego kodu może nie przejść przez palce 🙂 Ten sposób to includowanie produkcyjnego pliku .c w pliku testowym. Może to wyglądać tak:

Może i wygląda strasznie, ale działa i nie trzeba nic dopisać do pliku produkcyjnego. Musimy tylko uważać na parę smaczków podczas kompilacji:

  • Plik produkcyjny może być zaincludowany tylko raz. Każde kolejne dodanie spowoduje błąd wielokrotnej definicji tej samej funkcji.
  • Plik produkcyjny nie może być bezpośrednio kompilowany. Kompilacja pliku i jego dołączenie gdzie indziej spowoduje ten sam błąd, co powyżej.
  • Plik includujący kod produkcyjny musi być skompilowany z flagami produkcyjnymi. Chcemy mieć maksymalną pewność, że testujemy ten sam kod, co na produkcji. A często w plikach testowych stosujemy inne flagi np. mniej restrykcyjne warningi.

Testowanie prywatnych pól klasy w C++

To tyle jeżeli chodzi o tricki dotyczące testowania prywatnych symboli w C. Na koniec pokażę jeszcze mechanizm  dostępny w C++ do testowania prywatnych pól w klasie. Ale przedtem przypomnę po raz kolejny – dobre unit testy to takie, które nie muszą znać szczegółów implementacyjnych. Zanim użyjesz tej metody, najpierw spróbuj poradzić sobie bez takich sztuczek.

C++ posiada mechanizm klasy zaprzyjaźnionej. Takiej klasy nie obowiązują zasady enkapsulacji – ma dostęp do wszystkich pól i metod klasy bazowej. Jeżeli zdefiniujemy klasę zaprzyjaźnioną na czas unit testów, będziemy mogli za jej pomocą dostać się do wszystkich interesujących nas funkcji i wymusić wszystkie potrzebne stany. Poniżej przykład jak to zrobić:

Oczywiście stosując tą metodę w większym projekcie trzeba odpowiednio podzielić powyższy kod na pliki. Define friend class muszą być includowane do pliku produkcyjnego i klasa testowa musi być widoczna. Natomiast sama definicja klasy testowej powinna być w oddzielnym pliku wykorzystywanym tylko w testach.

Podsumowanie

W artykule przedstawiłem kilka technik, które możemy wykorzystać do testowana funkcji statycznych. Różnią się one między sobą stopniem skomplikowania i ingerencją w kod produkcyjny. Możemy więc wybrać odpowiedni sposób do naszych potrzeb. Zanim to zrobimy, pamiętajmy jednak, że najlepszym rozwiązaniem jest po prostu nie wnikanie w szczegóły implementacyjne i wywołanie każdego spodziewanego zachowania z zewnątrz.

 

 

 

 

 

 

TDD w systemach embedded - Nawigacja << Pisanie własnych mocków Jak testować nieskończone pętle? >>