piątek, 20 marca 2015

Test Doubles - z czym to się je?

o czym dzisiaj pomówimy?

Coś ostatnio niewiele kodu było na blogu, więc postanowiłem to trochę nadrobić. Tak więc dzisiaj czeka na Was jego odrobinę większa porcja.
Dzisiaj chciałbym Wam opowiedzieć trochę o Test Doubles. Czym są? Do czego je wykorzystujemy? Dlaczego to robimy?
A po krótkim teoretycznym wstępie przyjdzie pora na to, co programiści lubią najbardziej – kod :)

Tak więc, aby nie przedłużać, zaczynajmy…

test doubles – że co?

Celem Test Doubles jest zastąpienie na czas testów, prawdziwych zależności testowanego obiektu czymś, co możemy w pełni kontrolować. Decydujemy się na taki zabieg wtedy, gdy nie możemy albo z pewnych względów nie chcemy korzystać z prawdziwych zależności (np. gdy w obiekcie zależnym „zbyt dużo się dzieje”).
Test Doubles nie zawsze muszą zapewniać taką samą funkcjonalność, jaką dostarcza prawdziwy obiekt. Muszą jednak dostarczać wymagane API, tak aby obiekt klasy testowanej był przekonany, że korzysta z realnych zależności.

Jest kilka typów Test Doubles, które możemy wykorzystać w zależności od potrzeb:
  • Stub – z tego typu korzystamy wtedy, kiedy zachowanie klasy testowanej zależy od wartości zwracanych przez metodę innego obiektu.
    Aby móc w pełnie przetestować naszą funkcjonalność chcemy być pewni tego, co zostanie zwrócone. Nie chcemy zależeć od żadnych wartości przychodzących z zewnątrz. W takich przypadkach korzystamy ze Stuba, dzięki którego użyciu możemy zapewnić, że zwrócona wartość będzie dokładnie tą, którą określimy.

  • Spy – Jeżeli obiekty klasy testowanej wywołują metody innych obiektów nie spodziewając się odpowiedzi, a jedynie przekazując odpowiedzialność za dalsze działanie, to czasami zależy nam na tym, aby mieć pewność, że dana metoda została wywołana z oczekiwanymi przez nas parametrami. Aby to sobie umożliwić używamy Spy. Jest to nasz „punkt obserwacyjny”, który pozwala nam zweryfikować nasze założenia.

  • Mock Object – jest to coś, co można nazwać połączeniem Stub’a ze Spy’em. Z tego typu korzystamy wtedy, gdy zależy nam na konkretnej odpowiedzi w przypadku otrzymania konkretnych wartości. Sprowadza się to m.in. do tego, że zamockowaną metodę możemy w jednym teście wywoływać wielokrotnie, z różnymi parametrami otrzymując przy tym różne odpowiedzi.

  • Fake Object – czasami rzeczywistą zależność możemy zastąpić lżejszą implementacją, która dostarcza wymaganą funkcjonalność, ale bez żadnych efektów ubocznych. Prostota obiektu sprawia, że z dużą pewnością możemy polegać na jego zachowaniu i poprawnym działaniu bez konieczności definiowania założeń (jak w przypadku Mock Object i Stub) za każdym razem.

  • Dummy Object – czasami nie zależy nam na przekazaniu obiektu, który dostarcza jakąś funkcjonalność, a na samym przekazaniu jego instancji. Nie zależy nam na implementacji, a na samej obecności. W takich wypadkach wykorzystujemy Dummy Object.

Dobrze, to skoro już wiemy o czym mowa, to pora przejść do przykładów.

tell me a story about dummy things

Ok, wystarczy już tego dobrego. Pora na dawkę kodu, który zobrazuje to, o czym pisałem wyżej.

Dzisiaj stworzymy sobie bloga. Co prawda zdecydujemy się na wiele uproszczeń, ale cel taki właśnie nam przyświeca :) A żeby mieć bloga to potrzebujemy… ? Zapewne Bloga:
public class Blog {

}

Ok, wydaje mi się, że całkiem nieźle zaczęliśmy. Jakby jednak na to nie patrzeć, aby blog miał sens (i czytelników :) to na blogu powinny się znajdować jakieś artykuły.
Cóż, my na obecną chwilę nie mamy żadnego, więc:
public void shouldReturnZeroIfNoArticles() {
    assertThat(blog.numberOfArticles(), is(0));
}

