Kod, który jest używany, musi być też rozwijany. Świat idzie do przodu, potrzebne są nowe funkcjonalności, na które nie było zapotrzebowania w momencie projektowania. Inne funkcjonalności wymagają przeróbek ze względu na wymagania użytkowników, konieczność poprawienia wydajności albo bezpieczeństwa, czy wreszcie z powodu starzenia się technologii, w której zostały napisane oryginalnie.
Bardzo często okazuje się, że rozwojem zajmą się zupełnie inni ludzie niż ci, którzy zaprojektowali oryginalny kod. Owszem, jest dokumentacja, mogą być komentarze tu i ówdzie, dobrze napisany kod jest też zwykle czytelny, ale nic tak nie pomaga go zrozumieć, jak przykłady jego użycia. Testy są właśnie takimi przykładami, jasno pokazują czego należy się spodziewać i w jakich granicach można przeprowadzić refaktoryzację kodu, aby nie "zepsuć czegoś gdzie indziej".
Dlaczego testować w Pythonie?
- Testowanie jest bardzo ważnym procesem. Pozwala zapewnić jakość i niezawodność aplikacji, co z kolei daje pewność, że aplikacja działa zgodnie z wymaganiami i spełnia oczekiwania użytkowników. Dzięki testowaniu można wykryć błędy w kodzie szybciej, co pozwala uniknąć kosztownych błędów na środowisku produkcyjnym - zapewnia Bartosz Woźniak, mentor w Kodilli i DevOps w Tivix.
Ale dlaczego mamy testować właśnie w Pythonie? Jak podkreśla nasz mentor, ten język oferuje wiele możliwości w zakresie testowania.
- Istnieje wiele frameworków testowych wspieranych przez Pythona: selenium, pytest, unittest, robot, nose2, testify, doctest, testproject, behave, lettuce. Każdy charakteryzuje się innym podejściem do testowania - od TDD (Test-Driven Development), po BDD (Behaviour-Driven Development), przez ATDD (Acceptance Test Driven Development). Zależnie od obranej strategii testowania, można zaimplementować wybrane podejście w różnych frameworkach.
Ponadto Python charakteryzuje się prostotą i czytelnością, dzięki której można pisać auto-dokumentujący się kod. Na przykład, implementując strukturę bazy danych, otrzymujemy wymaganie, że email powinien być unikalny i dzięki niemu powinno dać się jasno zweryfikować użytkownika. W oparciu o doprecyzowane wymagania, można napisać następujące scenariusze testowe:
...i tak dalej.
Posiadając takie testy (i utrzymując środowisko uruchomieniowe, które po każdej zmianie sprawdzi, czy testy przechodzą), można mieć pewność, że mechanizm tworzenia konta w oparciu o unikalny email został nie tylko zaimplementowany poprawnie i działa teraz, ale także, że będzie działać kiedyś.
Jak testować, żeby to miało sens - przykłady
Załóżmy, że powyższe testy nie zostały zaimplementowane. Inny developer rozpoczyna pracę nad kodem i zmienia strukturę bazy tak, że pozwala na powtarzanie się adresów email wśród użytkowników. Miesiąc później klient zgłasza, że wystąpił problem - zarejestrowanych zostało milion użytkowników z tym samym adresem email. Tej sytuacji mogliśmy uniknąć, gdyby scenariusz
istniał i był uruchamiany po każdej zmianie.
Innym przykładem, który podsuwa Bartek, może być sytuacja, gdy pracuje się w projekcie, w którym często zmieniane są wymagania - na przykład pracując dla start-upu, który jeszcze klaruje swoją wizję na projekt.
- Załóżmy, że mamy porządnie przetestowany projekt - kilkaset testów, na różne scenariusze i przypadki skrajne. Pojawia się nowe wymaganie - aby w scenariuszu X, z konfiguracją Y, system zachowywał się w jakiś określony sposób. Zgodnie z zasadami test driven development, najpierw piszemy test. Test powinien najpierw nie przechodzić - to dobrze, ponieważ mamy informację czego wymagamy od systemu, ale jeszcze nie zostało to zaimplementowane. Wprowadzamy odpowiednie zmiany w kodzie, aby nowa funkcjonalność zaczęło działa. Puszczamy testy - nasz nowy test przechodzi, ale poprzedni test przestał przechodzić. Okazało się, że nasza zmiana wpływa na inną funkcjonalność.
W tym momencie możemy taką sytuację opisać klientowi, który zażyczył sobie nową funkcjonalność i zapytać, jaki jest oczekiwany efekt - czy należy jeszcze zmodyfikować implementację, ponieważ nie spełnia ona poprzednich wymagań, czy zmodyfikować istniejący test, ponieważ nowa funkcjonalność zmienia także poprzednią. Posiadamy wtedy pełną kontrolę nad działaniem aplikacji.
Automatyzacja testów w Pythonie
Mentor podkreśla, że pamiętać należy także, aby nie pisać testów przeczulonych. Oznacza to, że jeżeli testujemy np. czy system wysłał SMS, gdy użytkownik wysłał jakieś zapytanie na serwer, to nie musimy sprawdzać czy na pewno informacja o planie wysłania SMSa została zapisana w bazie. Mając taki test, w przypadku jakiejkolwiek zmiany struktury w bazie danych (np. dodania nowego pola w tabeli), wymaga to od programisty wyrównania (tzw. alignmentu) istniejącego testu, co efektywnie wydłuża dostarczenie małej zmiany, ponieważ wymaga to naprawy innych testów. Innymi słowy, nie należy mieszać poziomów testowania. Testy jednostkowe maja testować czy pojedynczy moduł działa poprawnie i on powinien być czuły na drobne zmiany. Testy funkcjonalne pokrywają szerszy zakres systemu, często wykorzystujący wiele modułów.
Wtedy należy unikać dobierania się do detali poszczególnych modułów, a test traktować jak czarną skrzynkę - wejściem jest np. akcja użytkownika, wyjściem jest np. wysłanie emaila. Jeżeli testy mają efekty uboczne (np. wysyłanie zapytań do zewnętrznych API), takie zachowanie w systemie należy mockować - czyli zamiast wywoływać faktyczną implementację, która np. wyśle SMS lub pobierze dane z serwera, dostarczyć testowi przykładowe dane, które można otrzymać z serwera.
Dzięki temu testy będą szybsze i nie będą zależne od zewnętrznych serwisów. Gdyby nie zamockować wysyłania emaili w testach, testując wysyłanie mail raz-dwa wykorzystalibyśmy limit zapytań z API i musielibyśmy dopłacać do kolejnych zapytań.
Chcesz pisać bezbłędny kod?
Jesteśmy ludźmi praktycznymi. Nawet początkujący programista, pisząc najprostszy skrypt, uruchamia go wielokrotnie na różnych etapach, z różnymi wymyślonymi danymi, aby sprawdzić "czy działa" i zamiast martwić się wszystkimi możliwymi problemami, uznaje kod za akceptowalnie poprawny, jeśli przejdzie on pomyślnie te wyrywkowe próby.
Zastosowanie podobnego podejścia w odpowiednio systematyczny sposób pozwala nam powiedzieć, że co prawda dalej nie mamy gwarancji, że błędów nie ma gdzie indziej, ale "w zakresie tego, co sprawdzaliśmy", kod jest bezbłędny. Pozostaje zatem skupić się na tym, aby "to, co sprawdzaliśmy" było wystarczające. Jak to zrobić?
Szkolenie PRO (dla doświadczonych)
Właśnie tym zajmujemy się podczas szkolenia z automatyzacji testów w Pythonie. Kurs jest przeznaczony dla osób, które chcą poszerzyć swoje kompetencje z programowania w tym języku, ale które posiadają już znajomość programowania w Pythonie na poziomie umożliwiającym swobodne tworzenie funkcji i klas. Szkolenie realizowane jest w formie online z indywidualnym Mentorem, na którego wsparcie można liczyć.
Czego się nauczysz?
- Jak przygotować różnego rodzaju testy w Pythonie?
- Pokrycia kodu testami
- Nowego podejście do programowania i testowania
Dbanie o jakość kodu jest dziś zadaniem każdego programisty, a umiejętność pisania testów to częste kryterium w czasie rekrutacji. Celem kursu jest pokazanie w jaki sposób można w Pythonie przygotować testy - od wersji najprostszej z użyciem słowa assert, poprzez testy w DocTest czy Unittest, po zastosowanie frameworków zewnętrznych (Pytest), w których wykorzystamy takie techniki jak fixtury, mockowanie.
Następnie sprawdzimy, jak dobrze kod pokryty jest testami (coverage) oraz postaramy się spiąć proces testowania w narzędziu automatyzujący (np. Tox). Zarysujemy proces ujęcia testów w CI/CD w oparciu o Gitlab Pipeline / Travis. Na koniec wspomnimy o nowych podejściach do programowania i testowania takich np. jak BDD.