poniedziałek, 28 listopada 2011

Hands on mocking

1. Wprowadzenie

Dokument przybliża idee i metody stosowane przy tworzeniu testów jednostkowych, do symulowania obiektów. Tytuł wynika z popularności pojęcia "Mock", ale nie oznacza, że opisane zagadnienia ograniczają się do tej techniki. Precyzyjne uwagi dotyczące terminologii znajdą się punkcie 2.

Pierwsza część opisuje powody stosowania takich rozwiązań w testach jednostkowych i zyski, które można dzięki nim osiągnąć. W dalszej części przedstawię dwa ogólne podejścia do wykorzystania mocków w procesie wytwarzania oprogramowania.

2. "Doubles" - why do we need them?

a. Terminologia

Zanim przedstawione zostaną motywacje stosowania mockowania i innych technik, konieczne jest usystematyzowanie terminologii, używanej w dalszej części artykulu. Określenie "double" zastosowane w tytule punktu wywodzi się z przemysłu filmowego i oznacza powtórzenie ujęcia. Zostało sformułowane w odniesieniu do oprogramowanie i wykorzystane przez Fowlera w (1).

double - ogólne pojęcie dotyczące każdego sposobu na symulowanie obiektów w testach (mock, stub, fake jest doublem)

dummy - obiekty inicjowane, ale nigdy nie używane. Stosowane do np. do wypełnienia listy argumentów funkcji.

fake - obiekt zaimplementowany, ale tymczasowy - nie nadający się do zastosowania w końcowym produkcie (np. baza danych przechowywana w pamięci). Spotkałem się również z używaniem pojęcia fake w roli takiej jak double, czyli ogólnego określenia. Zazwyczaj znaczenie wynika z kontekstu, proponuję jednak stosować jedną wersję.

stub - funkcja lub pole, które zostało spreparowane do zwracania wybranych, uproszczonych lub trudnych do osiągnięcia wyników. Przesłania on rzeczywistą implementację i pozwala na osiąganie efektów przydatych przy testowaniu (np. "przyspieszanie czasu", zwaracanie specyficznych błędnych wyników, etc.). W dalszych częściach zostaną przedstawione przykłady

mock - najbardziej trafnym będzie określenie - obiekt opakowujący inny obiekt przekazany jako parametr i pozwalający na opisywanie jego zachowania. Za jego pomocą możliwe jest nie tylko "zmuszenie" funkcji do zwrócenia określonego wyniku, ale też określenie jakie metody mockowanej klasy mają zostać wywołane, ile razy i w jakiej kolejności. O ile zatem stub pozwala na testowanie obiektu ze względu na jego stan (state), to mock daje również możliwość przetestowania jego zachowania (behavior).

SUT - System Under Test, czyli obiekt, funkcja, funkcjonalność, która poddawana jest testowaniu

b. Doubles - motywacje

Ostatnie z pojęć przedstawionych w poprzednim punkcie wskazuje na główną ideę testów jednostkowych. Taki test ma weryfikować działanie jednej spójnej części danego systemu, odizolowanej od pozostałych. W każdym teście mamy zatem do czynienia z SUTem oraz otoczeniem, które umożliwia przeprowadzenie testu.

Otoczenie powinno być określone, przewidywalne i możliwe do wygenerowania na żądanie przy przeprowadzeniu testu. Mocki i inne narzędzia tego typu pozwalają na osiągnięcie tego efektu.

c. Przykłady zastosowań wg (2)

Każdy z poniższych przykładów dotyczy sytuacji, gdy testowany SUT, wymaga do poprawnego działania któregoś z wymienionych elementów. Nie należy myilć z sytuacją, gdy testowany jest sam element (nie mockujemy bazy, którą chcemy przetestować). Poniższe punkty nie wyczerpują oczywiście możliwych zastosowań i stanowi raczej podpowiedź przy podejmowaniu decyzji o używaniu Mocków.

i. Symulowanie interakcji z użytkownikiem (kliknięcie myszką, przycisk, etc.)
ii. Web service's
iii. Dostęp do bazy danych
iv. Dostęp do sturktury katalogów, plików i innych elementó związanych z systemem operacyjnym
v. Dostęp do części systemów znajdujących się za fasadą (wzorzec)
vi. Dostęp do klas zdalnych

3. Przykładowa aplikacja

W dalszej części artykułu pojawią się przykłady oparte na grze OneCardMaster. Aplikacja zawiera następujące klasy:

a. OneCardMasterGame (IOneCardMasterGame)
Przykładowe testy będą się odnosić do metod tej klasy, stąd przedstawiony poniżej interfejs. Pozostałe będą w różny sposób symulowane.


