Download as pdf or txt
Download as pdf or txt
You are on page 1of 23

JAK ZACZĄĆ Z

TDD
W SYSTEMACH
EMBEDDED?
Po co nam TDD?

Najlepiej będzie to zilustrować na przykładzie.

Pracujemy nad urządzeniem sterującym klimatyzacją w biurowcu korporacyjnym.


Nasze urządzenie podaje sygnały na wentylatory, nagrzewnice, chłodnice i
przepustnice powietrza. Odczytuje również dane z czujników temperatury i
wilgotności. Do tego komunikuje się z panelami użytkownika znajdującymi się w
odpowiednich pomieszczeniach oraz z nadrzędnym systemem zarządzania
budynkiem.

Nasz system otrzymuje wartości zadane temperatury w różnych miejscach


budynku i stara się w taki sposób sterować klimatyzacją, aby je utrzymać.
Schemat sterowanego systemu przedstawia poniższy schemat.

2 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Naszym zadaniem jest implementacja programu nocnego. Chodzi o to, że w
godzinach pracy temperatura powinna być utrzymywana na poziomie
komfortowym dla ludzi przebywających w budynku. Wieczorem ludzie kończą
pracę i w budynku prawie nikogo nie ma. Możemy wtedy aktywować tryb nocny
utrzymujący niższą temperaturę i wyłączający część urządzeń wykonawczych
albo ustawiających je do pracy z obniżonym zużyciem energii.

Jeżeli tryb nocny jest aktywny, nasze urządzenie musi go aktywować o ustalonej
godzinie. Następnie po przekroczeniu godziny końca trybu nocnego - musi
aktywować z powrotem tryb dzienny. Tryb nocny możemy ustawiać oddzielnie na
dni pracy i weekendy. Szczegóły dotyczące działania trybu nocnego i w jaki
sposób zmienia działanie urządzeń wykonawczych są opisane w dokumentacji i w
wymaganiach projektowych.

6:00 22:00 6:00

3 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Jak się zabierzemy do realizacji zadania?
Pewnie tak:
1. Napiszemy kod implementujący nową funkcję.

2. Uruchomimy program na sprzęcie z podłączonymi wszystkimi czujnikami,


wentylatorami, nagrzewnicami itp. i zobaczymy, czy działa.

3. Poprawność będziemy sprawdzać krokowo na debugu. Nasz debugowy


program uruchamia sztucznie tryb nocny (w końcu nie będziemy czekać
z testem do nocy aż się odpali). Następnie w debugerze sprawdzimy,
czy wszystkie funkcje od poszczególnych urządzeń są poprawnie odpalane.

4. Na początku nasza implementacja będzie zawierać błędy. Dlatego


czynność będziemy wielokrotnie powtarzać za każdym razem zmieniając
nieco kod.

5. W końcu uznamy, że działa i weźmiemy się za testowanie wychodzenia z


trybu nocnego.

6. Potem czekają nas kolejne takie uruchomienia przy każdej zmianie


wymagań,czy kiedy nowe funkcje będą ingerować w tryb nocny.

Wiem to z autopsji. Kiedyś sam pracowałem


w takim projekcie. I wiesz co w tej metodzie
było najgorsze?

Mieliśmy w biurze taki wielki wentylator


dachowy i przy każdym uruchomieniu
zaczynał tak huczeć, że nikt nie słyszał
swoich myśli. W ten sposób każdy mój test
na sprzęcie paraliżował na kilka minut pracę
wszystkich osób w pokoju :)

4 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Dalsze kroki

No ale jak się bliżej zastanowimy - możemy wprowadzić usprawnienia.

Tak naprawdę pierwsze usprawnienie już zostało wprowadzone w przykładzie. Nie


czekamy na zadaną godzinę, tylko tryb nocny wywołujemy sztucznie. Możemy
powiedzieć, że nasz test symuluje nadejście nocy i jej koniec. Albo, że mockujemy
eventy startu i końca trybu nocnego.

