sobota, 27 marca 2010

TDD interfejsu użytkownika

29.03.2010 Zmieniłem trochę kod zgodnie z podpowiedzią Jakuba Nabrdalika, żeby zapisać to mockitową składnią BDD, oraz Michała Margiela, który znalazł duplikację w kodzie :-)
Pamiętam czas, kiedy miałem 19-20 lat. W ciągu dnia studia, wieczorami dla zabawy tłukłem oldskoolowe dos'owe wirusy i animacje fraktalne w asemblerze, a nad ranem i w weekendy zarabiałem, żeby było w tygodniu na obiady w 'Złotej Kurce' koło polibudy. Zarabiałem pisząc aplikacje w Delphi i C++ Builderze. To były narzędzia! Wystarczyło poukładać okienkowe komponenty na panelu i na każdym wykonać dwuklik. Od razu IDE przenosiło Cię do odpowiedniej metody gdzie wystarczyło napisać kod, który miał być wykonany gdy użytkownik kliknie w ten komponent. Ciągle się zastanawiam dlaczego mimo upływu 12 lat, środowiska Javowe nie pozwalają na to samo...
Ale wracając do tematu - za pomocą tych narzędzi: świetnego środowiska i tzw. Event-Driven Development stworzyłem parę... potworków. Dlaczego potworków? No więc pewnie dlatego, że moje doświadczenie programistyczne było małe i nie wiedziałem, że kod UI nie powinien być wymieszany z logiką. A już na pewno nie z kodem obsługi bazy danych...
W międzyczasie parę lat minęło i zdążyłem się zorientować, że są sposoby na pisanie kodu tak, by móc go zmienić bezpiecznie jakiś czas po napisaniu. Najpierw okazało się, że są wzorce projektowe, które dość skutecznie pomagają zaprojektować kod przed napisaniem. Dzięki temu dało się potem taki kod zmienić i poprawić. Niestety zwykle tylko raz, czasem dwa. Potem ten kod i tak stawał się do ... niczego nie podobny. Potem okazało się, że są testy jednostkowe, które pozwalają na utrzymywanie kodu w nieskończoność (o ile utrzymuje się też testy...) I tak doszedłem w końcu do mementu kiedy logikę miałem ujarzmioną testami, ale pozostawał interfejs użytkownika jako najsłabsze ogniwo. Tak mniej-więcej koło 2006 Martin Fowler napisał parę artykułów opisujących wzorce tworzenia UI (w szczególności chodzi mi tu o Passive View) i... sprawa stała się jasna :-)


W Pragmatists tworzymy oprogramowanie wyłącznie w TDD. Również interfejs użytkownika. Używamy więc wzorców przedstawionych przez Fowlera, ponieważ podstawowa ich zaleta to uniezależnienie logiki UI od widoku, więc idealnie nadają się do testowania. Nie zawsze jest to dokładnie to, co proponował Fowler, ale co do zasady jest to jakaś wersja wzorca Model-View-Presenter. Passive View polega na tym, że widok potrafi jedynie przekazać prezenterowi komunikat o tym, że zaistniało jakieś zdarzenie, oraz potrafi się odświeżyć na podstawie posiadanych przez siebie danych. Prezenter, po otrzymaniu komunikatu, że coś się w widoku zdarzyło, wykonuje odpowiednią operację na modelu i pobiera z niego dane. Następnie ustawia na widoku uaktualnione dane,a ten po zakończeniu działania prezentera, odświeża się. Widok jest zupełnie 'głupi' - bez jakiejkolwiek logiki poza wywoływaniem prezentera i własnym odświeżaniem się. Cała logika widoku jest w prezenterze, a model jest tak naprawdę Business Delegate'em do reszty aplikacji. Dzięki temu możemy odpuścić sobie testowanie view, bo ryzyko, że pomylimy się w jakimś getterze jest znikome ;-) Możemy dokładnie przetestować prezenter, włącznie z ew. przepływem między ekranami/panelami. A testy modelu to tak naprawdę testy funkcjonalne niższych warstw aplikacji.