namespace OneCardMaster
{
public interface IOneCardMasterGame
{
void AddPlayer(IPlayer player);
void RemovePlayer(IPlayer player);
void SetPlayerLimit(int number);
void PerformGame();
IPlayer GetWinner();
IList GetGameState();
int GetPlayerCount();
}
}

b. Deck (IDeck)
c. Game (IGame)
d. Player (IPlayer)
e. enumy GameColors, GameValues

4. Mocks a TDD

Punkt ma na celu określenie miejsca mocków w strukturze przykładowego testu. Standardowy poprawny test ma strukturę określoną m.in. w (3). Testowanie metody dodawania zawodników, może wyglądać np. w następujący sposób:

/// <summary>
///A test for AddPlayer
///</summary>
[TestMethod()]
public void AddPlayerSimpleTest()
{
// set up
Player player = new Player();
OneCardMasterGame oneCardMasterGame = new OneCardMasterGame();

// execute
oneCardMasterGame.AddPlayer(player);

// verify
Assert.AreEqual(1, oneCardMasterGame.GetPlayerCount());
}

Wyróżnione zostały trzy części. W pierwszej z nich ("set up" lub "arrange") przygotwywane są obiekty, w drugiej ("execute", "act") wykonywana jest testowana operacja, w ostatniej weryfikowane są stany obiektów i w konsekwencji poprawność testu.

Mocki można wykorzystać w pierwszej fazie, do przygotowania środowiska testowego. Test może wyglądać następująco:


///
///A test for AddPlayer
///

[TestMethod()]
public void AddPlayerTest()
{
// set up
IPlayer playerMock = MockRepository.GenerateStrictMock();

// execute
oneCardMasterGame.AddPlayer(playerMock);

// verify
Assert.AreEqual(1, oneCardMasterGame.GetPlayerCount());
}


Jedyna różnica to utworzenie do testów wirtualnego zawodnika, który nie musi posiadać żadnej implementacji. Jak dotychczas różnica nie jest wielka, chociaż można już zauważnyć pierwsze korzyści. Możemy testować metodę AddPlayer, bez jakiejkolwiek implementacji interfejsu IPlayer. Co więcej, sam interfejs nie musi mieć zdefiniowanych żadnych metod.

5. BDD - inny punkt widzenia

Poprzedni punkt zakończyłem stwierdzeniem, że Mocków można używać wobec obiektów które dopiero powstaną. Daje to ciekawe możliwości nie tylko dla testowania, ale też dla projektowania. W TDD testy mają poprzedzać implementację. Rozszerzeniem tej idei jest BDD, czyli Behavior Driven Development.

Załóżmy, że tworzę aplikacjęOneCardMaster. Przyjąłem, że konieczna jest klasa OneCardMasterGame i metody, w tym m.in. PerformGame. Domyślam się, że będę potrzebował w tej grze zawodników (IPlayer) oraz talii (IDeck). Tworzę zatem nastęujący test:



/// <summary>
///A test for PerformGame
///</summary>
[TestMethod()]
public void PerformGameTest()
{
// set up
MockRepository mocks = new MockRepository();
IDeck deck = mocks.StrictMock<IDeck>();
IPlayer player1 = mocks.StrictMock<IPlayer>();
IPlayer player2 = mocks.StrictMock<IPlayer>();

oneCardMasterGame = new OneCardMasterGame(deck);
oneCardMasterGame.AddPlayer(player1);
oneCardMasterGame.AddPlayer(player2);

// execute

oneCardMasterGame.PerformGame();

// verify
mocks.VerifyAll();
}


Wg TDD powinienem teraz stworzyć taką implementację metody PerformGame, żeby test przechodził pomyślnie. Nie jest to jednak możliwe, bo IPlayer ani IDeck nie mają nie zostały jeszcze zaimplementowane. Zastanawiam się zatem jak powinna wyglądać gra i przekładam ten opis na język testu. Gra polega na wybraniu karty z talii i przydzieleniu jej do każdego zawodnika. Tworzę zatem następujący opis:


/// <summary>
///A test for PerformGame
///</summary>
[TestMethod()]
public void PerformGameTest()
{
// set up
MockRepository mocks = new MockRepository();
IDeck deck = mocks.StrictMock<IDeck>();
IPlayer player1 = mocks.StrictMock<IPlayer>();
IPlayer player2 = mocks.StrictMock<IPlayer>();

oneCardMasterGame = new OneCardMasterGame(deck);
oneCardMasterGame.AddPlayer(player1);
oneCardMasterGame.AddPlayer(player2);

// execute

Expect.Call(() => deck.Shuffle());

Expect.Call( deck.GetCard() ).Return( null );
Expect.Call( () => player1.SetCard(null) ).IgnoreArguments();

Expect.Call( deck.GetCard() ).Return( null );
Expect.Call( () => player2.SetCard(null)).IgnoreArguments();

mocks.ReplayAll();
oneCardMasterGame.PerformGame();

// verify
mocks.VerifyAll();
}