Naszym pierwszym celem jest to, by ten test zaczął przechodzić.
Jak wiecie tak się nie stanie, bo brakuje nam metody, którą na szybko możemy dodać:
public int numberOfArticles() {
    return 0;
}
Na obecne potrzeby to jest dokładnie to czego potrzebujemy.

Wróćmy jednak do naszych artykułów, których na razie brak.
Wymaganie jednak jest, więc napiszmy test, który zweryfikuje czy funkcjonalność istnieje:
@Test
public void shouldReturnNumberOfArticlesOnBlog() {
    blog.addArticle(new Article());
    blog.addArticle(new Article());
    blog.addArticle(new Article());

    assertThat(blog.numberOfArticles(), is(3));
}

Jak wcześniej, kod bez zmian nie przejdzie, ale po dodaniu kilku linijek w kodzie:
public class Blog {

    private final List<Article> articles = new ArrayList<>();

    public void addArticle(Article article) {
        articles.add(article);
    }

    public int numberOfArticles() {
        return articles.size();
    }
}

wszystko zaczyna nabierać kształtów, a co więcej – zaczyna działać :)

Parametr przekazywany do metody add() to wcześniej wspomniany nasz Dummy Object. Nie jesteśmy zainteresowani konkretnym artykułem. Nie interesuje nas jego funkcjonalność. Dla nas jest istotne, że jest.
Dla dokładniejszego zobrazowania można zastosować drobną refaktoryzację:
@Test
public void shouldReturnNumberOfArticlesOnBlog() {
    blog.addArticle(getDummyArticle());
    blog.addArticle(getDummyArticle());
    blog.addArticle(getDummyArticle());

    assertThat(blog.numberOfArticles(), is(3));
}

private Article getDummyArticle() {
    return new Article();
}

Swoją drogą taki zabieg ma jeszcze dwie istotne zalety warte odnotowania.
Po pierwsze, wyraźnie zaznaczamy, że testowana funkcjonalność nie jest nastawiona na jakąkolwiek interakcję z przekazywanym obiektem (a przynajmniej nie w tym momencie). Dla nas liczy się tylko, że został przekazany.
Po drugie, jeżeli pojawią się jakieś nowe wymagania i konstruktor zostanie zmieniony (np. będzie konieczne przekazanie jakichś parametrów), to w efekcie będziemy zmuszeni zmodyfikować tylko jedną linijkę kodu.

to może coś opublikujemy?

Stworzenie artykułu na blogu nie jest równoznaczne jednak z tym, że ktokolwiek go przeczyta, aby to było możliwe musimy go jeszcze opublikować.

Na początek, po wszelkich edycjach i korektach, zadowoleni z siebie stwierdzamy, że artykuł jest gotowy do publikacji. Jak jednak możemy przypuszczać, sam się nie opublikuje, więc warto to połączyć z jakimś zdarzeniem.
Wzbogaćmy więc nasz kod:
public class Article {
    public PublishedArticleEvent readyToPublish() {
        return new PublishedArticleEvent(this);
    }
}

A teraz spójrzmy na funkcjonalność z powrotem od strony testów. Spróbujmy opublikować nasz artykuł:
@Test
public void shouldPublishArticle() {
    PublishedArticleEvent event = new PublishedArticleEvent(new Article());

    event.process();
}

Super. Tylko skąd my będziemy wiedzieli, że nasz artykuł został opublikowany? Możemy np. zmienić jego stan a następnie go sprawdzić:
@Test
public void shouldPublishArticle() {
    Article article = new Article();
    PublishedArticleEvent event = new PublishedArticleEvent(article);

    event.process();

    assertThat(article.isPublished(), is(true));
}

Żeby taki test nam przeszedł, to nasz kod może przyjąć następującą formę:
public class Article {
    private boolean isPublished = false;

    public PublishedArticleEvent readyToPublish() {
        return new PublishedArticleEvent(this);
    }

    public void published() {
        isPublished = true;
    }

    public boolean isPublished() {
        return isPublished;
    }
}

public class PublishedArticleEvent {
    private final Article article;