Weźmy konkretny przykład. Załóżmy, że piszemy aplikację a'la kalendarz w Outlook'u i mamy ekran tworzenia nowego zdarzenia w kalendarzu. Takie zdarzenie ma zwykle nazwę, datę z godziną i listę zaproszonych osób. Chcielibyśmy móc takie zdarzenie zapisać, wybrać z książki adresowej osoby zaproszone, i wysłać zaproszenia do osób zainteresowanych. W zależności czy aplikację piszemy top-down (zaczynamy od UI idąc 'w głąb') czy mamy już gotową logikę (baza danych + wysyłanie zaproszeń), możemy zacząć albo od prezentera albo od modelu. Załóżmy więc, że zaczynamy od zera, więc jakiś grafik dłubie dżejpegi z ekranem nowego zdarzenia, a my w tym czasie robimy coś ciekawego :-)

Wiemy, że prezenter trzyma sobie model i widok (od razu je mockujemy - przecież ich nie ma, bo zaczynamy od prezentera), i potrafi zapisać zdarzenie (na podstawie danych z widoku, za pośrednictwem modelu):
@RunWith(MockitoJUnitRunner.class)
public class NewEventPresenterTest {
 @Mock
 private NewEventView view;
 
 @Mock
 private NewEventModel model;

 private NewEventPresenter presenter;

 
 @Before
 public void setUpPresenter() {
  presenter = new NewEventPresenter(model, view);
 }
 
 @Test
 public void shouldSaveEvent() throws Exception {
  //when
  presenter.saveEvent();
  
  //then
  verify(model).saveEvent(presenter.event);  
 }
}

No i taki test pozwala nam sprawdzić, że prezenter faktycznie zapisze reprezentowane przez view zdarzenie w modelu.

No to teraz użytkownik zmienia nazwę zdarzenia:
@Test
 public void shouldSetEventTitle() throws Exception {
  //given
  given(view.getTitle()).willReturn("test title");
  
  //when
  presenter.onTitleChanged();
  
  //then
  verify(view).getTitle();    
  assertEquals("test title", presenter.event.getTitle());
 }

Ustawia datę:
@Test
 public void shouldSetEventDateTime() throws Exception {
  //given
  given(view.getDateTime()).willReturn("2010-01-01 12:00");
  
  //when
  presenter.onDateTimeChanged();
  
  //then
  verify(view).getDateTime();    
  assertEquals(new LocalDateTime(2010,1,1,12,0), presenter.event.getDateTime());
 }

Pokazuje użytkownikowi listę osób, z których może wybrać kogoś do zaproszenia:
@Test
 public void shouldFillPotentialInvitees() throws Exception {
  //when
  presenter.onShowPotentialInvitees();
  
  //then
  verify(model).listPotentialInvitees();
  verify(view).setPotentialInvitees((Collection)anyObject());
 }

No i w końcu zaprasza wybrane osoby:
@Test
 public void shouldSetSelectedInvitees() throws Exception {
  //given
  given(view.getSelectedInvitees()).willReturn(Arrays.asList("friend1", "friend2"));
  
  //when
  presenter.onSelectedInvitees();
  
   //then
  verify(model).invitePeople(view.getSelectedInvitees());
  assertEquals(view.getSelectedInvitees(), presenter.event.getInvitedPeople()); 
 }


Do tego mamy oczywiście model, który wykonuje te wszystkie operacje. Najpierw tworzymy sobie zdarzenie (tu mockujemy dalszą warstwę, bo testujemy model):
@RunWith(MockitoJUnitRunner.class)
public class NewEventModelTest { 
 @Mock
 private EventService eventService;
 
 private Event sampleEvent;

 private NewEventModel eventModel;
 