zmiana
ustawień
urządzeń
wykonawczych
wykrycie
Nagrzewnice
aktywacja
trybu
trybu
update nocnego
nocnego
Data i Chłodnice
RTC Zarządzane Algorytm
godzina

Wentylatory

Ale możemy iść dużo dalej. Jak się zastanowimy nad zadaniami wykonywanymi
przez nasz program, możemy je podzielić na dwie kategorie:

Interakcje ze sprzętem - obsługa zegara RTC (Real Time Clock),


wentylatorów, nagrzewnic, czujników itd

Wewnętrzna logika aplikacji - przyjęcie eventu, zmiana stanu na tryb nocny,


wydanie poleceń do urządzeń wykonawczych, oczekiwanie na event wyjścia z
trybu nocnego

Pomoże nam w tym stworzenie diagramu systemu.

5 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Zarządzanie

Pomiary Panele
Algorytm sterowania BMS
i nastawy użytkowania

Czujnik
Nagrzewnica Wentylatory Filtry BACnet
wilgotności

Nagrzewnica Czujniki Data i


Chłodnica Przepustnice Modbus
wstępna temperatury godzina

granica
HW

Coil
Out 0 - 10V

DIN

In 0 - 10V

PWM SPI RTC UART2

ADC GPIO UART1

6 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Testy integracyjne i testy jednostkowe

Możemy więc zaimplementować moduły obsługujące sprzęt i przetestować je


niezależnie. Te moduły będą miały funkcje API wykorzystywane później w naszej
procedurze trybu nocnego - włączenie nagrzewnicy na zadaną moc, ustawienie
obrotów na wentylatorze itp.

Logikę aplikacji również możemy podzielić na moduły i przetestować niezależnie.


Aktywacja trybu nocnego, wyłączenie nagrzewnicy itp. to tylko funkcje. Jeżeli ich
implementacja jest przetestowana gdzie indziej - możemy się zatrzymać w
momencie jej wywołania. Zakładamy, że poprawna realizacja funkcji to
odpowiedzialność innego modułu.

TEST INTEGRACYJNY
zmiana
ustawień
urządzeń
wykonawczych
wykrycie
Nagrzewnice
aktywacja
trybu
trybu
update nocnego
nocnego
Data i Chłodnice
RTC Zarządzane Algorytm
godzina

Wentylatory

To, co robimy można nazwać testem integracyjnym. Sprawdzamy konkretną


funkcję systemu i nasz test powoduje przejście przez różne moduły.

7 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Z kolei test jednostkowy stawia granice na wejściach/wyjściach modułu i testuje go
w odosobnieniu od reszty systemu. Dzięki temu łatwiej nam sprawdzić wszystkie
scenariusze i mamy mniej podejrzanych podczas poszukiwania błędów.

TEST JEDNOSTKOWY

Zastępcza Zastępcza
implementacja na Kod produkcyjny implementacja na
potrzeby testów potrzeby testów

Nagrzewnice

Zarządzane Algorytm Chłodnice

Wentylatory

Ale jest jeden haczyk! Docelowo aplikacja ma działać na sprzęcie. Dlatego do


końca nie uciekniemy od huczącego wentylatora. Musimy przetestować moduł
sprzętowy, który nim bezpośrednio steruje. Wentylator będzie nam również
potrzebny do testów integracyjnych. Na szczęście wtedy powinno pójść dużo
sprawniej.

Dlaczego? Bo wszystkie błędy takie jak zła obsługa wartości granicznych,


nieobsłużony null pointer, błędny warunek w ifie itp. wyłapiemy wcześniej na
testach jednostkowych. Na testach integracyjnych skupiamy się tylko na tym,
czego unit testy nie są w stanie wyłapać - interakcje między modułami, interakcje
ze sprzętem, współbieżność itp.

To ogromna oszczędność czasu i pieniędzy!

