CategoryUnit testy
Testowanie kodu, który nie wykorzystuje zewnętrznych zależności jest stosunkowo proste. W większości przypadków testowany moduł współpracuje jednak z innymi elementami systemu. Stawia to przed testami dwa wyzwania – po pierwsze powinny poprawnie działać, a po drugie sprawdzać poprawność tej współpracy. Nie jest to zadanie proste, a zewnętrzne zależności są jednym z głównych czynników utrudniających testowanie.
W ostatnim wpisie przybliżyłem zestaw dobrych praktyk w pisaniu unit testów. Dzisiaj będę kontynuować ten temat z trochę innej perspektywy i opowiem o antywzorcach. Dzięki charakterystycznym nazwom, piętnującym konkretne złe praktyki, antywzorce zostają w pamięci i mamy je przed oczami pisząc podejrzany kod.
Często unit testy nie są przez programistów traktowane jak prawdziwy kod. Są dla nich jedynie narzędziem do osiągnięcia określonego celu – sprawdzenia poprawności implementacji. Przez to testy stają się trudne w utrzymaniu albo wykonują się zbyt długo. Przez co uniemożliwiają pracę zgodnie z TDD i nie mają wartości dokumentacyjnej. Istnieją jednak proste zasady tłumaczące, jak powinny wyglądać dobrze napisane testy.
W poprzednich częściach cyklu skupiałem się na korzyściach płynących z TDD. Jeżeli ta metoda wejdzie nam w krew, te korzyści zachęcą nas, abyśmy pisali w ten sposób zawsze i wszędzie. Motywują nas do tego również eksperci mówiący, że każda linia kodu powinna być przetestowana. Okazuje się jednak, że nie zawsze testowanie wszystkiego na siłę jest dobrym rozwiązaniem. W tym artykule opiszę sytuacje, kiedy nie opłaca się używać TDD.
Próbując wprowadzić TDD w projekcie najczęściej spotkamy się z oporem. Argumenty przeciwko tej technice ze strony developerów i osób decyzyjnych, które nie miały z nią do czynienia często się powtarzają. Postanowiłem więc w tym wpisie zebrać te argumenty i je omówić. Krytyka TDD ze strony osób mających doświadczenie w temacie zwykle przybiera inną formę i jest to temat na osobny wpis.
Przestawienie się na Test Driven Development z pisania metodą tradycyjną nie jest łatwym zadaniem. Szczególnie na początku musimy walczyć ze starymi nawykami, a kiedy napotykamy trudności, naturalnym rozwiązaniem jest stosowanie metod, które znamy i rozumiemy. Poza tym początkowo TDD może nam się wydawać nieintuicyjne, a wkład pracy wydaje się większy. Jak to zwykle bywa w takich przypadkach, kluczem jest wytrwałość. Każda umiejętność wymaga czasu, aby ją dobrze opanować. Kiedy już nam się to uda, zauważymy, że praca w ten sposób przynosi nam pewne wymierne korzyści. W tym wpisie postaram się owe korzyści przybliżyć.
W poprzedniej części cyklu o TDD opisałem dlaczego sposób wytwarzania oprogramowania, który praktykowałem na początku się nie sprawdzał i co mnie skłoniło do zainteresowania się Test Driven Development. Dzisiaj opiszę jak wygląda praca zgodnie z TDD. Jak to często bywa w przypadku praktyk zwinnych zasady teoretyczne są dosyć proste, a kluczem do sukcesu jest dyscyplina.
Kiedy uczyłem się programować, pisałem metodą code and fix. Czyli najpierw pisałem jakiś fragment kodu – mogła to być jedna funkcja, moduł albo nawet cały program. Następnie uruchamiałem go i ręcznie sprawdzałem czy działa, przechodziłem kod debuggerem sprawdzając wartości zmiennych i przepływ sterowania. Następnie poprawiałem znalezione błędy, dodawałem funkcjonalności i znowu sprawdzałem. Na pewno każdy programista zaczynał w ten sposób.
W miarę jak moje umiejętności rosły i pisałem trudniejsze programy zauważałem coraz więcej problemów związanych z tą „metodologią”. Miałem jednak przeświadczenie, że robienie skomplikowanych aplikacji po prostu musi być skomplikowane. Tym bardziej, że na studiach (co prawda z Automatyki, a nie Informatyki) nikt się nawet nie zająknął o automatyzacji testów, o Test Driven Development nie wspominając. W kolejnych akapitach wytłumaczę, o jakie problemy mi chodziło.
Ostatnio w pracy miałem do wykonania dosyć proste zadanie. Chodziło o to, aby w przerwaniu od timera wykonującym się co 10 ms umieścić funkcję, która miała wykonywać się co 100 ms. W tym przerwaniu już wcześniej był wywoływany task co 10 ms i obsługa komunikacji. Napisany przeze mnie kod wyglądał mniej więcej tak:
void irq_timer_10ms(void) { static int32_t cnt = 0; task_comm(); task_10ms(); cnt++; if (cnt < 10) { cnt = 0; task_100ms(); } }
Kryje się tutaj dosyć często spotykany błąd – warunek w ifie jest odwrócony. Funkcja zamiast wywoływać się raz na 10, wykonuje się 9 razy na 10. Po dodaniu tej zmiany uruchomiłem aplikację i na pierwszy rzut oka wyglądało, że działa. Nie miałem do dyspozycji żadnych unit testów, ani dokładniejszych testów na docelowym hardware, więc wypushowałem tę zmianę razem z kilkoma innymi.
© 2023 ucgosu.pl
Theme by Anders Norén — Up ↑