2010. február 20., szombat

Mockito

A héten JMS-en kommunikáló alkalmazást kellett tesztelnem. Azt kellett ellenőrizni, hogy a sorokon beérkező XML formátumú üzeneteket megfelelőképpen képes fogadni, feldolgozni, ellenőrizni. A választ szintén JMS-en küldi el. Ehhez nem akartam valami JMS provider (pl. WebSphre MQ) telepíteni, helyette csak a fogadást, feldolgozást és ellenőrzést végző modult (osztályt) szerettem volna teszteseteknek alávetni. A környezet Spring, a teszteseteket JUnit 4-ben implementáltam.

Használhattam volna a Mockrunner eszközt, mely egy teljes JMS provider-t szimulál, azaz a JMS API-t stub objektumokkal (lásd később) valósítja meg. Ez gyakorlatilag egy olyan megvalósítás, mely a Destination objektumokat (Queue, Topic) List-ekkel valósítja meg, melyekbe a JMS API hívásokkal lehet üzeneteket tenni, de vannak külön metódusok, melyekkel aztán ezeket ellenőrizni lehet a tesztesetben.

Ez azonban már inkább integrációs teszt lett volna, és nem az üzenetkezelést akartam tesztelni, csupán az üzeneteket feldolgozó logikát.

A JMS műveletek már külön osztályba voltak csoportosítva (JmsCommService), elválasztva a feldolgozástól, így csupán ezt kellett lecserélnem. A Spring-hez illeszkedve ezen osztály egy interfészt valósított meg (CommService), melyre a tesztelendő osztályom (DefaultProcessService implements ProcessService) dependency injection-nel hivatkozott. A komponensek ezen laza csatolása lehetővé teszi (hiszen az egyik csak a másiknak az interfészét ismeri), hogy a konténernek (jelen esetben a pehelysúlyú Spring) megadjuk, hogy JUnit tesztelésnél ne az alapértelmezett implementációt töltse be, hanem helyette egy tesztelésre előkészített osztályt.

Használhattam volna un. stub osztályt, ami az interfésznek (CommService) egy saját implementációja, és az adott tesztesetre van felkészítve. Ennek viszont több hátulütője is van. Egyrészt bizonyos logikát külön osztályba kell szervezni, így a teszt kód nem csak a JUnit tesztben van, így kevésbé átlátható. Valamint tesztesetenként (vagy legalábbis bizonyos csoportonként) különböző stub osztályokat kellett volna létrehoznom. Ekkor, ha egy interfésznek sok metódusa van, javasolt absztrakt Adapter osztályt készíteni a Swing-hez hasonlóan (nincs köze az adapter tervezési mintához), mely üres metódusokkal implementálja az interfészt, és ebből kell csak leszármaztatni, és a teszteléshez szükséges metódusokat implementálni.

Az "The art of unit testing" könyv megkülönbözteti a stub objektumtól a mock objektum fogalmát, mely a teszteset során azt ellenőrzi, hogy az adott objektummal történt-e tényleges interakció. A stub-nál annyival több, hogy saját magára is állapít meg feltételeket, melyeknek a teszt során teljesülnie kell, pl. milyen metódusai lettek meghívva, hányszor, milyen paraméterekkel, stb.

Martin Fowler oldalán ennél több definíció is található. Gerard Meszaros a "XUnit Test Patterns" könyvben ezen segéd objektumokat "Test double" gyűjtőnéven illeti, és a következő kategóriákba sorolja: dummy, fake, stub, spy, mock.

A Mockrunner framework elnevezéséből is látszik, hogy a fogalmakat gyakran keverik. A Mockrunner a JMS API interfészeit stub-olja. Ugyanígy pl. a spring-test modul korábbi neve spring-mock volt, pedig az is a Java EE interfészeit stub-olja.

A stub/mock objektumokat használhatjuk, ha

  • Az eredeti objektum állapota nem megjósolható, külső tényezőktől függő
  • Az eredeti objektum felépítése bonyolult, lassú, sok erőforrás igénylő művelet
  • Az eredeti objektumok külső erőforrásokhoz fér hozzá, melyek állapotát nehéz befolyásolni. Pl. hálózati kapcsolatot használó objektum esetén mock objektum szimulálhatja a hálózat megszakadást, stb.