 @Before
 public void initSampleEvent() {
  Collection friends = Arrays.asList("friend1","friend2");
  sampleEvent = new Event()
   .title("Test Event")
   .dateTime(new LocalDateTime()
    .withDate(2010, 10, 23)
    .withTime(0, 0, 0, 0))
   .invited(friends);
  when(eventService.fetchEvent((LocalDateTime)anyObject())).thenReturn(sampleEvent);
  when(eventService.fetchPotentialInvitees()).thenReturn(Arrays.asList("friend1", "friend2", "friend3"));
  
  eventModel = new NewEventModel(); 
  eventModel.setEventService(eventService);
 }

 @Test
 public void shouldCreateNewEvent() throws Exception {  
  assertNotNull(sampleEvent);
 }
}

Weryfikujemy warunki brzegowe:
@Test
 public void shouldNotSendInvitationsWhenNoEvent() throws Exception {
  //given  
  sampleEvent = null;
  
  //when
  eventModel.sendInvitations(sampleEvent);
  
  //then
  verifyZeroInteractions(eventService);
 }

 @Test
 public void shouldNotSendInvitationsWhenInviteesNull() throws Exception {
  //given  
  given(eventService.fetchEvent((LocalDateTime)anyObject())).willReturn(sampleEvent.invited(null));
  
  //when
  eventModel.sendInvitations(sampleEvent);
  
  //then
  verifyZeroInteractions(eventService);
 }
 
 @Test
 public void shouldNotSendInvitationsWhenInviteesEmpty() throws Exception {
  //given  
  given(eventService.fetchEvent((LocalDateTime)anyObject())).willReturn(sampleEvent.invited(new ArrayList()));
  
  //when
  eventModel.sendInvitations(sampleEvent);
  
  //then
  verifyZeroInteractions(eventService);
 }

Wyciągamy listę potencjalnych zaproszonych:
@Test
 public void shouldRetrievePossibleInvitees() throws Exception {
  //given  
  NewEventModel eventModel = new NewEventModel();  
  eventModel.setEventService(eventService);  
  
  //when
  Collection possibleInvitees = eventModel.listPotentialInvitees();
  
  //then
  assertNotNull(possibleInvitees);
  assertFalse(possibleInvitees.isEmpty());  
 }

W końcu wysyłamy zaproszenia:
@Test
 public void shouldSendInvitationsWhenInviteesNotEmpty() throws Exception {
  //when
  eventModel.sendInvitations(sampleEvent);
  
  //then
  verify(eventService).sendInvitations(sampleEvent.getInvitedPeople());
 }

I zapisujemy zdarzenie:
@Test
 public void shouldSaveEvent() throws Exception {
  //when
  eventModel.saveEvent(sampleEvent);
  
  //then
  assertSame(sampleEvent, eventService.fetchEvent(sampleEvent.getDateTime()));
 }

W ten sposób nie tylko mamy dobrze przetestowany zarówno prezenter jak i model, ale również mamy ładny w nich kod (czyż fluent interface klasy Event nie jest śliczny?) - a to dzięki temu, że zaczęliśmy pisanie tego kodu od jego użycia (testu = przykładu). Teraz tylko czas popoganiać grafika, żeby szybciej te pikselki układał i wybrać jakąś wygodną technologię do napisania widoku...

Ach, zapomniałem wkleić tu właściwy kod.... Ech, z resztą, ten to przecież teraz już każdy potrafi napisać, w końcu eclipse wygenerował sam przynajmniej z 30%...


*Autor posta dziękuje autorowi Mockito za to, że dzięki niemu w powyższych przykładach mockowanie jest tak proste, że prawie go nie widać :)

poniedziałek, 22 marca 2010

W TDD nie chodzi o testy

