Mockito
Frissítve: 2019. július 22.
A héten JMS-en kommunikáló alkalmazást kellett tesztelnem. Azt kellett ellenőrizni, hogy az üzleti logika a megfelelő ponton küld-e JMS üzenetet. Ehhez nem akartam valami JMS provider (pl. WebSphere 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 használatával implementáltam.
Használhattam volna a Mockrunner eszközt, mely egy teljes JMS providert
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
) kollekciókkal 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 üzenetküldést hívó logikát.
A JMS műveletek már külön osztályba voltak csoportosítva, elválasztva az üzleti logikától, így csupán ezt kellett lecserélnem. Ez az osztály egy interfészt valósított meg, melyre a tesztelendő osztályom dependency injectionnel hivatkozott. A komponensek ezen laza csatolása lehetővé teszi (hiszen az egyik csak a másiknak az interfészét ismeri), hogy az implementációt lecseréljük egy tesztelésre előkészített objektumra.
Használhattam volna ún. stub osztályt, ami az interfésznek 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 Swinghez 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 írjuk le, hogy mit válaszoljon, és ellenőrzi, hogy az adott objektummal helyes interakció történt-e. A stubnál annyival több, hogy a teszteseből mondjuk meg hogy viselkedjen, mit rögzítsen és mit ellenőrizzen.
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észeinek készít fake
implementációt. Ugyanígy
pl. a spring-test
modul korábbi neve spring-mock
volt, pedig az is a
Java EE interfészeihez készít fake implementációt.
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. A Mockito framework íróját a az EasyMock és jMock keretrendszer ihlette, 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 Mockitonál ezzel szemben a futtatás előtt stubbolsz (adod meg, hogy hogy legyen a metódus implementálva), és az után teszel fel kérdéseket, azaz ellenőrzöd le a működést.
A poszthoz a GitHubon található a példaprojekt.
Az üzleti logikát az EmployeeService
tartalmazza, mely elment egy Employee
objektumot, majd erről küld egy üzenetet a MessageSender
interfész
sendMessage()
metódusát hívva.
Nézzük meg, hogy hozzunk létre egy mock MessageSender
implementációt,
hogyan adjunk át az EmployeeService
példánynak, és hogy ellenőrizzük,
hogy az meghívja-e a sendMessage()
metódust.
public class EmployeeServiceTest {
@Test
void testCreateEmployee() {
// Mock objektum előállítása
var messageSender = mock(MessageSender.class);
// Tesztelendő objektum példányosítása
var employeeService = new EmployeeService(messageSender);
// Futtatás
employeeService.createEmployee("John Doe");
// Ellenőrzés
verify(messageSender).sendMessage(any());
}
}
Az ellenőrzés során vizsgálva lett, hogy az EmployeeService
meghívja-e
a MessageSender
sendMessage()
metódusát.
A Mockito JUnit 5 MockitoExtension
osztálya segítségével ez még egyszerűbb,
ugyanis a @Mock
annotáció hatására injektálásra kerül egy mock objektum a
tesztesetbe, valamint az @InjectMock
annotáció hatására példányosítja a
tesztelendő osztályt, konstruktorban átadva a mock objektumot.
@ExtendWith(MockitoExtension.class)
public class EmployeeServiceTest {
@Mock
MessageSender messageSender;
@InjectMocks
EmployeeService employeeService;
@Test
void testCreateEmployeeWithExtension() {
employeeService.createEmployee("John Doe");
verify(messageSender).sendMessage(any());
}
}
Amennyiben a sendMessage()
metódus hívásának paraméterét egyéb
ellenőrzéseknek is alá akarjuk vetni, az ArgumentCaptor
osztályt kell
használnunk.
ArgumentCaptor<Employee> employeeArgumentCaptor = ArgumentCaptor.forClass(Employee.class);
employeeService.createEmployee("John Doe");
verify(messageSender).sendMessage(employeeArgumentCaptor.capture());
assertEquals("John Doe", employeeArgumentCaptor.getValue().getName());
A Mockito ezen kívül rengeteg egyéb dolgot is tud, konkrét osztályt is tud mockolni, a hívások számát és sorrendjét ellenőrizni, valamint akár nem mockolt 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.