Test Driven Development

Jak tworzyć solidne i niezawodne oprogramowanie

Ten artykuł jest również dostępny na LinkedIn w języku polskim.

This article is also available in English on Medium.

Ten artykuł powstał na podstawie prezentacji, którą przedstawiłem w październiku 2019r.

Dlaczego powinieneś pisać testy?

Po pierwsze testy pozwalają na pisanie kodu, który jest skupiony głównie na realizacji założeń biznesowych, a zatem jest prostszy.

Na przykład, jeżeli naszym zadaniem będzie znalezienie w słowa “klapaucius” to prawdopodobnie wykorzystamy String.includes w JavaScript zamiast analizować tekst skomplikowanymi modelami uczenia maszynowego 

Kolejną zaletą jest wyższa jakość kodu z powodu ułatwionego refaktoringu. Gdy mamy testy to możemy wprowadzać zmiany (także architektoniczne) bez obaw, że wywołamy “efekt motyla” i nasza mała zmiana w pewnym kawałku aplikacji spowoduje awarię w zupełnie innej, wydawało by się niezależnej części. Testy pomogą nam też znaleźć błędy wcześniej – przed wdrożeniem aplikacji na produkcję. Nie dostaniemy zatem telefonu od zezłoszczonego użytkownika, że “znowu coś nie działa”.

Samosprawdzający się kod

Skoro już mamy kod, który jest przetestowany nie musimy wykonywać weryfikacji czy wszystko działa poprawnie w sposób manualny. Większość problemów powinna się pokazać po wpisaniu komendy “npm test”(w przypadku Node.JS). Oczywiście nie oznacza to, że nie powinniśmy się upewnić “ręcznie” czy wszystko jest w porządku(na przykład na stageingu czy preprodzie). Nie mniej jednak gdy mamy testy to nie musimy tak często używać narzędzi takich jak na przykład Postman. Spędzając mniej czasu na ręczne sprawdzanie “czy wszystko jest ok” możemy dłużej pracować nad “mięskiem”. Czyli tym co rzeczywiście przynosi naszemu biznesowi wartość i dzięki temu możemy w trakcie sprintu dowieźć więcej zadań.

Zaletą testów jest między innymi to, że nie musimy ręcznie sprawdzać czy oprogramowanie działa poprawnie po naszych zmianach. Ponieważ(jako ludzie) jesteśmy dosyć leniwi to będziemy chcieli, żeby jak najwięcej rzeczy sprawdzało się “samo”. Zatem będziemy dążyć do tego, aby jak najwięcej kodu było pokryte testami – w końcu przyspiesza to naszą pracę. Wraz z rosnącym pokryciem testami mamy też coraz lepszą dokumentacj. Testy mogą być dokumentacją dla nowych pracowników, ale i też dla nas, gdy po pewnym czasie zapomnimy co pewien kawałek kodu konkretnie robił oraz w jaki sposób.

Testy automatyczne przyczyniają się do mniejszej liczby błędów na produkcji, ponieważ większość tak zwanego “złego kodu” jest wykrywana przed wdrożeniem – w momencie gdy na naszym ekranie pojawia się czerwony komunikat: “testy nie przeszły”.

Pisanie testów w dłuższej perspektywie pozwala na pisanie kodu szybciej niż gdybyśmy testów nie mieli. Na początku musimy w prawdzie więcej czasu poświęcić na skonfigurowanie środowiska testowego. Natomiast później nie musimy aż tyle czasu marnować na powtarzanie w kółko tych samych scenariuszy testowych oraz testowanie kodu, który już kiedyś przetestowaliśmy.

W jaki jeszcze sposób pisanie testów pomoże mi tworzyć lepsze oprogramowanie?

Nie będziesz potrzebował aż tyle czasu na debugowanie i testowanie ponieważ testy zrobią to za Ciebie dzięki automatyzacji i łatwości uruchamiania ich.

Testy pozwalają skupić się na biznesowym celu bardziej niż na “potężnej” architekturze. Oczywiście nie wolno o architekturze zapominać. Jest ona bardzo ważna – natomiast warto sobie zadać pytanie czy na pewno funkcjonalność, którą implementujemy jej w tym momencie wymaga.

Wraz z testami powstaje specyfikacja produktu. Testy pokazują jakie aspekty biznesowe aplikacja jest w stanie obsłużyć, a jakich nie.