W trakcie przygotowań do szkolenia z TDD:
  K: Trzeba jakoś przekazać uczestnikom szkolenia, że w TDD nie chodzi o testowanie
  M: Jak to nie chodzi o testowanie? To o co chodzi?
  K: No chodzi o to, że testy są przykładami. Specyfikują to co właściwy kod ma robić.
  M: No dobra, ale na koniec to i tak jest testowanie. 
  P: No tak, ale możliwość odpalenia testów i zweryfikowania poprawności kodu jest tylko efektem ubocznym. Podstawowym celem jest dobre zrozumienie problemu oraz utworzenie dobrego designu.
  M: No tak, ale i tak testujemy a więc chodzi o testowanie
  P: Racja, ale nawet gdybyśmy mieli po zakończeniu implementacji stracić związane z nią testy, to i tak byśmy chcieli je pisać. Po prostu wartość z dobrego zrozumienia implementowanego zagadnienia i poprawności designu jest już wystarczająca. Możliwość późniejszego przetestowania - safety net - jest tylko dodatkiem do podstawowej wartości.

Ta trwająca z 15min dyskusja dała mi chwilę do zastanowienia się o co tak naprawdę chodzi w TDD. I choć M ma rację, że piszemy testy - więc testujemy, to nazwy test oraz TDD są zdecydowanie mylące. Już napiszę dlaczego.
Tworzenie oprogramowania to bardzo skomplikowane zadanie. Wielu z nas doświadcza tego przy prawie każdym wdrożeniu. Albo (mam nadzieję) nawet wcześniej - jak tester dobiera się do aplikacji i znajduje w niej masę błędów. Skąd one się biorą? Oczywiście czasem jest to jakieś drobne lenistwo programistów, ale większość przypadków to niekompletne rozwiązanie jakiegoś problemu lub niezauważenie jakiejś możliwości przepływu w aplikacji. Czyli żeby takich błędów nie było, trzeba by zapewnić dwie rzeczy:
1) dobre i kompletne zrozumienie rozwiązywanego problemu przez programistę
2) zapewnienie poprawnego wykonania aplikacji dla dowolnych parametrów (dowolnego przepływu kontroli w kodzie)

TDD jest odpowiedzią na oba te problemy. Jeśli zaczynamy pisanie kodu od jego wyspecyfikowania, czyli podania przykładów jego użycia, nasze zrozumienie zarówno rozwiązywanego problemu jak i samego pisanego kodu jest dużo większe. Po prostu spędzamy więcej czasu na zastanawianiu się nad problemem oraz projektowaniem rozwiązania. Ze względu na to, że w TDD definiujemy przykłady wykorzystania implementowanego kodu, bardzo często TDD nazywa się kodowaniem przez przykłady (coding by example). Na niższym poziomie piszemy po prostu przykłady zachowań obiektów (czyli jak chcemy wykorzystywać kod obiektu z innego obiektu). 
Powiedzmy więc, że chcemy napisać kod do obsługi pianina. Zaczynamy więc od reakcji na wciskanie klawiszy:

@Test
public void shouldPlaySoundWhenKeyPressed() {
// given
Piano piano = new Piano();

//when
piano.pressKey(Piano.Key.C);

//then
assertEquals(Piano.Key.C, piano.lastPlayedSound());
}
Na razie nic specjalnego. Teraz dodajemy pedał:

@Test
public void shouldPlayLongWhenSustainPedalPressed()  {
// given
Piano piano = new Piano();

//when
piano.pressSustain();
piano.pressKey(Piano.Key.C);
waitFor(5).seconds();
piano.releaseSustain();

//then
assertEquals(5, piano.lastSound().lasted(seconds));
}
I już widzimy, że klasa piano wymaga refaktoryzacji, więc poprzedni test powinien mieć w assert


 piano.lastSound().value()



W ten sposób kształtujemy API tak jak najwygodniej bę
dzie nam go używać, wymuszając na implementacji właściwy design i poprawiając czytelność kodu (czy waitFor(5).seconds() nie jest czytelniejsze od try { Thread.sleep(5000); } catch (InterruptedException e) {}?). Poprawiamy też nasze zrozumienie problemu - zorientowaliśmy się, że dźwięk ma nie tylko wysokość, ale również długość trwania, więc odpowiednio zmieniliśmy design.


