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ć :)