Gdy do interfejsów IDeck i IPlayer dodam namiastki metod odpowiednio Shuffle, GetCard dla IDeck i SetCard dla IPlayer test przejdzie pomyślnie. Mogę powtarzać ten proces dla każdej funkcji z interfejsu IOneCardMasterGame i w ten sposób stworzyć pełny obraz interfejsów podrzędnych (IDeck, IPlayer). Następnie powtarzam wszystkie kroki dla niższej warstwy. Np. Za pomocą interfejsu IDeck określam interjes IGame. W ten sposób tworzę aplikację od tworów najogólniejszych do najkonkretniejszych, czyli dość naturalnie. Od pomysłu, do jego realizacji.

Jest to tylko bardzo pobieżny opis tematu BDD a właściwie tylko jednego jego aspektu związanego właśnie z Mockami. Sądzę jednak, że idea jest na tyle ciekawa, że warto poświęcić jej trochę uwagi. Dalsze informacje na temat BDD można znaleźć m.in. w (4) czy innych tekstach tego autora.



6. Rhino Mock, techniki

Dokumentacja Rhino Mocka jest dostępna pod adresem (5), w tym punkcie przedstawię najważniejsze funkcje na przykładowych testach.

a. Podstawowe zastosowanie mocków


/// <summary>
///A test for PerformGame
///</summary>
[TestMethod()]
public void PerformGameTest()
{
// set up
MockRepository mocks = new MockRepository();
IDeck deck = mocks.StrictMock<IDeck>();
IPlayer player1 = mocks.StrictMock<IPlayer>();
IPlayer player2 = mocks.StrictMock<IPlayer>();

oneCardMasterGame = new OneCardMasterGame(deck);
oneCardMasterGame.AddPlayer(player1);
oneCardMasterGame.AddPlayer(player2);

// execute

Expect.Call(() => deck.Shuffle());

Expect.Call( deck.GetCard() ).Return( null );
Expect.Call( () => player1.SetCard(null) ).IgnoreArguments();

Expect.Call( deck.GetCard() ).Return( null );
Expect.Call( () => player2.SetCard(null)).IgnoreArguments();

mocks.ReplayAll();
oneCardMasterGame.PerformGame();

// verify
mocks.VerifyAll();
}


Powyższy przykład posłuży do wyjaśnienia najważniejszych mechanizmów mockowania. Jak już zostało wspomniane, mocki pozwalają na weryfikowanie nie tylko stanu obiektów, ale też ich zachowania. W przykładzie używamy konstrukcji Expect.Call, która pozwala na zdefiniowanie jaka metoda ma zostać wywołana. W tym wypadku zastosowane zostały "restrykcyjne mocki" (StrictMock), więc polecenia muszą być wykonywane w określonej kolejności i ilość.

Po serii Expect.Call dzięki której opisujemy działanie jakiego oczekujemy od testowanej funkcji wykonywane jest polecenie mocks.ReplyAll(), które kończy proces opisywania. Na zakończenie wywoływane jest polecenie mocks.VerifyAll(), które jest odpowidnikiem Assert dla normalnych testów. Sprawdzane jest w tym momencie, czy wszystkie polecenia zostaly wykonane.

Wywołanie Expect.Call(), a następnie IgnoreArguments() pozwala na sprawdzanie wywołania funkcji niezależnie od argumentów.

b. StrictMock, DynamicMock, PartialMock


[TestMethod()]
public void TestExampleWithStrictMock()
{
MockRepository mocks = new MockRepository();
IPlayer demo = mocks.StrictMock<IPlayer>();
mocks.ReplayAll();
demo.Compare(null);
mocks.VerifyAll();//will never get here
}

Zastosowanie "restrykcyjnego mocka" powoduje, że wykonane mają być dokładnie wskazane funkcje i tylko one. W przykładzie próbujemy wykonać funkcję, która nie została przewidziana przed wywołaniem mocks.ReplayAll(), stąd wyjątek i test nie przechodzi.

Gdyby zamiast mocks.StrictMock() użyć mocks.DynamicMock() test zakończyłby się sukcesem. Dynamiczne mocki weryfikują czy określone metody zostały wykonane, ale nie wykluczają użycia innych metod.

PartialMocks pozwala na mockowanie części klasy. Np. gdy chcemy testować klasę abstrakcyjną, której część metod jest zaimplementowana, możliwe jest mockowanie tylko tych metod które nie mają implementacji.

c. Stub