Mock framework használatával megtakaríthatjuk, hogy az interfészeket magunk implementáljuk. Ezt megteszi a framework, az általunk megadott szabályok alapján. A két legelterjedtebb mock framework az EasyMock és jMock. A Mockito framework íróját is ezek ihlették, de ezeknél is egyszerűbb API-val rendelkező eszközt készített. Az előbbieket expect-run-verify library-knek nevezi. Azoknál először definiálni kell, hogy mit vársz el, majd lefuttatni a tesztet, és ellenőrizni az elvártakat. A Mockito-nál ezzel szemben a futtatás előtt stub-bolsz (adod meg, hogy hogy legyen a metódus implementálva), és az után teszel fel kérdéseket, azaz ellenőrzöl.

Az alkalmazás egyszerűsített osztálydiagramja az alábbi ábrán látható.

Osztálydiagram

A DefaultProcessService kapja az üzenetet ("ping"), és meghívja a CommService sendMessage() metódusát, átadva egy szöveges üzenetet ("pong").

Amennyiben a processService.processMessage() metódus egy String-et várna, egyszerű lenne a teszt metódusunk.

...
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

public class ProcessServiceTest {

@Test
public void testProcessMessage() throws Exception {
 // Tesztelendő objektum előkészítése
 DefaultProcessService processService = new DefaultProcessService();

 // Mock objektum előállítása
 CommService commService = mock(CommService.class);

 // Mock objektumra hivatkozás beállítása
 processService.setCommService(commService);

 // Futtatás
 processService.processMessage("ping");

 // Ellenőrzés
 verify(commService).sendMessage(eq("pong"));
}

}

Hogy a kód könnyebben olvasható legyen, statikusan importálva vannak a Mockito metódusai. Az egyszerűség kedvéért nem a Spring SpringJUnit4ClassRunner osztálya futtatja a tesztesetet, hanem szerepel benne a DefaultProcessService osztály példányosítása. A teszteset a CommService interfészből készít egy mock objektumot, és erre állítja a DefaultProcessService hivatkozását. Aztán lefuttatja a processMessage metódust. Ez a háttérben meghívja a a mock DefaultProcessService sendMessage() metódusát. Ezt követi az ellenőrzés. Ez azt mondja, hogy a hívás során meg kellett hívni a CommService sendMessage() metódusát úgy, hogy a paraméternek meg kellett egyeznie a "pong" szöveggel.

Nézzük a tesztesetet, ha a processMessage() TextMessage paramétert vár.

...
import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

public class ProcessServiceTest {

@Test
public void testProcessMessage() throws Exception {
 // Eredeti objektum példányosítása
 DefaultProcessService processService = new DefaultProcessService();

 // Mock objektum előállítása
 TextMessage message = mock(TextMessage.class);
 CommService commService = mock(CommService.class);

 // Mock objektumra hivatkozás beállítása
 processService.setCommService(commService);

 // Stub-bolás
 when(message.getText()).thenReturn("ping");

 // Futtatás
 processService.processMessage(message);

 // Ellenőrzés
 verify(commService).sendMessage(eq("pong"));
}

}

Itt a teszteset a TextMessage interfészből is csinál egy mock objektumot, és ezután stub-bolja azt, méghozzá úgy, hogy amennyiben meghívják a getText() metódusát, adja vissza a "ping" String-et.

Amennyiben a sendMessage() metódus hívásának paraméterét egyéb ellenőrzéseknek is alá akarjuk vetni, az ArgumentCaptor-t kell használnunk.

ArgumentCaptor argument = ArgumentCaptor.forClass(String.class);
verify(commService).sendMessage(argument);
String param = argument.getValue();
assertEquals("pong", param);

A Mockito ezen kívül rengeteg egyéb dolgot is tud, konkrét osztályt is tud mock-olni, a when feltételben a paraméter értékétől függően adni vissza eredményt, a hívások számát és sorrendjét ellenőrizni, valamint akár nem mock-olt valós objektumok hívásait is lehallgatni (Spy), stb.

A mock objektumoknak persze hátulütőik is akadnak, könnyen beleeshetünk a csapdába, hogy nem black box tesztelést végzünk az interfész alapján, hanem az implementáció alapján nézzük meg, hogy melyik metódust kellett volna hívni, és hányszor. Így egy refaktoring után hibára futhatnak a teszteseteink, és ilyenkor azokat is karban kell tartani. Gyakorlatilag nem az objektum viselkedését teszteljük, hanem annak kölcsönhatásait más objektumokkal.

