Mockito - unit test framework
Rád by som týmto článkom naviazal na zaujímavý článok ohľadom unit testov v Jave od Mateja.
Rád vám predstavím ďalšiu často používaný framework pre písanie unit testov v Jave a tým je Mockito.
Mockito slúži pre mockování tried. (Pr. Bean vo spring) Mockování je proces, kedy nie je volaná konkrétnej inštancie danej triedy, ale jej Mock.
Mock je náhrada za reálny objekt pre získavanie rôznych informácií o volaní daného objektu (pr. Počet volaní nejaké jeho metódy ...) Celý princíp je založený na návrhovom vzore proxy.
Sedláčka povedané: reálny objekt nahradíte objektom, ktorý zbiera štatistiku o objekte.
Postupne si prejdeme jednotlivé časti frameworku Mockito:
Runner
- spúšťa testovacie triedy, píše sa pomocou anotácie nad názov triedy
- až JUnit5 bude vedieť spúšťať viac runerov v jednej testovacej triede
@RunWith(MockitoJUnitRunner.class)
spustí MockitoAnnotations.initMocks (this) a nainicializuje kontext s mock komponentmi.
Zapne používanie @Mock:
@RunWith(PowerMockRunner.class)
silnejšie ako predošlí runner, dovoľuje Mocková aj statická dáta (static).
Skôr než ak ho použijete radšej sa zamyslite, či je váš návrh správny!
Anotácie
@Mock vytvorí Mock z daného objektu objektom (proxy)
@InjectMock vykoná injekciu na základe typu a vytvorí testovateľné inštanciu
@Spy vlezie priamo do inštancie - dobré po počítanie času v metóde a dedičnosť. Opäť na Vás ale apelujem - ak musíte použiť @Spy, zvážte predtým zmenu návrhu!
@Spy private EntityService entityService = new EntityServiceImpl(); doReturn(null).when(entityService).findByName(anyString());
WHEN vracia hodnoty (výnimka), ak je zavolaná daná metóda z Mock objektu.
základné volania:
when(carDao.findAll()).thenReturn(new ArrayList<Car>());
pre metódy, čo vracia void:
doThrow(IllegalStateException.class).when(carDao).create(Mockito.any(Car.class));
volanie danej metódy viackrát (pri prvom volaní vráti car, pri ďalšom vyhodí výnimku):
when(carService.findOne(any(Long.class))) .thenReturn(car) .thenThrow(IllegalStateException.class);
MOCKITO MATCHERS zástupky za konkrétnej inštancie
matcher | vysvetlenie |
---|---|
any () | zodpovedá typu inštancie |
eq () | zodpovedá danej inštanciu |
anyLong () | zodpovedá typu Long |
gt () | väčšia ako ... |
startsWith () | začína na ... |
intThat () | vracia int hodnotu |
argThat () | pre vlastné matcher |
VERIFY kontroluje volanie metódy v Mock objekte (koľkokrát bola volaná, ...)
základné volanie (metóda bola zavolaná raz):
verify(carDao).delete(Mockito.eq(id));
kontrola počtu volaní (metóda bola zavolaná dvakrát):
verify(carService, times(2)).findAll(any(Car.class));
kontrola nezavolaní metódy:
verify(carService, Mockito.never()).create(Mockito.any(Attribute.class));
kontrola nie viacero volanie. Skontroluje, že už viackrát nebolo volané:
verifyNoMoreInteractions(CarDao);
Captor dokáže odchytnout hodnoty pri volanie metódy vnútri testované metódy
// inicializace ArgumentCaptor<type> captor = ArgumentCaptore.forClass(type); // použití captor.capture() //odchytne hodnotu captor.getValue() //vrátíodchycenou hodnotu
príklad:
ArgumentCaptor<Car> carCaptor = ArgumentCaptor.forClass(Car.class);
when(carDao.create(carCaptor.capture())).thenReturn(car);
hasSetIdentNumber(carCaptor.getValue());
Špecialitky
kontrola poradie volania service:
InOrder inOrder = Mockito.inOrder(changeStrategy, noChangeStrategy); inOrder.verify(changeStrategy).execute(eq(car)); inOrder.verify(noChangeStrategy).execute(eq(car));
kontrola hodnôt (Captor) dvojného priechodu jednej metódy:
ArgumentCaptor<Car> carCaptor = ArgumentCaptor.forClass(Car.class); verify(carDao, times(2)).create(carCaptor .capture()); hasSetIdentNumber(carCaptor.getAllValues().get(0)); hasSetIdentNumber(carCaptor.getAllValues().get(1));
Poďme sa pozrieť na príklad z praxe
Zadanie:
Chceme si evidovať záznamy o ceste a prípadné finančné zľavy na rôzne cesty. Pre zjednodušenie pôjde o otestovanie metódy pre ukladanie záznamu o ceste.
Analýza:
Doménový objekt Journey reprezentuje cestu. Cesta je z nejakého miesta na nejaké miesto v daný čas a stála x peňazí (€).
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...
Trieda JourneyDao sa stará o prácu s databázou pre objekt Journey. V našom prípade slúži tiež pre vytvorenie Mock.
public interface JourneyDao { void save(Journey journey);
Trieda BonusService sa stará o prácu s bonusmi (cestovné zľavy). V našom prípade slúži tiež pre vytvorenie Mock.
public interface BonusService { /** * calculates bonus * * @param from city * @param to city * @return special bonus in this */ BigDecimal getBonus(String from, String to);
Najdôležitejšie je JourneyService, ktorá predstavuje testovanú triedu. Obsahuje jedinú metódu create (), ktorá vypočíta bonus za cestu a uloží ju do databázy.
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
Testovacie trieda obsahuje dva testy (s bonuse, bez bonusu). V jednom teste bonus nie je nájdený, v druhom teste je bonus nájdený.
//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); } }
Priložený súbor obsahuje tento posledný príklad.
Záverom ešte pár všeobecných rád na písanie testov:
- píšte krásnu rozprávku a nie bibliu, čo má veľa výkladu - Váš kód po Vás bude niekto čítať (i testy), snažte sa mu to pochopenie uľahčiť
- testujte všetku "zložitú" logiku - Slovíčko zložitú je na zváženie, ja osobne jednoriadkové metódy netestuje
- unit test by mal testovať čiernu skrinku, nie integráciu - Ak testovaná trieda využíva inú triedu, použite Mock objekt
- píšte krásne mená testov - Až po Vás niekto test bude čítať z názvu by mal vedieť, čo test robí
- nerobte moc dlhé testovacie triedy, rolovanie nemá nikto rád - Viac testov možno rozdeliť do viacerých tried
To je pre dnešok všetko - ďakujem za pozornosť.
V budúcej lekcii, Java spustenie - JVM argumenty , si predvedieme využitie JVM parametrov.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 30x (7.28 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Java