Tworzenie testów pozwala na uzyskanie lepszego podziału kodu – szybko zauważysz, że lepiej wstrzykiwać zależności niż tworzyć nowe obiekty na przykład w poszczególnych metodach. Podział kodu pozwoli Ci tworzyć testy w prostszy sposób, a aplikacja będzie łatwiejsza w rozwoju i utrzymaniu.

Refaktoring kodu nie będzie więcej spędzał snu z powiek. Testy sprawdzą o wiele więcej przypadków niż manualne “klikanie” po produkcie – wiesz też czy swoimi zmianami nie popsułeś przypadkiem czegoś w innej części aplikacji. Możesz więc wprowadzać ulepszoną architekturę, zmieniać sposób działania poszczególnych części kodu bez obaw, że o błędach poinformują Cię klienci.

Typowy sposób tworzenia oprogramowania.

Typowo, gdy nie korzystamy z TDD nasza praca wygląda następująco:

Otrzymujemy wymagania biznesowe następnie razem z zespołem próbujemy naszkicować w jaki sposób docelowe rozwiązanie mogło by wyglądać, a następnie dzielimy problem na mniejsze zadania.

Programiści przypisują się do zadań i zaczynają pisać “mięsko”. W międzyczasie albo po napisaniu kodu jakieś testy są pisane. Natomiast najczęściej liczba napisanych testów oraz pokrycie kodu testami dąży do zero w takim standardowym podejściu. Wszystkie przeprowadzone testy są robione zazwyczaj manualnie przez co po pewnym czasie gdy chcemy sprawdzić kod działa prawidłowo musimy powtarzać proces testowania -ręcznie – od nowa.

Zdecydowanie istnieje lepszy sposób!

Podejście Test-driven do inżynierii oprogramowania

W podejściu TDD nadal pierwszym etapem jest wstępne projektowanie i architektura docelowego rozwiązania. Natomiast w tym kroku pojawia się jeszcze jeden nowy aspekt: należy także omówić scenariusze testowe, które są ważne dla biznesu.

Gdy opis poszczególnych zadań jest przygotowany to biorąc nowe zadanie powinieneś zacząć pracę nie od pisania logiki biznesowej, a od kodu, który przetestuje Twój przyszły kod. Na początku standardowo gdy uruchamiasz nowo napisane testy to nie powinny one przechodzić.

Taki sposób pracy ma też dodatkowy atut: nie potrzebujesz(tak często) połączenia z internetem oraz nie musisz wszystkiego uruchamiać “w chmurze”. Wystarczy Twój komputer i zainstalowane środowisko oraz pakiety, które wykorzystujesz(np. Node.JS i biblioteka Jest). Żadnych baz danych, pamięci podręcznych(cache) czy zewnętrznych API. Jedna komenda(npm test) i w większości przypadków wiesz czy to co napisałeś zadziała poprawnie na produkcji.

Czerwony » Zielony » Refaktoring!

Zacznij od testów – na początku nie powinny przechodzić. Następnie napisz tylko tyle kodu, aby test przeszedł. Gdy uzyskasz już zielony napis “test passed” to zacznij refaktoring kodu oraz popraw architekturę.

Oczywiście na codzień zazwyczaj będziesz pisał więcej niż jeden test na raz oraz będziesz pisał kod, który przejdzie więcej niż tylko test, który napisałeś. Rezultatem powinno być otestowane oprogramowanie, którego nie trzeba sprawdzać ręcznie, aby stwierdzić czy wszystko poprawnie działa.

Rzeczy, których należy unikać podczas pisania testów jednostkowych

Testy jednostkowe muszą być szybkie, a więc nie możesz używać niczego co jest wolne. Dodatkowo muszą być niezależne od zewnętrznych bytów. Dlatego nie możesz korzystać z bazy danych, robić requestów do innych serwisów – nawet takich, którymi to ty zarządzasz – jeżeli ich potrzebujesz to powinieneś je zmockować.

Nie powinieneś wykorzystywać systemu plików, powłoki bash ani korzystać z przykładowo kontrolera do Xboxa. Twoje testy muszą być niezależne od platformy na której są uruchamiane.

Nie możesz w testach wykorzystywać elementów, które wykorzystując współbieżność mogły by edytować zmienne w procesach innych testów. Każdy test to niezależna jednostka.

Skoro testy mają być szybkie to nie możesz celowo ich spowalniać za pomocą sleep czy setTimeout – w Jest możesz go zmockować – muszą one działać błyskawicznie.

I skoro nie możesz wykorzystywać plików to nie powinieneś też zmieniać zmiennych środowiskowych czy plików konfiguracyjnych.

Testy powinny być pisane F.I.R.S.T