/// <summary>
///A test for GetWinner
///</summary>
[TestMethod()]
public void GetWinnerTest()
{
// set up

IDeck deck = MockRepository.GenerateStrictMock<IDeck>();
IPlayer player1 = MockRepository.GenerateStub<IPlayer>();
IPlayer player2 = MockRepository.GenerateStub<IPlayer>();
IPlayer player3 = MockRepository.GenerateStub<IPlayer>();

oneCardMasterGame = new OneCardMasterGame(deck);
oneCardMasterGame.AddPlayer(player1);
oneCardMasterGame.AddPlayer(player2);
oneCardMasterGame.AddPlayer(player3);

// execute

player1.Stub(val => player1.Compare(val)).Return(true);
oneCardMasterGame.PerformGame();
IPlayer winner = oneCardMasterGame.GetWinner();

// verify

Assert.AreSame(player1, winner);

}


W powyższym przykładzie testowana jest funkcja wskazująca zwycięscę gry, czyli zawodnika, który wylosował najwyższą kartę. Zawodnicy mogą być porównywani za pomocą metody Compare. Zwycięscą powinien być zawodnik, który w każdym porównaniu jest lepszy od przeciwnika. W tym scenariuszu, nie ma potrzeby sprwadzania kolejności wykonywanych funkcji. Tworzymy fikcyjnego "kandydata na zwycięscę" i określamy, że wygrywa on przy porównaniu z każdym. Zatem funkcja GetWinner powinna wskazać player1 jako wygranego gry.

d. Ograniczenia funkcji

W poprzednich przykładach została użyta metod IgnoreArguments(). Możliwe jest też dodawanie ograniczeń do argumentów, bez dokładnego ich specyfikowania, za pomocą metody Constraint().

e. Powtarzanie wywołań
Za pomocą LastCall.Repeat.Once() możliwe jest ponowne wywołanie ostatniego zapytania. LastCall.Repeat zawiera również inne metody pozwalające na automatyczne używanie pewnych funkcji.


Dalsze przykłady można znaleźć w (5) i (6). Sądzę też, że w miarę rozwoju naszej aplikacji, każdy spotka się z sytuacjami wymagającymi bardziej złożonego testu, a zatem również specyficznego zastosowania Mocków. Warto takie przykłady zbierać i uzupełnić poniższy dokument.

7. Podsumowanie

Dokument przedtawia tematykę mockowania dość pobieżnie, ale sądzę że wystarczająco, aby z jego pomocą zacząć to narzędzie stosować. Pojawia się jednak pytanie - gdzie i kiedy mocków używać? Fowler w dokumencie (1) zestawia dwa podejścia które nazywa "calssical TDD" i "mockist TDD". Pierwsze oznacza korzystanie mocków, tylko tam, gdzie stosowanie normalnych implementacji byłoby problematyczne, niewygodne, kosztowne. Czyli najczęściej w przypadkach wymienionych w punkcie 3. Praktycy "mockist TDD" promują stosowanie mocków wszędzie poza kluczową, testowaną funkcjonalnością (można użyć kolokwialnego określenia, poza SUTem). Fowler wskazuje jednak, że o ile podobny pomysł wygląda wspaniale jako artykuł na konferencję, to w przypadku tworzenia biznesowego kodu jest już zdecydowanie trudniejszy i żmudniejszy do zastosowania. Uważam, że przy wyborze obiektów do mockowania powinniśmy się kierować przede wszystkim zdrowym rozsądkiem, zasadmi czystości i czytelności kodu. Podobnie w przypadku decydowaniu pomiędzy Mockami a Stubami. Jeśli nie jest nam potrzebne weryfikowanie kolejności wykonywanych poleceń, Stub w zupełniości wystarczy. Przy praktyce mockowania i wątpliwościach dotyczących składni polecam przede wszystkim dokumenty (5) i (6). Więcej o zaletach "mockist TDD" można znaleźć w (7).

Bibliography
1. Fowler, Martin. Martin Fowler's software development blog. [Online] martinfowler.com/articles/mocksArentStubs.html.
2. Miller, Jeremy. Code better. Why and When to Use Mock Objects. [Online] http://codebetter.com/jeremymiller/2005/12/20/why-and-when-to-use-mock-objects/.
3. Weibel, Patrick. assumption of advancement. How to structure code in an unit test. [Online] http://blog.eweibel.net/?p=200.
4. North, Dan. Introducing BDD. [Online] http://dannorth.net/introducing-bdd/.
5. Rahien, Ayende. [Online] http://ayende.com/wiki/Rhino+Mocks+Documentation.ashx.
6. Aniserowicz, Maciej. Maciej Aniserowicz o programowaniu. [Online] http://www.maciejaniserowicz.com/post/2009/09/29/Spis-tresci-Cykl-o-mock-objects-i-Rhino-Mocks.aspx.
7. Steve Freema, Tim Mackinnon, Nat Pryce, Joe Walnes. Mock Roles, Not Objects (OOPSLA 2004). [Online] 2004. http://www.jmock.org/oopsla2004.pdf.

0 komentarze:

Prześlij komentarz

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Online Project management