    public PublishedArticleEvent(Article article) {
        this.article = article;
    }

    public void process() {
        // publishing
        article.published();
    }
}

Chyba jednak nie do końca tak chcemy to zrobić. Dlaczego? Bo powstała nam metoda isPublished(), która została stworzona wyłącznie na potrzeby testu. Takiego kodu tworzyć nie chcemy.

Usuńmy więc tą metodę i spróbujmy zmienić nasz test w taki sposób, aby była ona zbędna:
@RunWith(MockitoJUnitRunner.class)
public class PublishedArticleEventTest {
    @Spy private Article article;

    @Test
    public void shouldPublishArticle() {
        PublishedArticleEvent event = new PublishedArticleEvent(article);

        event.process();

        verify(article).published();
    }
}
Świetnie. Zakładam, że wykorzystanie adnotacji @Spy wystarczająco wyraźnie pokazuje jaki podtyp Test Doubles został tutaj wykorzystany :)
Jak widzicie, dodatkowym plusem jest odchudzenie kodu klasy Article, bo usunęliśmy z niej również zbędną metodę isPublished().

jedno zdarzenie to niewiele…

Możemy śmiało założyć, że typów zdarzeń będzie wiele, a z pewnością możliwość istnienia wielu instancji klasy PublishedArticleEvent jest czymś, co musimy mieć na uwadze.
W takim wypadku przydałby się nam jakiś twór do zarządzania nimi.
Myślę, że moglibyśmy zacząć od czegoś takiego:
public interface Queue {
    void add(Event event);
    
    void processAll();
}

public interface Event {
    public void process();
}

Nasza klasa PublishedArticleEvent musi oczywiście nowoutworzony interfejs Event implementować.

Dobra, to teraz testy (właściwie to od nich powinniśmy zacząć :), które musimy uzupełnić kodem:
public class QueueTest {
    private Queue queue;

    @Test
    public void shouldAddEvent() {

    }

    @Test
    public void shouldProcessAll() {

    }
}

Jakby na to nie patrzyć, to ciężko na obecną chwilę wyobrazić sobie testowanie dwóch metod osobno. Myślę jednak, że bez problemu umożliwimy sobie coś takiego.

Dobrze byłoby, gdyby wszystkie zdarzenia nie były trzymane jedynie w pamięci. Chcielibyśmy je gdzieś przechowywać. Idąc w tym kierunku powstaje nam taka klasa:
public class StoredQueue implements Queue {
    private Store store = new DbStore();

    @Override
    public void add(Event event) {
        store.add(event);
    }

    @Override
    public void processAll() {
        for (Event event : store.getAll()) {
            event.process();
        }
    }
}

public interface Store {
    void add(Event event);

    List<Event> getAll();
}

Ok, ale czy to w jakikolwiek sposób ułatwia nam życie? Właściwie to nie. Jednak pamiętacie może o czymś takim jak Dependency Injection? Dokładnie! To może jeszcze jedna drobna zmiana:
public class StoredQueue implements Queue {
    private Store store;

    public StoredQueue(Store store) {
        this.store = store;
    }

    //code
}

Lepiej, czyż nie? To wróćmy teraz do naszych testów.
public class QueueTest {
    private FakeStore store;
    private Queue queue;

    @Before
    public void init() {
        store = new FakeStore();
        queue = new StoredQueue(store);
    }

    @Test
    public void shouldAddEvent() {
        queue.add(dummyEvent());
        queue.add(dummyEvent());

        assertThat(store.size(), is(2));
    }

    private Event dummyEvent() {
        return mock(Event.class);
    }

    @Test
    public void shouldProcessAll() {
        Event event1 = spiedEvent();
        Event event2 = spiedEvent();
        store.add(event1);
        store.add(event2);

        queue.processAll();

        verify(event1).process();
        verify(event2).process();
    }

    private Event spiedEvent() {
        return mock(Event.class);
    }

    private class FakeStore implements Store {
        private final List<Event> events = new ArrayList<>();

        @Override
        public void add(Event event) {
            events.add(event);
        }

        @Override
        public List<Event> getAll() {
            return events;
        }

