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.

Osztálydiagram

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.