8 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Testy jednostkowe Testy integracyjne

Mały fragment kodu w Współpracę modułów w


Co testuje
izolacji wykonaniu zadania

pojedynczy test pojedynczy test kilka


Szybkość wykonania milisekundy, cały zestaw sekund, cały zestaw kilka
kilka sekund minut

Na docelowym sprzęcie Nie Tak

Kiedy wykonywane Podczas implementacji Po implementacji

Błędy logiczne, błędy we


Błędy logiczne w
Jakie błędy znajduje współpracy modułów,
implementacji
kwestie sprzętowe

9 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


koszty

Development Unit Testy Testy na produkcji czas


Testy integracyjne systemowe

Właśnie z powodu minimalizacji kosztów chcemy wykrywać błędy w jak


najwcześniejszej fazie developmentu. Dlatego dążymy do testowania jak
największej części systemu przy pomocy tanich i szybkich testów jednostkowych.
Im testy są dłuższe i wymagają więcej sprzętu, tym rzadziej będziemy je
wykonywać. Dlatego zwykle chcemy, aby proporcje między ilością testów
jednostkowych, integracyjnych i systemowych układały się w kształt piramidy.

koszty
czas

TESTY
SYSTEMOWE

TESTY INTEGRACYJNE

UNIT TESTY

ilość
testów

10 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Wnioski

Możemy zauważyć, że problemy napotykane na bieżąco w projekcie skłoniły nas


do przemyślenia sposobu testowania. Mamy testy pojedynczych modułów i testy
integracyjne skupiające się na współpracy różnych modułów.

Nie dodaliśmy tych testów żeby były, ani dlatego, że ktoś nam kazał. W ten
sposób rozwiązujemy konkretne problemy projektowe. Nawet jeżeli nie mamy
żadnej strategii testowania, tworzymy taką nieformalną strategię, bo to nam po
prostu ułatwia życie.

Możemy więc ułatwić sobie życie na kilka sposobów:

testujemy mniejsze fragmenty kodu i potem składamy je w całość

testy ze sprzętem robimy tylko tam, gdzie musimy

możemy w danym momencie testować tylko to, czego potrzebujemy

oszczędzamy czas

ograniczamy zużycie sprzętu, koszty energii, zmniejszamy ryzyko awarii

Poza tym jak widzieliśmy przed chwilą - testy idą w parze z dobrym projektem
systemu. Zależy nam na odpowiednim podziale systemu na moduły, czy na
stworzeniu odpowiednich funkcji API. To nam ułatwia późniejszą pracę.

Zamiast odkrywać za każdym razem koło na nowo programiści zauważyli


powtarzające się prawidłowości i metody pomagające w różnych projektach.
Następnie je dokładniej opisali i zbadali. Okazało się, że czasem lepiej najpierw
pomyśleć w jaki sposób chcemy przetestować dane zachowanie, a dopiero potem
dopisać implementację.

W ten sposób powstało Test Driven Development (TDD).

11 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Podstawy TDD

Jak pokazałem w poprzednim rozdziale - Test Driven Development powstało jako


odpowiedź na typowe problemy programistów powtarzające się w wielu różnych
projektach:

Nie możemy uciec od testowania - choćby nieformalnego i ręcznego

Testy są czasochłonne i wymagają powtarzania tych samych czynności

Testy na sprzęcie wymagają więcej czasu, generują dodatkowe koszty i


mogą być trudne do przeprowadzenia

Nieformalne testy usuwamy, kiedy program działa. Potem przychodzą


nowe funkcje, zmiany w projekcie i od nowa robimy te same testy.

Skoro musimy testować nasze systemy - lepiej róbmy to z głową.

1. W TDD robimy testy automatyczne - piszemy scenariusz testowy, a


następnie możemy go wywołać wiele razy i wynik dostajemy od razu.

2. Testujemy małe fragmenty systemu w izolacji - robimy testy jednostkowe