        public int size() {
            return events.size();
        }
    }
}
Wykorzystaliśmy tutaj Fake Object aby przetestować naszą funkcjonalność. Nasz Fake jest niczym więcej tylko wrapperem na zwyczajną listę, ale właśnie o to chodzi w tego typu klasach – aby ich obiekty dostarczały w najprostszy sposób gwarantowane przez implementowany interfejs zachowanie.

Jeżeli popatrzycie uważnie na testy to z pewnością zauważycie, że do implementacji pierwszego testu z powodzeniem moglibyśmy wykorzystać Spy’a, a do drugiej…

daj mi to co chcę…

No właśnie, drugą metodę możemy przetestować za pomocą kolejnego typu Test Double – mam na myśli Stub.
A tak ten test mógłby wyglądać:
@RunWith(MockitoJUnitRunner.class)
public class QueueTest {
    private Store store;
    private Queue queue;

    @Before
    public void init() {
        store =  mock(Store.class);
        queue = new StoredQueue(store);
    }

    @Test
    public void shouldProcessAll() {
        Event event1 = spiedEvent();
        Event event2 = spiedEvent();
        stub(store.getAll()).toReturn(asList(event1, event2));

        queue.processAll();

        verify(event1).process();
        verify(event2).process();
    }

    private Event spiedEvent() {
        return mock(Event.class);
    }
}
Dla sprostowania i wyeliminowania wszelkich dwuznaczności chcę zaznaczyć, że stworzenie „mocka” obiektu Store za pomocą metody mock() jest konieczne, aby można było skorzystać z jego instancji jako ze stuba. Metoda mock() jedynie tworzy obiekt i nie jest powiązana w żaden sposób z Mock Object.

i procesowania ciąg dalszy

Wyobraźcie sobie, że jednym z wymagań jest możliwość takiego przeprocesowania, aby odpalić jedynie te zdarzenia, które spełniają określone wymogi. W naszym przypadku mogłoby chodzić o opublikowanie wszystkich artykułów, których czas publikacji nie jest ustawiony bądź nie wskazuje na przyszłość.

Czyli chcielibyśmy, aby coś takiego było możliwe:
@RunWith(MockitoJUnitRunner.class)
public class QueueTest {
    @Mock private EventPredicate predicate;
    @Mock private Store store;
    private Queue queue;

    @Before
    public void init() {
        queue = new StoredQueue(store);
    }

    @Test
    public void shouldProcessOnlyThoseWhichSatisfiesPredicate() {
        Event event1 = spiedEvent();
        Event event2 = spiedEvent();
        Event event3 = spiedEvent();
        given(store.getAll()).willReturn(asList(event1, event2, event3));
        given(predicate.isSatisfiedBy(event1)).willReturn(true);
        given(predicate.isSatisfiedBy(event2)).willReturn(false);
        given(predicate.isSatisfiedBy(event3)).willReturn(true);

        queue.processAll(predicate);

        then(event1).should().process();
        then(event2).should(never()).process();
        then(event3).should().process();
    }

    private Event spiedEvent() {
        return mock(Event.class);
    }
}
Gdzie instancja EventPredicate jest przykładem na wykorzystanie Mock Object.

Implementację kodu, który pomoże spełnić to wymaganie pozostawiam Wam.

kilka słów na…

Zdaję sobie sprawę, że dzisiejszy wpis wyszedł „odrobinę” przydługi, jednak mam nadzieję, że udało mi się zarówno krótko omówić i wytłumaczyć czym są Test Doubles oraz zademonstrować ich wykorzystanie w praktyce.

Jest kilka rzeczy, które warto zapamiętać i które mogą rzucić się w oczy po ponownym przejrzeniu kodu przedstawionego w przykładach:
  • Dzięki wykorzystaniu Test Doubles możemy testować unit w izolacji.
  • Często do testowania wystarczą nam interfejsy, nie potrzebujemy konkretnych implementacji.
  • Jedną rzecz można przetestować na naprawdę wiele sposobów.
To tyle na dzisiaj. Tym jednak, którzy nadal są głodni wiedzy i chcą jeszcze bardziej ją poszerzyć namawiam do zapoznania się z informacjami znajdującymi się na tej stronie xUnit Patterns.

Udanego testowania :)


Brak komentarzy:

Prześlij komentarz