10 megjegyzés:

  1. erről pár napja leveleztem valakivel, én tartom h teljes félreértés van a dologban és egész más a stub és a mock közti különbség. abban a vitában is az 'art of unit testing' volt a félremagyarázásért a bűnös..

    VálaszTörlés
  2. Személy szerint nekem mindegy. :) Sajnos ebben a világban nincsenek egységes elnevezések, ezért volt jó a tervezési minták megjelenése, mert adott ilyeneket. Nem volt nagy találmány, mindenki ismerte azokat a megoldásokat, de végre nevet kaptak. A lényeg, hogy a fejlesztők ismerjék a különböző megközelítéseket, és mielőtt elkezdenek beszélgetni, egyeztessék előtte melyikük mire gondol egy név alatt. Én az irodalomból indultam ki, és sajnos már más könyv is átvette ezt a megközelítést, pl. a Next Generation Java Testing is. És igazából az a definíció lesz elfogadott, amelyiket többen idézik. :)

    Szerinted mi a különbség?

    VálaszTörlés
  3. ez igaz h a népszerűbb lesz az "igaz" ha elég sokáig mondják:)

    idézem amit múltkor írtam az említett levélben:
    "- szerintem stub az, ami forráskód szinten megjelenik (XStub.java)
    - szerintem mock az, ami forráskód szinten nem jelenik meg, egy
    mocking framework generálja automatikusan (mock(X.class).when(..) stb)"
    ".. update: előszedtem a Fowler cikket és én abban is azt látom leírva
    amit én gondolok.. ha megnézed, a Stub ez:
    public class MailServiceStub implements MailService { .. }

    a mock meg ez:
    Mock mailer = mock(MailService.class);

    innen józan ésszel nekiugorva látszik hogy a tesztelés szempontjából a
    végeredmény közelítőleg ugyanaz, a különbség h stubbal több energiával
    de pontosabb eredményt tudsz elérni, mockkal meg hirtelen valami
    egyszerűbbet. Az esetek 95%-ában pedig édesmindegy melyiket használod,
    csak mockot kisebb energia.."

    ami a további kavart okozza h a 'stub' szót igeként is használják angolul, tehát lehet stubbing method callról beszélni amit egy mockkal érsz el, ettől ez még nem stub csak stubbing :)

    VálaszTörlés
  4. Ezt is simán elfogadom, de nekem megfér mindkét definíció egymás mellett. :)
    A cikkben is, mikor az elnevezéseket taglaltam (Mockrunner, spring-mock), pont erre utaltam.
    Viszont szerintem a mock-olósdi sokkal alkalmasabb az 'Art of unit testing' féle technikára, ugyanis stub-ban nem szoktunk olyanokat implementálgatni, hogy meghívja e az adott metódust, és mennyiszer, milyen sorrendben. Az vár értéket amit lehett assert-elni, meg visszaad értéket, ami meg használ a tesztelendő objektum.
    Szóval a te definíciód szerintem feltétele a másodiknak, a második már ahhoz rak hozzá plusz feltételt.
    Persze nem biztos, hogy jogosan. :) Szóval a két definíció szerintem nem független egymástól, az egyik a másiknak a továbbvitele.
    És ráadásul a másik definíciót erősen használva lehet csapdába esni, mert ott jön elő, hogy nem az input/output-ra figyelsz, hanem a belső működésre, hiszen mindegy, hogy valamit hányszor hív meg, a lényeg, hogy az adott input-ra a kívánt ouput-ot adja.

    VálaszTörlés
  5. http://www.youtube.com/watch?v=GJOcvQpMu2A 1:10

    VálaszTörlés
  6. Ha lesz egy kis időm, csinálok egy kutatást a neten, hogy kiknek mit jelentenek ezek a fogalmak, és honnan származnak az elnevezések és az egyéni (félre?)értelmezések.

    VálaszTörlés
  7. Nagyon jó a cikk.
    Az osztálydiagrammot melyik programmal készítetted?

    VálaszTörlés
  8. Szia! Köszönöm! A kép URL-jéből kiderül, hogy ez egy webes alkalmazással készül: http://yuml.me/

    VálaszTörlés
  9. Ruby Best Practices (http://sandal.github.com/rbp-book/pdfs/ch01.pdf), 17. oldal. A stub amikor felülvágja az eredeti metódust konkrét kóddal, a mock amikor a mock frameworkkel átdefiniálja a hívás eredményét.

    (és ha már belenéztek, a 16. oldalon ahogy szórja tömegesen a teszteket a QuestionerTest osztályban kicsit bdd stílusban önleíróan, iszonyú tömören és olvashatóan, azon behasaltam. rubysoktól érdemes tesztelést tanulni nem .NET-es könyvből.. :)

    VálaszTörlés