To, że prawdziwe TDD pozwala na zapewnienie poprawnego wykonania kodu dla dowolnych parametrów jest jasne... jak się wie co to jest prawdziwe TDD ;-) Tak jak definiował je Kent Beck i jak zwykło się go nauczać, krok implementowania testu realizuje się przez napisanie najprostszego kodu, który powoduje, że test przechodzi. Jak najbardziej "po bożemu" jest więc pisanie trywialnego kodu po to tylko, żeby na drodze małych kroczków wymusić w kodzie istnienie wyłącznie tego, co jest wymagane i przetestowane. Tak więc dla powyższego przykładu, pierwsza implementacja metody pressKey wyglądałaby tak:


public Key lastPlayedSound() {
return Key.C;
}


Do przejścia testu wystarczy, a powoduje, że na pewno nie napiszemy żadnego kodu niepokrytego testami. Właściwa, kompletna implementacja powstanie już pewnie w ciągu następnych minut. Po napisaniu podobnego testu z inną nutą ten test już nie przejdzie i będzie trzeba napisać pełniejsze rozwiązanie. Choć na pierwszy rzut oka takie podejście wydaje się naiwne i bezsensowne, pomaga ono spojrzeć na problem z punktu widzenia zasady YAGNI - implementujemy wyłącznie to, czego wymaga od nas specyfikacja - czyli testy. W ten sposób łatwiej nam wpaść na prostsze rozwiązania.

TDD to więc prowadzenie naszego rozumowania i rozumienia problemu. To tworzenie sobie w głowie modelu implementowanej dziedziny. To również sposób na dbanie o jakość kodu z punktu widzenia design'u (czytelność, bo implementujemy tak jak chcemy wykorzystywać i na bieżąco refaktoryzujemy by nam jak najbardziej pasowało) oraz poprawności (prowadzenie przykładami wszystkich możliwych ścieżek wykonania kodu).
To, że za jakiś czas będziemy mogli spokojnie zmienić dowolną część tego kodu, pewni, że jeśli coś zepsujemy, to odpowiedni test nas o tym poinformuje, jest tylko efektem ubocznym. Miłym dodatkiem do poprawnego kodu. A mówili, że nie istnieje coś takiego jak darmowy lunch...

środa, 17 marca 2010

Testy end-to-end w TDD


Steve Freeman i Nat Pryce napisali książkę, która wywołuje dużo poruszenia w światku wytwarzania oprogramowania. Growing Object-Oriented Software Guided by Tests jest oceniana jako unikalna - dotychczas takiej książki o TDD nie było. Moim zdaniem w całej dziedzinie tworzenia oprogramowania rzadko spotyka się książki tak praktyczne, z dużą ilością kodu i świetnym przedstawieniem toku myślenia autorów.

Lektura tej książki skłoniła też Jacka Laskowskiego do napisania (jak na razie) dwóch postów. Chciałbym tutaj skomentować jego wypowiedź z pierwszego z nich. Najpierw krótko sparafrazuję myśli Jacka, do których chciałbym się odnieść:

