IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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í MockitoAnnota­tions.initMoc­ks(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(gre­aterThan(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ů:

  1. 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
  2. testujte veškerou “složitou” logiku - Slovíčko složitou je na zvážení, já osobně jednořádkové metody netestuji
  3. unit test by měl testovat černou skříňku, ne integraci - Pokud testovaná třída využívá jinou třídu, použijte Mock objekt
  4. 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á
  5. 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 27x (7.28 kB)
Aplikace je včetně zdrojových kódů v jazyce Java

 

Všechny články v sekci
Testování v Javě
Článek pro vás napsal Petr Kunčar
Avatar
Uživatelské hodnocení:
2 hlasů
Nejlepší práce je taková, která vás baví. Nejlepší manželka je taková, co vás chápe. Nejlepší rodina je taková, co vás podporuje. Nejlepší relax je v přírodě. Nejlepší, co pro svět můžeš udělat, je řešit problémy rychle a elegantně.
Aktivity