(unit testy). Dzięki temu łatwiej napisać scenariusz testowy, sprawdzić
wyniki i znaleźć przyczyny ewentualnych błędów.

3. Musimy później dodać testy integracyjne, żeby sprawdzić czy interakcje


między modułami nie powodują dodatkowych problemów.

4. Piszemy test przed implementacją, co zmusza nas do myślenia jak


potwierdzić, że dane wymaganie jest spełnione.

5. Mając testy sprawdzające zachowanie modułu możemy bezpiecznie


przeprowadzić refactoring.

To tak w skrócie - teraz zastanówmy się jakie są konsekwencje takiego podejścia.

12 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Dual targeting

Test powinien odbywać się automatycznie, zwracać od razu wynik i testować mały
fragment kodu w odosobnieniu. Nie jesteśmy w stanie tego zrealizować
wprowadzając małe modyfikacje w docelowym programie.

Scenariusze testowe są wykonywane w oddzielnie


kompilowanych programach testowych.

Nasz projekt zawiera więc docelową aplikację i wiele pomniejszych programów


testowych. Ale to nie wszystko!

W systemach embedded unit testów najczęściej


nie odpalamy na docelowym sprzęcie!

Dlaczego? Dla wygody.


Testy mają dawać szybko wynik. A wgranie programu na sprzęt, uruchomienie,
przechwycenie wyjścia z konsoli debugowej - to wszystko zajmuje czas.

Czasem w ogóle nie mamy docelowego sprzętu, a jesteśmy w stanie rozwijać kod
używając unit testy. Jak to możliwe?

Testy jednostkowe testują logikę aplikacji. Odcięliśmy się w nich od sprzętu.


Jedynie symulujemy interakcję ze sprzętem przy pomocy mocków. A język
programowania zostaje taki sam. Nie ważne, czy piszemy w C, C++, czy innym
języku. Błędny warunek w ifie, argument spoza zakresu, przepełnienie tablicy, null
pointer i inne typowe błędy wykryjemy tak samo na maszynie developerskiej jak
na docelowym procesorze.

Wykorzystujemy więc dual targeting - nasz kod jest uruchamiany na różnych


procesorach. Jest to kolejny czynnik, który dopinguje nas do dbania o granice
między modułami, funkcje API i dobry projekt systemu.

13 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Test First

Kolejnym nieintuicyjnym aspektem TDD jest pisanie testu przed implementacją.


Na początku wydaje nam się to bez sensu. Po co testować coś, czego nie ma?

Ale okazuje się, że w ten sposób osiągamy wymierne korzyści.

W swojej praktyce zawodowej niezliczoną ilość razy podczas pisania testu


znajdowałem nieścisłości w wymaganiach. Normalnie trafiłbym na nie w połowie
implementacji i albo bym wybrał rozwiązanie, które mi pasuje (niekoniecznie
pasuje też twórcy wymagania), albo bym czytał dziesięć razy dokumentację i
próbował wywnioskować co program ma robić.

W końcu i tak zawsze trzeba doprecyzować wymagania. Lepiej to zrobić od razu i


nie tracić czasu.

Poza tym jeżeli zaczniemy od testu, a potem dodamy implementację - mamy


pewność, że w kodzie jest tylko to, co ma być i nic więcej. Jeżeli potem będziemy
chcieli zmienić implementację - testy pokażą nam, czy nowa wersja robi to samo.

Nie musimy się zastanawiać, czy ta linijka jest ważna. Nie ma na nią testu,
więc nie jest!

14 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Mikrocykl TDD

Znamy już myśl jaka stoi za TDD - możemy więc przejść do szczegółów. Aby
pisać kod zgodnie z Test Driven Development wykonujemy w kółko trzy proste
kroki:

Faza Czerwona - dopisujemy minimalną ilość testów wystarczającą, aby nasz