Ten popularny akronim oznacza, że po pierwsze testy muszą być tak szybkie jak to możliwe aby developerzy nie zniechęcali się ich uruchamianiem. Testy muszą się wykonywać maksymalnie kilka sekund.

Aby testy były szybkie muszą się one uruchamiać jednocześnie(czy też współbieżnie) – więc muszą być od siebie niezależne. Każdy test powinien mieć swój własny zestaw danych.

Testy powinny być pisane w taki sposób, żeby nie miało znaczenia w jakiej kolejności je uruchomimy – na przykład data ustawiana przez jeden test nie może zostać wykorzystana przez inny test.

Testy muszą być samo sprawdzające się – nie może być tak, że musimy sprawdzić ręcznie(albo oczami) czy wynik jakiejś funkcji to przykładowo 444. Jedynym rezultatem powinien być kolor czerwony(jeżeli test nie przeszedł) oraz zielony(test przeszedł).

I ostanie, ale nie najmniej ważne – testów nie piszemy tylko po to, aby mieć 100% pokrycie kodu. Piszemy je aby sprawdzić ważne dla biznesu scenariusze oraz między innymi warunki brzegowe. Testy nie powinny też “betonować” kodu i powodować, że dodanie jakiejkolwiek funkcjonalności wymaga przepisania połowy testów.

Narzędzia do TDD

Oto lista narzędzi, których używam w projektach nad którymi pracuję. Poniżej znajdziesz je krótko opisane.

TypeScript to “nakładka” na JavaScript. TS dodaje wsparcie dla sprawdzania typów zmiennych co pozwala na zmniejszenie liczby błędów typu literówka, zła kolejność parametrów oraz podobnych. TypeScript ma świetną społeczność, wspiera funkcje JavaScript, które dopiero zostaną wdrożone(zwane ES-Next). Pozwala na pisanie czystszego, wyższej jakości kodu oraz poprzez typowanie ułatwia refaktoring. TypeScript transpiluje się do różnych wersji JavaScript – możesz zatem wybrać czy chcesz wspierać starsze przeglądarki.

Jest to framework do testów, który jest szybki, prosty w użyciu i posiada wiele opcji do sprawdzania czy kod, który napisaliśmy działa poprawnie. Ma też świetną dokumentację.

Jest posiada wiele asercji. Do wyboru jest między innymi porównywanie wartości, przeszukiwanie tablic, obiektów. Można też na przykład sprawdzić z jakimi parametrami wykonała się wybrana metoda.

Kolejną zaletą Jest jest generowany przez niego raport pokrycia kodu testami. Możemy sprawdzić czy dany plik został przetestowany, w jakim procencie, które branche(warunki if/else) zostały wykonane. Pamiętaj natomiast, że osiągnięcie 100% pokrycia nie powinno być celem. Staraj się przetestować jak najwięcej scenariuszy, które są ważne dla Twojego biznesu i nie zapominaj o warunkach brzegowych. użytkownicy lubią wpisywać różne rzeczy w pola formularzy.

Narzędzia: SonarQube.

SonarQube wykonuje statyczną analizę kodu – sprawdza czy kod zawiera znane błędy oraz czy nie ma w nim luk bezpieczeństwa. Wspiera wiele języków programowania włączając w to JavaScript oraz TypeScript.

Narzędzia: Bamboo.

Bamboo jest to narzędzie, które pozwala na połączenie wszystkich elementów procesu wdrażania aplikacji. Od automatycznych testów po każdym commicie, przez wybudowanie produkcyjnej aplikacji do wdrożenia jej na środowisko integracyjne czy produkcyjne. Można do niego podłączyć nie tylko testy, ale też lintery kodu czy analizę za pomocą SonarQube.

TDD na przykładzie – sesja live.

W tym miejscu była sesja programowania “na żywo”.

Kod znajdziesz tutaj: https://github.com/niezgoda/LT

FizzBuzz w TDD

Krótkie nagranie jak można podejść do TDD na klasycznym przykładzie Fizz Buzz.

Miłego dnia!

P.S. Podczas pisania kodu – nawet gdy jest on przetestowany – pamiętaj o bezpieczeństwie. Dowiedz się dlaczego każda linia kodu ma znaczenie dla bezpieczeństwa Twojej aplikacji i firmy.

Masz pytanie lub chciałbyś/chciałabyś udzielić mi feedbacku?

Super! Napisz do mnie na LinkedIn. Mój mail to: mateusz@niezgoda.io lub znajdź mnie na Facebooku lub Instagramie 🙂 

Źródła i dodatkowe informacje