Zabranie się za pisanie testów jednostkowych zajęło mi sporo czasu. Nigdy nie byłem fanem narzędzi wymagających skomplikowanej i długotrwałej konfiguracji. Pisanie Makefile’i też wydawało się być dla mnie karą. Ostatnio jednak pisząc kod do hobbistycznego projektu po raz pierwszy zauważyłem, że pisanie kodu bez testów utrudnia mi pracę i powoduje dyskomfort. Uczucie to było bodźcem do podzielenia się moją krótką drogą, na końcu której wykorzystywanie testów jednostkowych stało się wręcz podstawową potrzebą. A to wszystko za sprawą prostego narzędzia Ceedling. W tym pierwszym artykule nie zamierzam się rozpisywać na temat teorii i dobrych praktyk pisania testów. Na teorię przyjdzie jeszcze czas. Wiem, że mogę mieć tylko jedną szansę, by przykuć Twoją uwagę. Nie chcę zmarnować jej na przekazywanie suchej teorii, którą już gdzieś słyszałeś i przez którą artykuł spuchnie. Ty skończysz go czytać w połowie, a co gorsza nadal nie napiszesz żadnych testów. Zacznę więc od krótkiej motywacji i od razu przejdę do konfigurowania środowiska i pisania pierwszego testu. Możliwe, że dla Ciebie, tak jak dla mnie konfiguracja narzędzi wydawała się zawsze dużym problemem. Liczę, że jeżeli szybko pozbędziemy się tej przeszkody i za 30 minut będziesz pisał pierwszy test to nabierzesz wystarczającego rozpędu i wrócisz tutaj po więcej wiedzy praktycznej.
szybka motywacja
Dlaczego warto pisać testy jednostkowe? Bo popełniamy błędy. Każdy, bez wyjątku. Bo spędzamy potem godziny na ich debugowaniu. Bo zmieniamy lekko kod i potem nawet nie wiemy, że właśnie zepsuliśmy jakiś szczególny przypadek. Albo właśnie nie zmieniamy istniejącego kodu, ponieważ boimy się go zepsuć, co prawda nie da się na niego patrzeć, „ale działa to nie ruszajmy”. Słyszałem kiedyś i sam tak myślałem, że w embedded nie da się pisać testów jednostkowych. Bo sprzęt. Sprzęt jednak można ukryć pod warstwą abstrakcji (ang. HAL – Hardware Abstraction Layer) i na potrzeby testów „udawać”, że go mamy. Gdy testy jednostkowe mogą kontrolować zachowanie sprzętu, możemy szybko sprawdzić czy poprawnie odczytujemy ujemne wartości szerokości geograficznej z modułu GPS. Na pewno jest to szybszy sposób niż wybranie się na drugą półkulę. I niech jeszcze jedną, ostateczną motywacją będzie fakt, że piszemy w języku, w którym poniższe wyrażenie jest prawdą.
-1 > 1U
instalacja
Jako narzędzia do uruchamiania testów używam Ceedling’a (https://github.com/ThrowTheSwitch/Ceedling). Jest to konsolowy system budowania projektów w C. Narzędzie napisano w Ruby, jednak nie martw się, nie musisz znać tego języka. Oprócz wykorzystywania Rakefile (zamiast przyjemnego jak tropienie hardfaulta, formatu Makefile) do budowania projektów, Ceedling wykorzystuje Unity i CMock jako frameworki do testowania kodu w C. Instalacja i korzystanie jest bardzo proste. Unity zapewnia nam wszystkie makra testujące wyniki testów, a CMock załatwia za nas mockowanie („udawanie”) zależności od innych modułów (opiszę po co i jak z tego korzystać w następnym artykule). Początek instalacji różni się w zależności od tego, czy korzystamy z windowsa czy linuxa. Na windowsie potrzebna nam będzie jakieś środowisko do budowania, w którym możemy zainstalować Ruby. Ja używam MSYS2 (https://www.msys2.org/).
Po zainstalowaniu MSYS2 polecam zaktualizować bazę menadżera pakietów. W konsoli MSYS2 wpisujemy
pacman -Syu
wyłączamy terminal i włączamy ponownie, a następnie wpisujemy
pacman -Su
Mając dostęp do konsoli instalujemy Ruby
pacman -S ruby
na Ubuntu komenda będzie wyglądała w ten sposób
sudo apt-get install ruby
Gdy już zainstalowaliśmy Ruby, dalsze kroki powinny być niezależne od tego, na jakiej platformie pracujecie. Wykorzystując menadżer pakietów Ruby o nazwie gem instalujemy ceedlinga
gem install ceedling
W tym momencie Wasze środowisko może poinformować, że w zmiennej środowiskowej PATH nie ma katalogu z którym gem przechowuje zainstalowane aplikacje.
WARNING: You don't have /home/username/.gem/ruby/2.6.0/bin in your PATH,
gem executables will not run.
Successfully installed ceedling-0.29.1
Parsing documentation for ceedling-0.29.1
Done installing documentation for ceedling after 1 seconds
1 gem installed
W takim przypadku uruchomienie Ceedling będzie niemożliwe. Rozwiązaniem jest dodanie wymaganej ścieżki do PATH. Bez strachu, w MSYS2 wykonaj poniższe dwie komendy i wszystko powinno działać. Zwróć uwagę, aby ewentualnie podmienić wersję Ruby w ścieżce. W momencie pisania artykułu był to numerek 2.6.0, ale w przyszłości może się to zmienić. Numer wersji, który powinien być w ścieżce zawiera się w treści ostrzeżenia przy instalacji Ceedlinga.
echo "export PATH=\$PATH:~/.gem/ruby/2.6.0/bin" >> ~/.bashrc
. ~/.bashrc
Jeżeli wszystko się udało to komenda
ceedling new --docs hello_world
utworzy katalog hello_world a w nim całą strukturę folderów i wszystko, co jest potrzebne do pisania testów. Komendę uruchom tam gdzie chcesz aby powstał projekt. Jeżeli zrobisz to w domyślnej lokalizacji po otwarciu MSYS2 (czyli w katalogu domowym ~) to z poziomu Windowsa projektu hello_world możesz szukać tam gdzie zainstalowałeś MSYS2. U mnie jest to
C:\msys64\home\hubert.melchert\hello_world
Podanie przy tworzeniu projektu opcji --docs
spowoduje, że w podkatalogu vendor/ceedling/docs
zostanie umieszczona dokumentacja pomocna przy pracy z ceedlingiem. Aby sprawdzić czy ostatecznie wszystko działa wchodzimy do katalogu hello_world (cd hello_world)
i uruchamiamy ceedling test:all. Jest to komenda rozkazująca Ceedlingowi uruchomienie wszystkich testów. Z racji, że nie napisaliśmy jeszcze żadnych testów powinniśmy dostać taki wynik.
$ ceedling test:all
--------------------
OVERALL TEST SUMMARY
--------------------
No tests executed.
W tym momencie może się okazać, że Ceedling zarzuci nas błędami związanymi z brakiem kompilatora GCC w zmiennej PATH. Najprostszym rozwiązaniem będzie wykorzystanie menedżera pakietów do zainstalowania GCC. W MSYS2 uruchamiamy komendę
pacman -S gcc
a na Ubuntu
sudo apt-get install gcc
I to tyle w temacie instalacji. W tym momencie mamy gotową podstawową konfigurację pozwalającą na kompilacje testów dla wielu modułów bez konieczności użerania się z Makefile’ami. Od teraz możemy już korzystać z bardzo przyjemnego w obsłudze mockowania zależności. Dodając minimalny wysiłek (doinstalowanie modułu do pythona) dostaniemy raporty z pokrycia kodu takie jak np.
pierwszy test
Ceedling sam znajduje pliki z testami i zarządza ich budowaniem oraz dostarczaniem mockowanych modułów. Plików szuka w podkatalogu test i zakłada, że mają przedrostek test_. Dalsza nazwa pliku test_*.c musi być taka sama, jak moduł, który ten plik ma testować. Na potrzeby prostego przykładu napiszemy i przetestujemy funkcję wyliczającą potęgę. Moduł można stworzyć na dwa sposoby. Albo zrzucamy tę robotę na Ceedlinga i uruchamiamy komendę
$ ceedling module:create[power]
File src/power.c created
File src/power.h created
File test/test_power.c created
Generate Complete
albo ręcznie w podkatalogu src/ tworzymy pliki power.c i power.h, a w podkatalogu test/ tworzymy plik test_power.c. Niezależnie od wybranej metody struktura naszego katalogu hello_world powinna wyglądać w ten sposób
$ tree -r
.
├── test
│ ├── test_power.c
│ └── support
├── src
│ ├── power.h
│ └── power.c
├── project.yml
└── build
Brakujący program tree możemy doinstalować poprzez pacman -S tree
. Jeżeli moduł stworzyliśmy za pomocą komendy ceedlinga to od razu dostaniemy przygotowane niezbędne include’y.
#ifndef POWER_H
#define POWER_H
#endif // POWER_H
#include "power.h"
#include "unity.h"
#include "power.h"
void setUp(void)
{
}
void tearDown(void)
{
}
void test_power_NeedToImplement(void)
{
TEST_IGNORE_MESSAGE("Need to Implement power");
}
Jeżeli pliki tworzyłeś ręcznie, to teraz ręcznie zapewnij odpowiednie zależności. Funkcje setUp i tearDown w test_power.c generuje Ceedling, ale nie są nam teraz potrzebne, więc pominę je w dalszej części. Funkcja test_power_NeedToImplement jest przykładowym testem stworzonym przez Ceedlinga. Wszystkie funkcje testujące muszą mieć przedrostek test_, aby system budowania testów mógł je znaleźć. Już teraz możemy uruchomić zbudowanie i uruchomienie testów
$ ceedling test:power
Test 'test_power.c'
-------------------
Generating runner for test_power.c...
Compiling test_power_runner.c...
Compiling test_power.c...
Compiling power.c...
Linking test_power.out...
Running test_power.out...
--------------------
IGNORED TEST SUMMARY
--------------------
[test_power.c]
Test: test_power_NeedToImplement
At line (15): "Need to Implement power"
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 1
PASSED: 0
FAILED: 0
IGNORED: 1
Jeżeli wszystko zadziałało poprawnie to efekcie dostajemy proste w interpretacji wyniki testów. W podsumowaniu („OVERALL TEST SUMMARY”) widzimy, że uruchomiono łącznie jeden test, z czego jeden zignorowano. Powyżej dokładnie widzimy, że zignorowany test jest w linii 15 pliku test_power.c. Już teraz, uzbrojony w listę dostępnych w Unity asercji powinieneś być w stanie kontynuować pisanie testów na własną rękę. Dokumentację asercji możesz znaleźć w repozytorium Unity: https://github.com/ThrowTheSwitch/Unity/ . Można też stworzyć projekt z opcją –docs.
$ ceedling new --docs hello_world
Welcome to Ceedling!
create hello_world/vendor/ceedling/docs/CeedlingPacket.md
create hello_world/vendor/ceedling/docs/CException.md
create hello_world/vendor/ceedling/docs/CMock_Summary.md
create hello_world/vendor/ceedling/docs/UnityAssertionsCheatSheetSuitableforPrintingandPossiblyFraming.pdf
create hello_world/vendor/ceedling/docs/UnityAssertionsReference.md
create hello_world/vendor/ceedling/docs/UnityConfigurationGuide.md
create hello_world/vendor/ceedling/docs/UnityGettingStartedGuide.md
create hello_world/vendor/ceedling/docs/UnityHelperScriptsGuide.md
create hello_world/vendor/ceedling/docs/ThrowTheSwitchCodingStandard.md
create hello_world/project.yml
Dzięki tej opcji w podkatalogu vendor/ceedling/docs znajdziemy pliki w formacie Markdown z niezbędną dokumentacją. Ja osobiście wolę to rozwiązanie. Napiszmy teraz jakiś faktyczny test.
#include "power.h"
uint32_t POWER_power(uint32_t base, uint32_t exponent)
{
return 0;
}
#ifndef POWER_H
#define POWER_H
#include <stdint.h>
uint32_t POWER_power(uint32_t base, uint32_t exponent);
#endif // POWER_H
#include "unity.h"
#include "power.h"
void test_POWER_power_should_Return1_for_BaseGreaterThanZeroAndExponentZero(void)
{
TEST_ASSERT_EQUAL(1, POWER_power(1, 0));
}
Tym razem makro ignorujące test zostało zastąpione odpowiednią asercją. Nazwa funkcji stara się oddać to, jakie są warunki testu i czego oczekujemy jako efektu. Uruchamiamy testy
$ ceedling test:power
Test 'test_power.c'
-------------------
Compiling power.c...
Linking test_power.out...
Running test_power.out...
-------------------
FAILED TEST SUMMARY
-------------------
[test_power.c]
Test: test_POWER_power_should_Return1_for_BaseGreaterThanZeroAndExponentZero
At line (7): "Expected 1 Was 0"
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 1
PASSED: 0
FAILED: 1
IGNORED: 0
---------------------
BUILD FAILURE SUMMARY
---------------------
Unit test failures.
Wiemy wszystko, co wiedzieć musimy. 1 do potęgi 0 powinno dać w efekcie 1, a wywołanie POWER_power(1, 0) zwróciło 0. Nic dziwnego, zważywszy na ciało tej funkcji. Poprawmy ją trochę.
uint32_t POWER_power(uint32_t base, uint32_t exponent)
{
return 1;
}
$ ceedling test:power
Test 'test_power.c'
-------------------
Compiling power.c...
Linking test_power.out...
Running test_power.out...
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 1
PASSED: 1
FAILED: 0
IGNORED: 0
Oszukaliśmy samych siebie, ale kod przechodzi testy. Czas napisać kolejny test, który dowiedzie niepoprawności implementacji funkcji POWER_power. Taka metodologia pisania zawodzących testów i dopisywania minimalnej ilości kodu, aby zapewnić działanie testów nazywa się Test Driven Development (TDD). Zagadnienie na pewno warte zgłębienia, ale to temat na osobny artykuł. Kontynuując rozwój naszej funkcji dopisujemy prosty test.
void test_POWER_power_should_Return8_for_Base2AndExponent3(void)
{
TEST_ASSERT_EQUAL(8, POWER_power(2, 3));
}
$ ceedling test:power
Test 'test_power.c'
-------------------
Compiling power.c...
Linking test_power.out...
Running test_power.out...
-------------------
FAILED TEST SUMMARY
-------------------
[test_power.c]
Test: test_POWER_power_should_Return8_for_Base2AndExponent3
At line (12): "Expected 8 Was 1"
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 2
PASSED: 1
FAILED: 1
IGNORED: 0
---------------------
BUILD FAILURE SUMMARY
---------------------
W tym momencie łatwiej już napisać poprawną funkcję zamiast oszukiwać. Miłej zabawy! Jeżeli potrzebujesz większego placu zabaw możesz pobrać moje repozytorium z użytym w poprzednim artykule buforem cyklicznym i tam uruchomić testy, popsuć kod i zobaczyć czy testy to wyłapią.
https://bitbucket.org/personalmechatronics/circular_buffer
dokąd zmierzamy
Zdaje sobie sprawę, że powyższy przykład jest mało życiowy. W praktyce testowane moduły są zależne od innych modułów, a najczęściej także od sprzętu. Rozwiązaniem tych problemów jest mockowanie zależności z wykorzystaniem CMock. Jak to robić pokażę w następnym artykule na przykładzie modułu GPS czytającego znaki z UART. Nie jest to wcale trudne, a daje mnóstwo możliwości. Przy okazji pokażę, jak prosto jest używać Ceedlinga w projekcie stworzonym i rozwijanym w środowisku takim, jak Atollic True Studio dla STM32. W tej konfiguracji projekt będziemy kompilować i wgrywać do mikrokontrolera z poziomu wygodnego IDE. Jednocześnie do testów nadal będziemy używać Ceedling’a.
Dzisiaj chciałem Ci pokazać, że instalacja środowiska do testów i ich uruchamianie nie musi być skomplikowane. W zasadzie można to zrobić w pół godziny, ucząc się przy tym raptem pięciu komend.
ogłoszenia
Zachęcam do polubienia mojego fanpage’a i/lub zapisania się na mój newsletter.
W obu tych miejscach informuję o nowych artykułach, więc nie przegapisz kolejnego odcinka serii o testach jednostkowych. Przy okazji czytelników z Gdańska i okolic oraz tych nadzwyczaj mobilnych zapraszam na kolejną edycję Gdańsk Embedded Meetup. Jak zwykle ciekawe tematy, darmowa pizza i losowanie płytek ewaluacyjnych. Zarezerwuj sobie wieczór 4 lutego 2020. Zapisz się na stronie wydarzenia, aby wziąć udział w losowaniu nagród w trakcie spotkania. Do zobaczenia!
Hubert Melchert
embedcode.pl
#5 Spotkanie Gdańsk Embedded Meetup
Piąte spotkanie gdańskiego meetupu embedded odbędzie się we wtorek 4 lutego o godzinie 18.00. Miejscem spotkania będzie Inkubator STARTER, ul. Lęborska 3B. Po prawej stronie znajdziecie mapkę i instrukcję dojazdu. Miejsce jest przyjazne dla kierowców – w porze meetupu parking jest darmowy i tuż obok wejścia.