kod nie przechodził testów. Zwykle wtedy raport z testu świeci się na czerwono
- stąd nazwa.

Faza Zielona - dodajemy minimalną ilość kodu produkcyjnego, aby testy


zaczęły przechodzić. Wtedy raport będzie pokolorowany na zielono.

Faza Refactor - mając przechodzące testy sprawdzamy, czy możemy


poprawić implementację kodu produkcyjnego albo scenariuszy testowych.

I tak w kółko :)

Pojedynczy test
Budujemy projekt

Mały refactoring Mała zmiana w kodzie


budujemy projekt Budujemy projekt

15 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


TDD jest na początku nieintuicyjne

O fazie czerwonej już sobie powiedzieliśmy, o refactoringu również, ale faza


zielona też jest nieintuicyjna. Szczególnie na początku. A to dlatego, że mamy w
niej napisać MINIMALNĄ ilość kodu produkcyjnego, żeby testy przechodziły.

A ręka mnie świerzbi, żeby wybiec do przodu. Przecież wiem, że docelowo tam
będzie tablica, a nie zmienna. W głowie już widzę, jak będzie wyglądać logika
programu. Więc dlaczego mam tracić czas na bezsensowną implementację, którą i
tak potem zmienię?

Ano dlatego, żeby najpierw napisać test, który failuje. Jak napiszesz całą
implementację od razu, to nie wiesz, czy testy wyłapią ewentualny błąd. Możesz
też usunąć za dużo przy refactoringu. Najprostsza implementacja jest najlepsza
dopóki nie napiszesz testu, który ją obali.

Czasem w ogóle okazuje się, że ta prosta implementacja jednak jest


wystarczająca. Piszesz kolejne testy i nie możesz wymyślić przypadku, który
wykaże błąd. Wtedy zostawiasz prostą implementację i cieszysz się, że będziesz
mieć łatwiejszy kod do utrzymania.

16 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


No i to tyle teorii :)

Tak naprawdę nie musisz wiedzieć nic więcej, żeby zacząć. Całą resztę nabywasz
z praktyką. Musisz ćwiczyć i pilnować, żeby trzymać się cyklu TDD i wyrabiać
sobie nowe nawyki.

Pojedynczy cykl Red - Green - Refactor jest bardzo szybki.

Dopisujesz test, kompilujesz, uruchamiasz.

Dodajesz implementację (najczęściej w momencie pisania testu już wiesz


gdzie), kompilujesz, uruchamiasz

Sprawdzasz, czy możesz coś poprawić i jeśli tak - kompilujesz i uruchamiasz.

Pojedynczą iterację możesz zrobić w 1-2 minuty. Tylko czasem się zatniesz na
dłużej na jakimś nieprzechodzącym teście albo na większym refactorze.

Aby zacząć z TDD w praktyce potrzebujesz jeszcze jednej rzeczy - frameworka


testowego.

17 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Jaki framework testowy wybrać?

To jedno z najczęściej zadawanych pytań dotyczących TDD.

Jest w nim dobra intencja. W końcu żeby się nauczyć potrzebujemy praktyki.
Dlatego znając polecany framework, możemy po prostu wziąć się do roboty. Ale
jest też druga strona medalu!

Na wybór idealnego frameworka możesz stracić ogromną ilość czasu. W ten


sposób nie uczysz się tego, co naprawdę ważne - jaka idea stoi za TDD, jak
poprawnie stosować tę technikę, kiedy ułatwia życie, a kiedy się nie sprawdzi.
Dlatego nie daj się rozproszyć przez ogrom opcji. Tym bardziej, że wszystkie
frameworki są tak naprawdę takie same.

Co to oznacza? Że każdy z nich tworzy scenariusze testowe, sprawdza warunki


przejścia testu, dzieli testy na grupy, uruchamia je, wyświetla wyniki itp.

Mamy też frameworki do mocków, czyli do symulowania interakcji z innymi