Jacek pisze, że dotychczas zakładał, że w TDD chodzi o testy jednostkowe, czyli testujące poszczególne klasy w izolacji.
Tymczasem zaskoczyło go proponowane w książce podejście, w którym rozpoczyna się tworzenie aplikacji od napisania przekrojowego testu dotyczącego jednej prostej funkcjonalności. Chodzi o wyspecyfikowanie zachowania aplikacji z uwzględnieniem jej wszystkich warstw, od interfejsu użytkownika do połączeń z zewnętrznymi systemami, zanim jeszcze powstała architektura aplikacji. Jacek zastanawia się, który zespół wytrzyma tworzenie takiego testu, kiedy nic jeszcze nie ma? Wymaga to dużej determinacji i zdyscyplinowania - rzadko spotykanych zalet zespołów programistycznych.
Co daje nam stosowanie podejścia Test Driven? Jedną z zalet jest szybkie pojawienie się informacji zwrotnych co do poprawności stworzonego przeze mnie kodu. Kiedy piszę test jednostkowy, uruchamiam go i widzę czerwony pasek jUnita, wiem, że mam coś do zrobienia. Po napisaniu implementacji zielony pasek mówi mi, że skończyłem, że ten kawałek kodu działa, i mogę zająć się refaktoryzacją. Dzięki temu pracuję szybko, bo nie muszę uruchamiać aplikacji po każdej zmianie (lub debuggera jeśli po wprowadzeniu 10 zmian uruchamiam aplikację i nie działa poprawnie).

Patrząc od strony tego doświadczenia, to jest szybkości i pewności tworzenia oprogramowania, dość naturalnym wydaje się chęć, aby tak pracować jak najczęściej. Powiedzmy, że stworzyłem test-first warstwę dostępu do danych dla pewnej funkcjonalności i tak samo warstwę domenową. Teraz chciałbym wiedzieć, czy będą poprawnie współpracowały. Piszę zatem test, który będzie testem integracyjnym, idącym przez te dwie warstwy. Wymusi on na mnie napisanie kodu łączącego ze sobą obiekty z tych dwóch warstw w odpowiedni sposób (wiring).

Idąc dalej: powiedzmy, że właśnie rozpoczynam tworzenie projektu i czeka mnie stworzenie jego początkowej architektury. Mając doświadczenie szybkiej i automatycznej informacji zwrotnej, uzyskiwanej dzięki testom pisanym przed implementacją, mogę chcieć uzyskać taki feedback także i w tym przypadku. W tym celu tworzę właśnie test czegoś, co twórcy książki nazwali The Walking Skeleton, czyli szkieletu wszystkich warstw aplikacji. Uruchamiam go i szybko mam informację zwrotną, czy moja infrastruktura jest już gotowa, czy może potrzebuję jeszcze dodatkowego kodu lub konfiguracji, aby aplikacja zaczęła działać.

Powyższe rozumowanie mam nadzieję pokazuje, dlaczego mógłbym chcieć tworzyć aplikację w sposób opisany przez autorów książki. Tym niemniej, nie znaczy to, że jest to jedyna możliwość w zakresie szeroko rozumianego nurtu TDD. Możliwości tworzenia aplikacji w sposób test-driven jest wiele. Programiści często wypracowują swój styl, a różnice mogą dotyczyć różnych aspektów, takich jak stosowanie testów akceptacyjnych, integracyjnych, używanie mocków, podejście do testowania interfejsu użytkownika.

Na przykład autorzy wzmiankowanej książki, Steve Freeman i Nat Pryce, stosują podejście do TDD, które można określić jako "Mockist approach", tzn. że mocki są podstawowym narzędziem w tworzeniu testów. Zainteresowanych dokładnym omówieniem różnic pomiędzy tym podejściem a podejściem klasycznym odsyłam do artykułu Martina Fowlera.

Co do pisania testów przekrojowych (end-to-end), to ja nie jestem ich zwolennikiem i zdecydowanie staram się ich unikać. Moim zdaniem testy jednostkowe plus trochę testów integracyjnych daje wystarczająco dużą pewność co do poprawności działania aplikacji. Wadą testów przekrojowych jest przede wszystkim ich powolność. Wyobrażam sobie, że mógłbym ich użyć do stworzenia architektury i być może zainspirowany książką spółki Freeman&Pryce spróbuję, jak taki sposób pracy "smakuje". Dotychczas używałem ich rzadko i raczej jako testy regresji, czy smoke testy - zgrubne sprawdzenie, czy nie popsuliśmy aplikacji.
Jeśli już używamy testów end-to-end, trzeba pamiętać o zachowaniu proporcji - zgodnie z piramidą testów tych przekrojowych powinno być jak najmniej.

