Taki sposób tworzenia oprogramowania obciążony jest kilkoma wadami:
- Testowanie
- Testerzy testują kod nie znając go zbyt dobrze
- Brak zaangażowania testerów
- Na czas testowania kod jest zamrożony
- Naprawa błędu po zakończeniu implementacji jest zazwyczaj 2-krotnie wyższa niż w jej trakcie
- Utrzymanie
- Testy „po” sprawdzają wybiórczo kod
- Testy nie przyczyniają się do zwiększenia jakości kodu, a jedynie sprawdzają, jak działa napisany kod
Przepis na TDD jest prosty:
- Najpierw napisz test, który nie przechodzi.
- Stwórz najprostsze możliwe rozwiązanie
- Refaktoryzuj
- Wróć do kroku 1
Najpierw napisz test
- W TDD jest to ważne narzędzie – tutaj powstają pierwsze decyzje projektowe.
- Programista musi przemyśleć jak kod zostanie użyty
- Test wymusi odnalezienie tego, co rzeczywiście jest potrzebne w systemie
- Testy stają się dokumentacją
Stwórz najprostsze możliwe rozwiązanie
- Napisz dokładnie to, co jest wymagane przez test.
- Nie dodawaj fajerwerków do kodu
- Kod musi przejść stworzone testy – tyle wystarczy.
Refaktoryzuj
- Przyjrzyj się swojemu rozwiązaniu
- Zastanów się, co należy zrobić lepiej
- Zmieniaj to, co rzeczywiście pomoże lepiej zrealizować wymagania
- Popraw brzydkie fragmenty kodu
W danym momencie dokonuj tylko jednej zmiany w systemie (możliwie najmniejszej)
Taka technika programowania posiada kilka zalet:
- W każdym momencie masz w pełni działający kod
- Małe zmiany powodują, że łatwiej nad mini zapanować
- Małe zmiany powodują, że łatwiej znaleźć błędy, gdyż powodów błędów jest mniej.
- Pracowanie na mniejszych fragmentach kodu daje większą kontrolę programiście
- Projekt powstaje ewolucyjnie – programista może czerpać doświadczenie z tego, co napisał do tej pory.
W dalszej części tego dokumentu zauważycie, że metody przyrostowe wykorzystywane są na każdym kroku w TDD. Zaczynamy od rzeczy najprostszych i najprostszych rozwiązań
Przyczyną opracowania tej techniki programowania są ograniczenia pola świadomości:
- Ludzki świadomy umysł jest w stanie przetwarzać 5-7 informacji w danym momencie
- Dzieląc problem na mniejsze części ułatwiasz sobie pracę
- Mniej szczegółów musisz zapamiętać
- Jesteś bardziej świadomy konsekwencji wprowadzonych zmian
Pierwszą rzeczą, jaką należy zrobić przystępując od tworzenia projektu jest spis funkcjonalności. Możemy tutaj posłużyć się szablonem User Story, który pozwala na opisanie funkcjonalności w sposób najbardziej ogólny:
Jako <<osoba, rola>>, chcę <<funkcjonalność, czynność>>, aby <<uzasadnienie biznesowe>>; | As <<osoba, rola>>;, I want <<funkcjonalność, czynność>>, so that <<uzasadnienie biznesowe>> |
Tutaj zahaczamy trochę o Behaviour-Driven Development. Jest to popularny pomysł na sterowanie procesem pisania kodu. Skupia się na wymaganiach użytkownika i interesariuszy. Określa formę zapisu wymagań, które są przekształcane w testy.
W BDD osią rozwoju systemu jest zachowanie systemu. Testy powinny przede wszystkim opisywać (i testować) zachowanie systemu. Formułowanie zachowania staje się sposobem definiowania wymagań i zarazem ich dokumentacją.
Tutaj znajdziecie ogólny opis BDD http://www.araneo.pl/blog/behaviour-driven-developmentPosiadając już taką listę User Story „Musisz odpowiedzieć sobie na jedno zajebiście, ale to zajebiście ważne pytanie: co lubisz w życiu robić? Jaka jest następna najważniejsza rzecz, którą system powinien robić? A potem zacznij to robić.”
Jednym słowem należy wybrać najważniejszą z punktu widzenia użytkownika funkcjonalność i stworzyć dla niej kryteria akceptacyjne. Kryteria akceptacyjne formułowane są jako scenariusze. Możemy skorzystać tutaj z szablonu:
Zakładając że Gdy <<opis zdarzenia w systemie>> Wtedy <<spodziewany rezultat>> | Diven <<opis kontekstu>> when <<opis zdarzenia w systemie>> then <<spodziewany rezultat>> |
Kryteria akceptacyjne powinny być wykonywalne. Aby kryteria akceptacyjne miały największą wartość użytkową powinny mieć odzwierciedlenie w kodzie. Istnieje wiele frameworków wspierających BDD
- JBehave (Java)
- NBehave (.NET)
- Spec (Ruby)
- i wiele innych …
Teraz przyszedł czas na testy akceptacyjne.
Ogólny schemat testowania akceptacyjnego
Testy akceptacyjne testują funkcjonalność z punktu widzenia użytkownika. Skupiają się na perspektywie użytkownika końcowego. Symulują one interakcję z systemem zewnętrznym (np. z użytkownikiem, usługą sieciową):
- poprzez interakcję z użytkownikiem
- wysyłając komunikaty symulujące system zewnętrzny
- wywołując usługi sieciowe
- parsując generowane raporty
Testy akceptacyjne odpowiadają za tzw. jakość zewnętrzną, czyli w jakim stopniu system zaspokaja potrzeby klienta i użytkowników. Pozwalają nam także sterować testami jednostkowymi, które to z kolei odpowiadają, za jakość wewnętrzną, czyli w jakim stopniu system zaspokaja potrzeby programistów i administratorów. (Testy integracyjne są gdzieś pośrodku J)
To jak już tyle wiemy to można już coś zrobić.
Od czego zacząć?
- Zbuduj infrastrukturę, aby zautomatyzować proces
build – deploy – test - Zwiększasz ilość uzyskiwanej informacji zwrotnej
- Dzięki temu od razu będziesz mógł weryfikować budowanie i wdrażanie systemu
- Szkielet – implementacja bardzo wąskiego wycinka systemu, który można zbudować, wdrożyć i przetestować
- Zobacz jak system działa najszybciej jak to tylko możliwe
- Jeśli istnieje system zewnętrzny, którego trudno użyć, zbuduj jego namiastkę
Przydatne jest także stworzenie zgrubnego projektu, czyli wysokopoziomowej struktury aplikacji lub części nad którą pracujesz. Przemyśl główne komponenty i ich powiązania. Jak komponenty będą się komunikować. Na stworzenie zgrubnego projektu przeznacz co najwyżej kilkanaście minut – przydatny jest flipcharta lub tablicy.
Dobre praktyki
|
Na początku tego dokumentu wspominałem, że podejście przyrostowe można napotkać na każdym kroku TDD. Implementując daną funkcjonalność zaczynamy od najprostszego przypadku (testu akceptacyjnego), a następnie go rozwijamy. Przykładowo nasza wspaniała aplikacja karciana będzie umożliwiać użytkownikowi wyświetlenie listy gier, jakie rozegrał.
Jako Użytkownik Chcę zobaczyć listę gier, Aby móc przeglądać historię rozegranych partii. |
To by było nasze User Story J. Dodatkowymi wymaganiami byłoby, że listę można sortować, a wszystkie przegrane oznaczone są na czerwono.
Przykładowa lista testów w inkrementacjach mogłaby wyglądać tak:
- Pusta lista wyników (dla użytkownika bez rozegranych partii)
- Lista wyników z kilkoma grami
- Lista wyników z wyróżnionymi porażkami
- Lista wyników z sortowaniem
Cały proces testowania zaczyna się i kończy na teście akceptacyjnym. Jakby to powiedzieli mądrzy ludzie, test akceptacyjny jest klamrą dopinającą cały proces implementacji funkcjonalności. Pisząc test akceptacyjny, zauważamy braki w naszym projekcie. (Brak metod, klas itd. Przypomnijmy, że na początku posiadamy jedynie szkielet aplikacji.) Co robimy, jeżeli zauważymy brak danej metody? Piszemy dla niej test (oczywiście nie zapominając o specyfikacji i podejściu przyrostowym). Potem implementacja, znowu może nam czegoś brakować i znowu testy i znowu implementacja i tak w kółko, aż dojdziemy do końca.
Wspominałem wcześniej, że testy akceptacyjne sterują testami jednostkowymi. Zaczynami od testów najwyższego poziomu. W trakcie ich powstawania powstają testy niżej poziomowe. Po ich implementacji wracamy z powrotem do testu wysokopoziomowego. Może to trochę słabo opisałem, ale mam nadzieję, że wszyscy wiedzą, co chciałem powiedzieć.
3 komentarze:
Spoko wpis. Jedyne do czego mogę się przyczepić to literówka - "zgubnego projektu"- powinno raczej być "zgrubnego projektu". Poniżej zdanie w którym występuje błąd
"Przydatne jest także stworzenie zgubnego projektu, czyli wysokopoziomowej struktury aplikacji lub części nad którą pracujesz."
Tak :)
Prześlij komentarz