częściami aplikacji i ze światem zewnętrznym. Tutaj możemy się zdecydować na
pisanie własnych mocków (wystarczające w 99% przypadków), proste biblioteki
albo frameworki mające wszystkie możliwe opcje.

Moje rekomendacje znajdziesz w tabelce.

Unity Test Framework + FFF (Fake Function


Na start
Framework)

Docelowy do projektu googletest + googlemock (oba są w jednym repo)

Na później, jak chcesz poznać


Catch2 + googlemock
inne podejście

18 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Konfiguracja frameworka

Konfiguracja narzędzi często powoduje pewne komplikacje. Tym bardziej, że


każdy z nas ma własne preferencje co do używanego edytora kodu, sposobu
budowania projektu, kompilatora itp. Dlatego nie jestem w stanie przedstawić Ci
instrukcji krok po kroku dla każdego możliwego setupu.

Na szczęście przygotowanie projektu do nauki TDD z użyciem Unity i FFF jest


bardzo proste.

Pamiętamy, że unit testy zawsze chcemy uruchamiać na maszynie developera.


Niezależnie, czy docelowa aplikacja działa na PC-cie, mikrokontrolerze, Raspberry
Pi, czy czymkolwiek innym. Na początek musimy mieć prosty projekt z samym
plikiem main, który się poprawnie kompiluje.

Następnie z biblioteki Unity dodajemy 3 pliki zawierające implementację


frameworka testowego:

19 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Będziemy potrzebować również zawartości folderu fixture, co pozwoli nam na
łączenie testów w grupy:

Z kolei biblioteka FFF składa się tylko z jednego pliku:

Mając te pliki w projekcie musimy jeszcze zmodyfikować plik main oraz dodać pliki
obsługujące naszą grupę testową oraz scenariusze testowe.

Szczegóły znajdziesz na moim blogu:


https://ucgosu.pl/2018/04/unity-framework-testowy-w-c/

20 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Jak się uczyć TDD?

Przez praktykę! Nie ma nic złego w czytaniu o TDD i oglądaniu przykładów na


YouTube. Ale to nie wystarczy. Programowanie to umiejętność praktyczna i
pewnych rzeczy nie nauczysz się samym czytaniem. Dlatego:

Aby nauczyć się programować, musisz programować!

~ Mateusz Kupilas

Albo ujmując to samo w bardziej wysublimowany sposób:

There are two parts to learning craftsmanship: knowledge and work. You
must gain the knowledge of principles, patterns, practices, and heuristics
that a craftsman knows, and you must also grind that knowledge into your
fingers, eyes, and gut by working hard and practicing.

~ Robert Martin

Zadania dobre do pierwszych prób z TDD znajdziesz tutaj:


https://kata-log.rocks/starter.html

21 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Dodatkowe materiały
Jeżeli chcesz dowiedzieć się więcej o TDD i jego zastosowaniu w embedded -
tutaj znajdziesz moje rekomendacje dotyczące blogów, książek i filmów na
YouTube.

Na początek zachęcam do odwiedzenia mojego bloga, gdzie znajdziesz sporo


artykułów o TDD w embedded.

Nagrałem też kilka Live’ów na YouTube - zobaczysz tutaj dwa przykłady


wykorzystujące TDD i konfigurację środowiska:
- część 1
- część 2

Polecam również artykuł na stronie Embedded Artistry o tym jak TDD pomaga
poprawiać błędy mimo, że nie mamy dostępu do docelowego hardware’u.

Na blogu Electron Vector także znajdziesz ciekawe artykuły o TDD.

Przykładowy projekt wykorzystujący TDD - komputer pokładowy satelity:


https://github.com/PW-Sat2/PWSat2OBC

22 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?


Dodatkowe materiały
W temacie TDD Embedded lekturą obowiązkową jest książka Jamesa Grenninga:

23 JAK ZACZĄĆ Z TDD W SYSTEMACH EMBEDDED?

You might also like