poniedziałek, 15 marca 2010

Oficjalna strona szkolenia z TDD

Powoli dopinamy kwestie organizacyjne związane ze szkoleniem Test-Driven Development. Zainteresowanych szczegółami odsyłam do oficjalnej strony szkolenia.
Polecam szybkie rejestrowanie się, ponieważ ze względu na to, że szkolenie będzie w formie warsztatów, liczba miejsc jest ograniczona!

czwartek, 4 marca 2010

Szkolenie Test-Driven Development

Szkolenie ze Zwinnego Rozwijania Oprogramowania zostało dobrze przyjęte przez uczestników. Dało nam to dużo radości, satysfakcji, jak również motywacji do tego, aby kontynuować dzielenie się swoją wiedzą i umiejętnościami.

Tym razem chcemy przekazać wiedzę, która wydaje się nam, zespołowi Pragmatists, bardzo cenna. Test-Driven Development, czyli sposób programowania, w którym zaczynamy od wyrażenia w kodzie naszych intencji, wywiera ogromny pozytywny wpływ na naszą pracę. Bezpieczniejsze, pewniejsze kodowanie. Rzadsze korzystanie z debuggera. Mniej defektów. Lepszy design. Czytelniejszy kod. Krótko mówiąc - zupełnie inaczej niż kiedyś i bardzo przyjemnie.

Zatem zapraszamy na szkolenie, gdzie podzielimy się naszym doświadczeniem ze stosowania TDD w prawdziwych, komercyjnych projektach. Adresowane jest do programistów i architektów (wymagana znajomość Javy). Możecie liczyć na solidną porcję wiedzy, ale przede wszystkim mnóstwo ćwiczeń, warsztatów, oglądania i pisania kodu. Czas trwania to 3 dni, od 19 do 21 kwietnia 2010. Więcej szczegółów zamieścimy już wkrótce.

Szkolenie poprowadzi wraz ze mną Michał Margiel, pasjonat Javy, współorganizator Javarsovii, codziennie test-drive'ujący mnóstwo dobrego kodu.

Poniżej treści, które zamierzamy poruszyć, a większość z nich także przećwiczyć praktycznie podczas warsztatów.

Wprowadzenie do TDD

  • czym jest TDD (cykl Red-Green-Refactor, porównanie do test-last)
  • korzyści ze stosowania TDD
  • TDD a projektowanie (design)
  • TDD a BDD (Behaviour-Driven Design)
  • rodzaje i poziomy testów: jednostkowe, integracyjne, akceptacyjne itp.

Mechanika TDD

  • wprowadzenie do JUnit
  • nazewnictwo testów
  • wsparcie przez IDE (integracja z JUnit, automatyczne refaktoryzacje, skróty klawiaturowe)
  • dyscyplina i rytm
  • mocki (Mock Objects)
    • tworzenie ręczne
    • frameworki (szczególny nacisk na Mockito)
    • inne rodzaje zaślepek (Test Doubles)

Projektowanie obiektowe

  • projektowanie pod kątem testów (Testability)
  • zasady wspierające dobry design (SOLID principles, Inversion of Control/Dependency Injection, powiązania, spójność)

Pokonywanie przeszkód

  • tworzenie test-driven warstwy dostępu do danych
  • tworzenie test-driven interfejsu użytkownika (GUI)

Praca z odziedziczonym kodem (Legacy Code)

  • jak pracować bezpiecznie (testy charakteryzacyjne, zmienne ułatwiające śledzenie)
  • techniki rozcinania zależności (dependency breaking)
  • identyfikacja punktów newralgicznych

Gdzie iść dalej

  • utrzymywanie testów
  • różne podejścia (BDD, ATDD, podejście klasyczne, podejście mockowe)
  • programowanie w parach
  • pokrycie kodu testami (code coverage)
  • wprowadzanie TDD w zespole