Mockito - unit test framework

Java Pro pokročilé 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ženo 3x (7.28 kB)
Aplikace je včetně zdrojových kódů v jazyce java

 

  Aktivity (2)

Článek pro vás napsal Petr Kunčar
Avatar
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ě.

Jak se ti líbí článek?
Celkem (1 hlasů) :
55555


 


Miniatura
Předchozí článek
Unit testy v Javě a JUnit
Miniatura
Všechny články v sekci
Java - Pro pokročilé
Miniatura
Následující článek
Spring - Transakce v Javě

 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!