Aby mikrocykl TDD Red-Green-Refactor był efektywny, kompilacja i wykonanie testu powinny trwać kilka sekund. W praktyce oznacza to, że testy nie są wykonywane na docelowej platformie i należy podjąć dodatkowe kroki w celu wykrycia ewentualnych problemów związanych ze sprzętem. W poprzednim wpisie wytłumaczyłem jak może w tym pomóc dual targeting. Dzisiaj opiszę jak powinien wyglądać proces tworzenia oprogramowania, aby minimalizować ryzyko związane ze sprzętem. Proces ten jako pierwszy opisał James Grenning (prowadzi świetnego bloga o TDD Embedded – link) w swojej książce „Test Driven Development for Embedded C”. Zaproponował on Embedded TDD Cycle składający się z pięciu kroków.

Krok 1 – Klasyczny mikrocykl TDD

W tym kroku skupiamy się, aby logika kodu była poprawna. Interesuje nas szybka informacja zwrotna z testów, czy niczego nie zepsuliśmy. W tym kroku nie dodajemy sobie kolejnego poziomu złożoności związanego ze sprzętem. Nie musimy więc czekać na wgranie programu, uruchomienie na sprzęcie, eliminujemy problemy np. z niestykającym przewodem debuggera, czy zawieszeniem programatora. Ten krok powinien być wykonywany co kilka minut w środowisku developerskim.

Krok 2 – Kompilacja na docelowy procesor

Wiedząc, że nie mamy błędów w logice, możemy przejść do zapewnienia, że wszystkie targety kompilują się na docelowy procesor. Często dodając nową funkcjonalność dodajemy jakiś header produkcyjny, który następnie mockujemy w teście, ale zapominamy o produkcyjnej implementacji, albo psujemy w ten sposób wcześniejsze testy. Poza tym możemy wykryć problemy z portowaniem – specyficzną składnię dla konkretnego kompilatora (np. attribute, interrupt), czy różnice w bibliotekach standardowych (kompilatory embedded posiadają headery z definicjami na konkretne procesory). Przy okazji dzięki temu przyszła migracja na inny procesor będzie mniej bolesna. Ten krok powinien być wykonywany w środowisku developerskim przed każdym commitem, oraz przez system Continuous Integration po każdym pushu na serwer.

Krok 3 – Uruchomienie testów na eval boardzie

Sprawdzenie, że kod się poprawnie kompiluje na docelowy procesor nie załatwia wszystkich problemów z kompatybilnością. Może się zdarzyć, że niektóre funkcje działają inaczej niż w środowisku developerskim. Właśnie takie błędy ma za zadanie wykryć ten krok. Dzięki użyciu płytki ewaluacyjnej możemy we wczesnej fazie projektu zacząć testy na sprzęcie i wyłapać niekompatybilności. Później, kiedy docelowy hardware już jest dostępny, przez jakiś czas ten krok nadal jest przydatny. W końcu PCB może również zawierać błędy i dzięki testom na eval boardzie jesteśmy w stanie upewnić się, że problem leży po stronie sprzętu. Ten krok powinien być wykonywany przez Continuous Integration raz dziennie – najlepiej podczas nocnego builda.

Krok 4 – Uruchomienie testów na docelowym sprzęcie

Spełnia podobne zadania jak krok 3. Kiedy mamy już stabilną platformę sprzętową, możemy zaniechać kroku 3 i wykonywać tylko testy na produkcyjnym sprzęcie. Do eval boarda możemy zawsze wrócić w przypadku wykrycia jakiegoś podejrzanego zachowania. Ten krok również powinien być wykonywany przez CI podczas nocnego builda.

Krok 5 – Testy wyższych poziomów na docelowym sprzęcie

W tym kroku wykonujemy testy integracyjne, akceptacyjne, manualne, czy wydajnościowe. Są rzeczy, których testy automatyczne nie są w stanie sprawdzić. W końcu ktoś musi fizycznie nacisnąć przycisk, czy potwierdzić, że czerwona dioda na pewno się zapaliła. Takie testy można wykonywać rzadziej niż pozostałe. Pełny cykl testów powinien być przeprowadzony przed każdym releasem. Jeżeli przez dłuższy czas nie wydajemy releasów, a dochodzi nowy kod, również warto przeprowadzić przynajmniej część testów wysokiego poziomu, aby upewnić się, że system działa zgodnie z założeniami.

Symulatory

James Grenning w swojej książce w kroku 3 proponuje używanie symulatorów jako alternatywa dla eval boardów. Z mojego doświadczenia wynika jednak, że z symulatorami trzeba uważać. Zwykle nie przechodzą one tak rygorystycznej kontroli jakości jak procesory i istnieje większa szansa, że będą zawierać swoje własne błędy. Dlatego cele kroku 3 łatwiej osiągnąć za pomocą płytki ewaluacyjnej. Symulator może być jednak użyty w kroku 1 do uruchamiania testów w środowisku developerskim. Wtedy możemy zrezygnować z kompilacji unit testów kompilatorem dla PC.

Potrzebne narzędzia

Aby rozwijać systemy embedded zgodnie z opisanymi tutaj krokami należy mieć odpowiednio skonfigurowany system buildowania (np. Makefile) oraz narzędzie do Continuous Integration. Skrypty buildowania muszą zapewniać możliwość uruchamiania kompilacji i testów dla wszystkich targetów na raz oraz dla każdego pojedynczo, powinny obsługiwać opcjonalne flagi np. do kompilowania testów na PC albo na HW. Ale to już temat na odrębny wpis.

Podsumowanie

Podczas prac nad systemami embedded bardzo ważnym czynnikiem jest sprzęt. Powinniśmy mieć go na uwadze podczas pisania kodu od samego początku projektu. Dzięki temu możemy szybko interweniować, jeśli znajdziemy jakieś problemy. Dlatego właśnie standardowy Test Driven Development musi zostać rozszerzony o dodatkowe kroki sprawdzające integracje kodu ze sprzętem.