Mockito - unit test framework
Rád bych tímto článkem navázal na zajímavý článek ohledně unit testů v Javě od Matěje.
Rád vám představím další často používaný framework pro psaní unit testů v javě a tím je Mockito.
Mockito slouží pro mockování tříd. (př. bean ve spring) Mockování je proces, kdy není volána konkrétní instance dané třídy, ale její Mock.
Mock je náhražka za reálný objekt pro získávání různých informací o volání daného objektu (př. počet volání nějaké jeho metody…) Celý princip je založený na návrhovém vzoru proxy.
Sedlácky řečeno: reálný objekt nahradíte objektem, který sbírá statistiku o objektu.
Postupně si projdeme jednotlivé části frameworku Mockito:
Runner
- spouští testovací třídy, píše se pomocí anotace nad název třídy
- až JUnit5 bude umět spouštět více runnerů v jedné testovací třídě
@RunWith(MockitoJUnitRunner.class)
spustí MockitoAnnotations.initMocks(this) a nainicializuje kontext s mock komponentami.
Zapne používání @Mock:
@RunWith(PowerMockRunner.class)
silnější než předešlí runner, dovoluje mockovat i statická data (static).
Dříve než-li ho použijete radši se zamyslete, jestli je váš návrh správný!
Anotace
@Mock vytvoří Mock z daného objektu objektem (proxy)
@InjectMock provede injekci na základě typu a vytvoří testovatelnou instanci
@Spy vleze přímo do instance - dobré po počítání času v metodě a dědičnost. Opět na Vás ale apeluji - pokud musíte použít @Spy, zvažte předtím změnu návrhu!
@Spy private EntityService entityService = new EntityServiceImpl(); doReturn(null).when(entityService).findByName(anyString());
WHEN vrací hodnoty (výjimka), pokud je zavolána daná metoda z Mock objektu.
základní volání:
when(carDao.findAll()).thenReturn(new ArrayList<Car>());
pro metody, co vrací void:
doThrow(IllegalStateException.class).when(carDao).create(Mockito.any(Car.class));
volání dané metody vícekrát (při prvním volání vrátí car, při dalším vyhodí výjimku):
when(carService.findOne(any(Long.class))) .thenReturn(car) .thenThrow(IllegalStateException.class);
MOCKITO MATCHERS zástupky za konkrétní instance
matcher | vysvětlení |
---|---|
any() | odpovídá typu instance |
eq() | odpovídá dané instanci |
anyLong() | odpovídá typu Long |
gt() | větší než ... |
startsWith() | začíná na ... |
intThat() | vrací int hodnotu |
argThat() | pro vlastní matcher |
př.: intThat(is(greaterThan(9000))) - hodnota je větší jak 9000
VERIFY kontroluje volání metody v Mock objektu (kolikrát byla volána, ...)
základní volání (metoda byla zavolána jednou):
verify(carDao).delete(Mockito.eq(id));
kontrola počtu volání (metoda byla zavolána dvakrát):
verify(carService, times(2)).findAll(any(Car.class));
kontrola nezavolaní metody:
verify(carService, Mockito.never()).create(Mockito.any(Attribute.class));
kontrola ne vícero volání. Zkontroluje, že už vícekrát nebylo voláno:
verifyNoMoreInteractions(CarDao);
Captor dokáže odchytnout hodnoty při volání metody uvnitř testované metody
// inicializace ArgumentCaptor<type> captor = ArgumentCaptore.forClass(type); // použití captor.capture() //odchytne hodnotu captor.getValue() //vrátíodchycenou hodnotu
příklad:
ArgumentCaptor<Car> carCaptor = ArgumentCaptor.forClass(Car.class);
when(carDao.create(carCaptor.capture())).thenReturn(car);
hasSetIdentNumber(carCaptor.getValue());
Specialitky
kontrola pořadí volání service:
InOrder inOrder = Mockito.inOrder(changeStrategy, noChangeStrategy); inOrder.verify(changeStrategy).execute(eq(car)); inOrder.verify(noChangeStrategy).execute(eq(car));
kontrola hodnot (Captor) dvojného průchodu jedné metody:
ArgumentCaptor<Car> carCaptor = ArgumentCaptor.forClass(Car.class); verify(carDao, times(2)).create(carCaptor .capture()); hasSetIdentNumber(carCaptor.getAllValues().get(0)); hasSetIdentNumber(carCaptor.getAllValues().get(1));
Pojďme se podívat na příklad z praxe
Zadání:
Chceme si evidovat záznamy o cestě a případné finanční slevy na různé cesty. Pro zjednodušení půjde o otestování metody pro ukládání záznamu o cestě.
Analýza:
Doménový objekt Journey reprezentuje cestu. Cesta je z nějakého místa na nějaké místo v daný čas a stála x peněz (KČ).
public class Journey { private Long id ; private String from ; private String to ; private LocalDateTime dateTime ; //in Czech Crone private BigDecimal price = BigDecimal. ZERO ; //gettry, settry, equals, hashCode...
Třída JourneyDao se stará o práci s databází pro objekt Journey. V našem případě slouží také pro vytvoření Mock.
public interface JourneyDao { void save(Journey journey);
Třída BonusService se stará o práci s bonusy (cestovní slevy). V našem případě slouží také pro vytvoření Mock.
public interface BonusService { /** * calculates bonus * * @param from city * @param to city * @return special bonus in this */ BigDecimal getBonus(String from, String to);
Nejdůležitejší je JourneyService, která představuje testovanou třídu. Obsahuje jedinou metodu create (), která vypočte bonus za cestu a uloží ji do databáze.
public class JourneyServiceImpl implements JourneyService { @Autowired private JourneyDao journeyDao; @Autowired private BonusService bonusService; public void save(Journey journey) { BigDecimal bonus = bonusService.getBonus(journey.getFrom(), journey.getTo()); if (bonus != null ) { journey.addBonus(bonus); } journeyDao.save(journey); }
Samotný test s využitím Mockita
Testovací třída obsahuje dva testy (s bonuse, bez bonusu). V jednom testu bonus není nalezen, v druhém testu je bonus nalezen.
//inicializuje MOCKITO @RunWith (MockitoJUnitRunner.class) public class SaveJourneyTest { //testovaná třída @InjectMocks private JourneyService journeyService = new JourneyServiceImpl(); //vytvoření mock objektu @Mock private JourneyDao journeyDao ; @Mock private BonusService bonusService ; @Test public void whenSaveJourneyWithoutBonus_thenCalculatePriceWithoutBonus() { Journey journey = JourneyFactory.createDefaultJourney(); // výsledek by měla být nezměněná cena, neboť neexistuje žádný bonus BigDecimal expected = new BigDecimal( JourneyFactory.DEFAULT_PRICE.doubleValue()); // neexistuje bonus: jeli metoda getBonus volána, vrací null when (bonusService.getBonus( eq(JourneyFactory.DEFAULT_FROM ), eq(JourneyFactory.DEFAULT_TO ))) .thenReturn(null); //testovaní metody journeyService.save(journey); //kontrola správanosti výsledků checkCalling(expected); } @Test public void thenSaveJourneyWithBonus_thenCalculatePriceWithBonus() { Journey journey = JourneyFactory.createDefaultJourney (); //tentokráte musí výsledek být i s bonusem BigDecimal expected = new BigDecimal( JourneyFactory.DEFAULT_PRICE.subtract( JourneyFactory.DEFAULT_BONUS).doubleValue()); //po zavolání getBonus je poslát uživateli hodnota bonusu when(bonusService.getBonus( eq(JourneyFactory.DEFAULT_FROM), eq(JourneyFactory.DEFAULT_TO))) .thenReturn(JourneyFactory.DEFAULT_BONUS); journeyService.save(journey); checkCalling(expected); } // skontroluje správnost výsledku: správné volání metod a uložení správné ceny private void checkCalling(BigDecimal expected) { //kontrola, že methoda pro získání bonusu byla vůbec zavolána verify(bonusService).getBonus(anyString(), anyString()); // odchytnem si objekt, který ve výsledku jde do uložení ArgumentCaptor<Journey> journeyCaptor = ArgumentCaptor.forClass(Journey.class); verify (journeyDao).save(journeyCaptor.capture()); // skontrolujem, že byla na objektu nastavena správná cena assertEquals (expected, journeyCaptor.getValue().getPrice()); // kontrola, že více toho nebylo zavoláno, než bylo potřeba verifyNoMoreInteractions(bonusService, journeyDao); } }
Přiložený soubor obsahuje tento poslední příklad.
Závěrem ještě pár obecných rad ke psaní testů:
- pište krásnou pohádku a ne bibli, co má hodně výkladu - Váš kód po Vás bude někdo číst (i testy), snažte se mu to pochopení usnadnit
- testujte veškerou “složitou” logiku - Slovíčko složitou je na zvážení, já osobně jednořádkové metody netestuji
- unit test by měl testovat černou skříňku, ne integraci - Pokud testovaná třída využívá jinou třídu, použijte Mock objekt
- piště krásné jména testů - Až po Vás někdo test bude číst z názvu by měl vědět, co test dělá
- nedělejte moc dlouhé testovací třídy, rolování nemá nikdo rád - Více testů lze rozdělit do více tříd
To je pro dnešek vše - děkuji za pozornost.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 30x (7.28 kB)
Aplikace je včetně zdrojových kódů v jazyce Java