Un ghid al practicianului de testare unitară pentru Mockito de zi cu zi
Publicat: 2022-03-11Testarea unitară a devenit obligatorie în era Agile și există multe instrumente disponibile pentru a ajuta la testarea automată. Un astfel de instrument este Mockito, un cadru open source care vă permite să creați și să configurați obiecte batjocorite pentru teste.
În acest articol, vom aborda crearea și configurarea modelelor și utilizarea lor pentru a verifica comportamentul așteptat al sistemului testat. De asemenea, ne vom scufunda puțin în interiorul lui Mockito pentru a înțelege mai bine designul și avertismentele acestuia. Vom folosi JUnit ca un cadru de testare unitară, dar din moment ce Mockito nu este legat de JUnit, puteți urma chiar dacă utilizați un cadru diferit.
Obținerea lui Mockito
A obține Mockito este ușor în zilele noastre. Dacă utilizați Gradle, este o chestiune de a adăuga această singură linie la scriptul dvs. de compilare:
testCompile "org.mockito:mockito−core:2.7.7"
În ceea ce privește cei ca mine, care încă preferă Maven, adaugă doar Mockito la dependențele tale astfel:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>
Desigur, lumea este mult mai largă decât Maven și Gradle. Sunteți liber să utilizați orice instrument de management de proiect pentru a prelua artefactul borcan Mockito din depozitul central Maven.
Se apropie de Mockito
Testele unitare sunt concepute pentru a testa comportamentul unor clase sau metode specifice, fără a se baza pe comportamentul dependențelor acestora. Deoarece testăm cea mai mică „unitate” de cod, nu trebuie să folosim implementări reale ale acestor dependențe. În plus, vom folosi implementări ușor diferite ale acestor dependențe atunci când testăm diferite comportamente. O abordare tradițională, bine-cunoscută, este crearea de „stubs” – implementări specifice ale unei interfețe potrivite pentru un anumit scenariu. Astfel de implementări au de obicei o logică hard-coded. Un stub este un fel de test dublu. Alte tipuri includ falsuri, batjocuri, spioni, manechine etc.
Ne vom concentra doar pe două tipuri de duble de testare, „mocks” și „spioni”, deoarece acestea sunt foarte folosite de Mockito.
Batjocuri
Ce este batjocorirea? Evident, nu este locul unde îți faci de râs de colegii tăi dezvoltatori. Batjocorirea pentru testarea unitară este atunci când creați un obiect care implementează comportamentul unui subsistem real în moduri controlate. Pe scurt, mock-urile sunt folosite ca înlocuitor pentru o dependență.
Cu Mockito, creezi un simulacro, îi spui lui Mockito ce să facă atunci când anumite metode sunt apelate pe el și apoi folosești instanța simulată în testul tău în loc de lucrul real. După testare, puteți interoga simularea pentru a vedea ce metode specifice au fost numite sau puteți verifica efectele secundare sub formă de stare schimbată.
În mod implicit, Mockito oferă o implementare pentru fiecare metodă de simulare.
Spionii
Un spion este celălalt tip de test dublu pe care Mockito îl creează. Spre deosebire de batjocuri, crearea unui spion necesită o instanță pe care să o spioneze. În mod implicit, un spion deleagă toate apelurile de metodă către obiectul real și înregistrează ce metodă a fost numită și cu ce parametri. Acesta este ceea ce îl face un spion: spionează un obiect real.
Luați în considerare utilizarea batjocurilor în loc de spioni ori de câte ori este posibil. Spionii ar putea fi utili pentru testarea codului moștenit care nu poate fi reproiectat pentru a fi ușor de testat, dar nevoia de a folosi un spion pentru a bate joc parțial de o clasă este un indicator că o clasă face prea mult, încălcând astfel principiul responsabilității unice.
Construirea unui exemplu simplu
Să aruncăm o privire la un demo simplu pentru care putem scrie teste. Să presupunem că avem o interfață UserRepository
cu o singură metodă de a găsi un utilizator după identificatorul său. Avem și conceptul unui codificator de parolă pentru a transforma o parolă clară într-un hash de parolă. Atât UserRepository
, cât și PasswordEncoder
sunt dependențe (numite și colaboratori) ale UserService
injectate prin intermediul constructorului. Iată cum arată codul nostru demonstrativ:
UserRepository
public interface UserRepository { User findById(String id); }
Utilizator
public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
PasswordEncoder
public interface PasswordEncoder { String encode(String password); }
Serviciu utilizator
public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }
Acest exemplu de cod poate fi găsit pe GitHub, așa că îl puteți descărca pentru revizuire alături de acest articol.
Aplicarea Mockito
Folosind codul nostru exemplu, să vedem cum să aplicăm Mockito și să scriem câteva teste.
Crearea de false
Cu Mockito, crearea unui simulacro este la fel de ușoară ca și apelarea unei metode statice Mockito.mock()
:
import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);
Observați importul static pentru Mockito. Pentru restul acestui articol, vom considera implicit acest import adăugat.
După import, ne batem joc de PasswordEncoder
, o interfață. Mockito batjocorește nu numai interfețele, ci și clasele abstracte și clasele concrete non-finale. Din cutie, Mockito nu poate bate joc de clasele finale și metodele finale sau statice, dar dacă aveți nevoie cu adevărat, Mockito 2 oferă plugin-ul experimental MockMaker.
De asemenea, rețineți că metodele equals()
și hashCode()
nu pot fi batjocorite.
Crearea de spioni
Pentru a crea un spion, trebuie să apelați metoda statică spy()
a lui Mockito și să îi transmiteți o instanță pe care să o spionați. Metodele de apelare ale obiectului returnat vor apela metode reale, cu excepția cazului în care aceste metode sunt blocate. Aceste apeluri sunt înregistrate și faptele acestor apeluri pot fi verificate (vezi descrierea suplimentară a verify()
). Hai să facem un spion:
DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));
Crearea unui spion nu diferă foarte mult de crearea unei simulari. În plus, toate metodele Mockito utilizate pentru configurarea unui simulacro sunt aplicabile și la configurarea unui spion.
Spionii sunt rar utilizați în comparație cu bateriile, dar le puteți găsi utile pentru testarea codului moștenit care nu poate fi refactorizat, unde testarea necesită o batjocură parțială. În aceste cazuri, puteți doar să creați un spion și să blocați unele dintre metodele sale pentru a obține comportamentul dorit.
Valori de returnare implicite
Apelarea mock(PasswordEncoder.class)
returnează o instanță a PasswordEncoder
. Putem chiar numi metodele sale, dar ce vor reveni? În mod implicit, toate metodele unui simulat returnează valori „neinițializate” sau „vide”, de exemplu, zerouri pentru tipurile numerice (atât primitive, cât și în casete), false pentru valori booleene și valori nule pentru majoritatea celorlalte tipuri.
Luați în considerare următoarea interfață:
interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }
Acum luați în considerare următorul fragment, care oferă o idee despre ce valori implicite să vă așteptați de la metodele unei simulari:
Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());
Metode de împingere
Machetele proaspete, nemodificate sunt utile doar în cazuri rare. De obicei, dorim să configuram simularea și să definim ce să facem atunci când sunt apelate anumite metode ale modelului. Aceasta se numește stubbing .
Mockito oferă două moduri de a împinge. Prima modalitate este „ când această metodă este apelată, atunci faceți ceva”. Luați în considerare următorul fragment:
when(passwordEncoder.encode("1")).thenReturn("a");
Se citește aproape ca în engleză: „Când se apelează passwordEncoder.encode(“1”)
, returnați a
.”
Cea de-a doua modalitate de a împinge este mai degrabă de genul „Fă ceva când metoda acestei simulari este apelată cu următoarele argumente”. Acest mod de înțepare este mai greu de citit, deoarece cauza este specificată la sfârșit. Considera:
doReturn("a").when(passwordEncoder).encode("1");
Fragmentul cu această metodă de stubbing ar citi: „Return a
when passwordEncoder
's encode()
's method is called with an argument of 1
”
Prima modalitate este considerată preferată pentru că este sigură și pentru că este mai lizibilă. Rareori, totuși, ești forțat să folosești a doua modalitate, cum ar fi atunci când împingi o metodă reală a unui spion, deoarece apelarea acesteia poate avea efecte secundare nedorite.
Să explorăm pe scurt metodele de stubbing oferite de Mockito. Vom include ambele moduri de stubbing în exemplele noastre.
Valori returnate
thenReturn
sau doReturn()
sunt folosite pentru a specifica o valoare care trebuie returnată la invocarea metodei.
//”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");
sau
//”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");
De asemenea, puteți specifica mai multe valori care vor fi returnate ca rezultate ale apelurilor consecutive de metodă. Ultima valoare va fi utilizată ca rezultat pentru toate apelurile de metodă ulterioare.
//when when(passwordEncoder.encode("1")).thenReturn("a", "b");
sau
//do doReturn("a", "b").when(passwordEncoder).encode("1");
Același lucru se poate realiza cu următorul fragment:
when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");
Acest model poate fi folosit și cu alte metode de stubbing pentru a defini rezultatele apelurilor consecutive.
Retur de răspunsuri personalizate
then()
, un alias pentru thenAnswer()
și doAnswer()
realizează același lucru, care este configurat un răspuns personalizat pentru a fi returnat atunci când o metodă este apelată, astfel:
when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");
sau
doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");
Singurul argument pe care îl ia thenAnswer()
este o implementare a interfeței Answer
. Are o singură metodă cu un parametru de tip InvocationOnMock
.
De asemenea, puteți arunca o excepție ca urmare a unui apel de metodă:
when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });
...sau apelați metoda reală a unei clase (nu se aplică interfețelor):
Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());
Ai dreptate dacă crezi că pare greoi. Mockito oferă thenCallRealMethod()
și thenThrow()
pentru a eficientiza acest aspect al testării dumneavoastră.
Apelarea metodelor reale
După cum sugerează și numele, thenCallRealMethod()
și doCallRealMethod()
apelează metoda reală pe un obiect simulat:
Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());
Apelarea unor metode reale poate fi utilă pentru simulari parțiale, dar asigurați-vă că metoda apelată nu are efecte secundare nedorite și nu depinde de starea obiectului. Dacă o face, un spion ar putea fi mai potrivit decât o simulare.
Dacă creați o simulare a unei interfețe și încercați să configurați un stub pentru a apela o metodă reală, Mockito va arunca o excepție cu un mesaj foarte informativ. Luați în considerare următorul fragment:
when(passwordEncoder.encode("1")).thenCallRealMethod();
Mockito va eșua cu următorul mesaj:
Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();
Felicitari dezvoltatorilor Mockito pentru grija suficienta pentru a oferi descrieri atat de amanuntite!
Aruncarea de excepții
thenThrow()
și doThrow()
configurează o metodă batjocorită pentru a arunca o excepție:
when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());
sau
doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");
Mockito se asigură că excepția care este aruncată este valabilă pentru acea metodă specifică stubbed și se va plânge dacă excepția nu se află în lista de excepții verificată a metodei. Luați în considerare următoarele:
when(passwordEncoder.encode("1")).thenThrow(new IOException());
Va duce la o eroare:
org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException
După cum puteți vedea, Mockito a detectat că encode()
nu poate arunca o IOException
.
De asemenea, puteți trece o clasă a unei excepții în loc să treceți o instanță a unei excepții:
when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);
sau
doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");
Acestea fiind spuse, Mockito nu poate valida o clasă de excepție în același mod în care va valida o instanță de excepție, așa că trebuie să fiți disciplinat și să nu treceți obiecte de clasă ilegale. De exemplu, următoarele vor arunca IOException
, deși encode()
nu este de așteptat să arunce o excepție verificată:
when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");
Batjocorirea interfețelor cu metode implicite
Este demn de remarcat faptul că atunci când creează un simulacro pentru o interfață, Mockito bate joc de toate metodele acelei interfețe. Începând cu Java 8, interfețele pot conține metode implicite împreună cu metode abstracte. Aceste metode sunt, de asemenea, batjocorite, așa că trebuie să aveți grijă să le faceți să acționeze ca metode implicite.
Luați în considerare următorul exemplu:
interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());
În acest exemplu, assertFalse()
va reuși. Dacă nu este ceea ce vă așteptați, asigurați-vă că Mockito a apelat la metoda reală, astfel:
AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());
Potriviri de argumente
În secțiunile anterioare, am configurat metodele noastre batjocoritoare cu valori exacte ca argumente. În aceste cazuri, Mockito doar apelează equals()
intern pentru a verifica dacă valorile așteptate sunt egale cu valorile reale.
Uneori, însă, nu știm aceste valori dinainte.
Poate că pur și simplu nu ne pasă de valoarea reală care este transmisă ca argument, sau poate dorim să definim o reacție pentru o gamă mai largă de valori. Toate aceste scenarii (și multe altele) pot fi abordate cu potriviri de argumente. Ideea este simplă: în loc să oferiți o valoare exactă, furnizați un instrument de potrivire pentru Mockito pentru a se potrivi argumentele metodei.
Luați în considerare următorul fragment:
when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));
Puteți vedea că rezultatul este același, indiferent de valoarea pe care o transmitem lui encode()
deoarece am folosit potrivirea argumentului anyString()
în prima linie. Dacă rescriem acea linie în limba engleză simplă, ar suna ca „când codificatorului parolei i se cere să codifice orice șir, apoi returnează șirul „exact”.
Mockito vă cere să furnizați toate argumentele fie prin potriviri, fie prin valori exacte. Deci, dacă o metodă are mai mult de un argument și doriți să utilizați potriviri de argumente doar pentru unele dintre argumentele sale, uitați-l. Nu poți scrie cod așa:
abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn't work. when(mock.call("a", anyInt())).thenReturn(true);
Pentru a remedia eroarea, trebuie să înlocuim ultima linie pentru a include potrivirea argumentului eq
pentru a
, după cum urmează:
when(mock.call(eq("a"), anyInt())).thenReturn(true);
Aici am folosit potrivirile argumentelor eq()
și anyInt()
, dar există multe altele disponibile. Pentru o listă completă a potrivirilor de argumente, consultați documentația de pe clasa org.mockito.ArgumentMatchers
.
Este important să rețineți că nu puteți utiliza potriviri de argument în afara verificării sau stubbing-ului. De exemplu, nu puteți avea următoarele:
//this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);
Mockito va detecta potrivirea argumentelor greșite și va lansa o InvalidUseOfMatchersException
. Verificarea cu potriviri de argumente ar trebui făcută astfel:
verify(mock).encode(or(eq("a"), endsWith("b")));
Potrivirile de argument nu pot fi folosite nici ca valoare de returnare. Mockito nu poate returna anyString()
sau orice; o valoare exactă este necesară atunci când apelurile stubbing.
Potriviri personalizate
Potrivirile personalizate vin în ajutor atunci când trebuie să oferiți o logică de potrivire care nu este deja disponibilă în Mockito. Decizia de a crea o potrivire personalizată nu ar trebui luată cu ușurință, deoarece necesitatea de a potrivi argumentele într-un mod netrivial indică fie o problemă în proiectare, fie că un test devine prea complicat.
Ca atare, merită să verificați dacă puteți simplifica un test utilizând unele dintre potrivirile argumentelor indulgente, cum ar fi isNull()
și nullable()
înainte de a scrie o potrivire personalizată. Dacă tot simțiți nevoia să scrieți o potrivire de argumente, Mockito oferă o familie de metode pentru a face acest lucru.
Luați în considerare următorul exemplu:
FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher<File> hasLuck = file -> file.getName().endsWith("luck"); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File("/deserve"))); assertTrue(fileFilter.accept(new File("/deserve/luck")));
Aici creăm potrivirea argumentului hasLuck
și folosim argThat()
pentru a transmite potrivirea ca argument unei metode batjocorite, împingând-o să returneze true
dacă numele fișierului se termină cu „noroc”. Puteți trata ArgumentMatcher
ca pe o interfață funcțională și puteți crea instanța sa cu o lambda (care este ceea ce am făcut în exemplu). Sintaxa mai puțin concisă ar arăta astfel:
ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };
Dacă trebuie să creați un potrivire de argumente care funcționează cu tipuri primitive, există câteva alte metode pentru asta în org.mockito.ArgumentMatchers
:
- charThat(Potrivire ArgumentMatcher<Personaj>)
- booleanThat(Potrivire ArgumentMatcher<Boolean>)
- byteThat(Potrivire ArgumentMatcher<Byte>)
- shortThat(Potrivire ArgumentMatcher<Short>)
- intThat(Potrivire ArgumentMatcher<Integer>)
- longThat(Potrivire ArgumentMatcher<Long>)
- floatThat(Potrivire ArgumentMatcher<Float>)
- doubleThat(Potrivire ArgumentMatcher<Double>)
Combinarea potrivirilor
Nu este întotdeauna în valoare de a crea un potrivire de argument personalizat atunci când o condiție este prea complicată pentru a fi gestionată cu potriviri de bază; uneori, combinarea potrivirilor va face șmecheria. Mockito oferă potriviri de argumente pentru a implementa operații logice comune („nu”, „și”, „sau”) pe potriviri de argumente care se potrivesc atât cu tipurile primitive, cât și cu cele neprimitive. Aceste potriviri sunt implementate ca metode statice în clasa org.mockito.AdditionalMatchers
.
Luați în considerare următorul exemplu:
when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));
Aici am combinat rezultatele a două potriviri de argumente: eq("1")
și contains("a")
. Expresia finală, or(eq("1"), contains("a"))
, poate fi interpretată ca „șirul de argument trebuie să fie egal cu „1” sau să conțină „a”.
Rețineți că există potriviri mai puțin obișnuite listate în clasa org.mockito.AdditionalMatchers
, cum ar fi geq()
, leq()
, gt()
și lt()
, care sunt comparații de valori aplicabile pentru valorile primitive și instanțele java.lang.Comparable
.

Verificarea comportamentului
Odată ce o simulare sau un spion a fost folosit, putem verify
dacă au avut loc interacțiuni specifice. Literal, spunem „Hei, Mockito, asigură-te că această metodă a fost invocată cu aceste argumente.”
Luați în considerare următorul exemplu artificial:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");
Aici am creat o simulare și am numit metoda sa encode()
. Ultima linie verifică că metoda encode()
a simulatului a fost apelată cu valoarea argumentului specific a
. Vă rugăm să rețineți că verificarea unei invocări stubbed este redundantă; scopul fragmentului anterior este de a arăta ideea de a face verificare după ce au avut loc unele interacțiuni.
Dacă schimbăm ultima linie pentru a avea un argument diferit - să spunem, b
- testul anterior va eșua și Mockito se va plânge că invocarea reală are argumente diferite ( b
în loc de a
așteptat).
Potrivirile de argumente pot fi utilizate pentru verificare, la fel ca pentru stubbing:
verify(passwordEncoder).encode(anyString());
În mod implicit, Mockito verifică că metoda a fost apelată o dată, dar puteți verifica orice număr de invocari:
// verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());
O caracteristică rar folosită a verify()
este capacitatea sa de a eșua la un timeout, care este util în principal pentru testarea codului concurent. De exemplu, dacă codificatorul nostru de parolă este apelat într-un alt fir concomitent cu verify()
, putem scrie un test după cum urmează:
usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");
Acest test va reuși dacă encode()
este apelat și terminat în 500 de milisecunde sau mai puțin. Dacă trebuie să așteptați întreaga perioadă pe care o specificați, utilizați after()
în loc de timeout()
:
verify(passwordEncoder, after(500)).encode("a");
Alte moduri de verificare ( times()
, atLeast()
, etc) pot fi combinate cu timeout()
și after()
pentru a face teste mai complicate:
// passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");
Pe lângă times()
, modurile de verificare acceptate includ only()
, atLeast()
și atLeastOnce()
(ca alias pentru atLeast(1)
).
Mockito vă permite, de asemenea, să verificați ordinea apelurilor într-un grup de simulari. Nu este o caracteristică care să fie folosită foarte des, dar poate fi utilă dacă ordinea invocărilor este importantă. Luați în considerare următorul exemplu:
PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode("f1"); second.encode("s1"); first.encode("f2"); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode("f1"); inOrder.verify(second).encode("s1"); inOrder.verify(first).encode("f2");
Dacă rearanjam ordinea apelurilor simulate, testul va eșua cu VerificationInOrderFailure
.
Absența invocărilor poate fi verificată și folosind verifyZeroInteractions()
. Această metodă acceptă o simulare sau mock-uri ca argument și va eșua dacă au fost apelate vreo metodă din mock-urile transmise.
De asemenea, merită menționată metoda verifyNoMoreInteractions()
, deoarece ia simulari ca argument și poate fi folosită pentru a verifica dacă fiecare apel la acele simulari a fost verificat.
Captarea argumentelor
Pe lângă verificarea faptului că o metodă a fost apelată cu anumite argumente, Mockito vă permite să capturați acele argumente, astfel încât să puteți rula ulterior aserțiuni personalizate pe ele. Cu alte cuvinte, spui „Hei, Mockito, verifică dacă această metodă a fost numită și dă-mi valorile argumentului cu care a fost apelată”.
Să creăm o imitație de PasswordEncoder
, să apelăm encode()
, să captăm argumentul și să verificăm valoarea acestuia:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("password", passwordCaptor.getValue());
După cum puteți vedea, trecem passwordCaptor.capture()
ca argument al lui encode()
pentru verificare; aceasta creează intern un potrivire de argumente care salvează argumentul. Apoi recuperăm valoarea capturată cu passwordCaptor.getValue()
și o inspectăm cu assertEquals()
.
Dacă trebuie să captăm un argument în mai multe apeluri, ArgumentCaptor
vă permite să preluați toate valorile cu getAllValues()
, astfel:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password1"); passwordEncoder.encode("password2"); passwordEncoder.encode("password3"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList("password1", "password2", "password3"), passwordCaptor.getAllValues());
Aceeași tehnică poate fi utilizată pentru capturarea argumentelor metodei de aritate variabilă (cunoscute și sub numele de varargs).
Testarea exemplului nostru simplu
Acum că știm mai multe despre Mockito, este timpul să revenim la demonstrația noastră. Să scriem testul metodei isValidUser
. Iată cum ar putea arăta:
public class UserServiceTest { private static final String PASSWORD = "password"; private static final User ENABLED_USER = new User("user id", "hash", true); private static final User DISABLED_USER = new User("disabled user id", "disabled user password hash", false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of "password" verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser("invalid id", PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById("invalid id"); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), "invalid"); assertFalse(userIsValid); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("invalid", passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn("any password hash"); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }
Scufundare sub API
Mockito oferă un API lizibil și convenabil, dar haideți să explorăm unele dintre funcționalitățile sale interne pentru a înțelege limitările sale și pentru a evita erorile ciudate.
Să examinăm ce se întâmplă în interiorul Mockito când este rulat următorul fragment:
// 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode("a")).thenReturn("1"); // 3: act mock.encode("a"); // 4: verify verify(mock).encode(or(eq("a"), endsWith("b")));
Evident, prima linie creează o simulare. Mockito folosește ByteBuddy pentru a crea o subclasă a clasei date. Noul obiect de clasă are un nume generat precum demo.mockito.PasswordEncoder$MockitoMock$1953422997
, equals()
va acționa ca verificare a identității, iar hashCode()
va returna un cod hash de identitate. Odată ce clasa este generată și încărcată, instanța sa este creată folosind Objenesis.
Să ne uităm la următoarea linie:
when(mock.encode("a")).thenReturn("1");
Ordonarea este importantă: prima instrucțiune executată aici este mock.encode("a")
, care va invoca encode()
pe mock cu o valoare returnată implicită null
. Deci, într-adevăr, trecem null
ca argument pentru when()
. Mockito nu-i pasă ce valoare exactă este transmisă la when()
deoarece stochează informații despre invocarea unei metode batjocorite în așa-numita „stubbing în curs” atunci când acea metodă este invocată. Mai târziu, când apelăm when()
, Mockito trage acel obiect stubbing în curs și îl returnează ca rezultat al when()
. Apoi numim thenReturn(“1”)
pe obiectul stubbing în curs returnat.
A treia linie, mock.encode("a");
este simplu: numim metoda stubbed. Pe plan intern, Mockito salvează această invocare pentru verificare ulterioară și returnează răspunsul la invocare blocată; în cazul nostru, este șirul 1
.
În a patra linie ( verify(mock).encode(or(eq("a"), endsWith("b")));
), îi cerem lui Mockito să verifice că a existat o invocare a encode()
cu acelea argumente specifice.
verify()
este executat mai întâi, ceea ce transformă starea internă a lui Mockito în modul de verificare. Este important să înțelegeți că Mockito își păstrează starea într-un ThreadLocal
. Acest lucru face posibilă implementarea unei sintaxe frumoase, dar, pe de altă parte, poate duce la un comportament ciudat dacă cadrul este folosit necorespunzător (dacă încercați să utilizați potriviri de argument în afara verificării sau stubbing-ului, de exemplu).
Deci, cum creează Mockito un or
potrivire? Mai întâi, eq("a")
este apelat și un potrivire equals
este adăugat la stiva de potriviri. În al doilea rând, endsWith("b")
este apelat și un potrivire endsWith
este adăugat la stivă. În cele din urmă, se apelează or(null, null)
— folosește cele două potriviri pe care le scoate din stivă, creează potrivitorul or
și îl împinge în stivă. În cele din urmă, encode()
este apelat. Mockito verifică apoi că metoda a fost invocată de numărul așteptat de ori și cu argumentele așteptate.
În timp ce potrivirile de argumente nu pot fi extrase din variabile (deoarece schimbă ordinea apelurilor), acestea pot fi extrase în metode. Aceasta păstrează ordinea apelurilor și menține stiva în starea corectă:
verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }
Modificarea răspunsurilor implicite
În secțiunile anterioare, ne-am creat mock-urile în așa fel încât atunci când sunt apelate orice metode batjocorite, acestea returnează o valoare „vide”. Acest comportament este configurabil. Puteți chiar să furnizați propria implementare a org.mockito.stubbing.Answer
. Răspundeți dacă cele furnizate de Mockito nu sunt potrivite, dar ar putea fi un indiciu că ceva nu este în regulă atunci când testele unitare devin prea complicate. Amintiți-vă de principiul KISS!
Să explorăm oferta lui Mockito de răspunsuri prestabilite predefinite:
RETURNS_DEFAULTS
este strategia implicită; nu merită menționat în mod explicit atunci când se instalează o simulare.CALLS_REAL_METHODS
face ca invocările nestopate să apeleze metode reale.RETURNS_SMART_NULLS
evită oNullPointerException
, returnândSmartNull
în loc denull
atunci când se utilizează un obiect returnat de un apel de metodă nestubbed. Veți eșua în continuare cu oNullPointerException
, darSmartNull
vă oferă o urmărire mai bună a stivei cu linia în care a fost apelată metoda unstubbed. Acest lucru face ca este util caRETURNS_SMART_NULLS
să fie răspunsul implicit în Mockito!RETURNS_MOCKS
încearcă mai întâi să returneze valori obișnuite „vide”, apoi batjocorește, dacă este posibil, șinull
în caz contrar. Criteriile de golire diferă puțin față de ceea ce am văzut mai devreme: în loc să returnezenull
pentru șiruri și matrice, mock-urile create cuRETURNS_MOCKS
returnează șiruri goale și, respectiv, matrice goale.RETURNS_SELF
este util pentru batjocorirea constructorilor. Cu această setare, un mock va returna o instanță a lui însuși dacă este apelată o metodă care returnează ceva de tip egal cu clasa (sau superclasa) a clasei batjocorită.RETURNS_DEEP_STUBS
merge mai adânc decâtRETURNS_MOCKS
și creează false care sunt capabile să returneze false din false din false etc. Spre deosebire deRETURNS_MOCKS
, regulile de golire sunt implicite înRETURNS_DEEP_STUBS
, deci returneazănull
pentru șiruri și matrice:
interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());
Numirea unui Mock
Mockito vă permite să denumiți un mock, o caracteristică utilă dacă aveți o mulțime de mock-uri într-un test și trebuie să le distingeți. Acestea fiind spuse, nevoia de a numi batjocuri ar putea fi un simptom al designului slab. Luați în considerare următoarele:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());
Mockito se va plânge, dar pentru că nu le-am numit oficial mock-urile, nu știm care dintre ele:
Wanted but not invoked: passwordEncoder.encode(<any string>);
Să le denumim prin trecerea unui șir pe construcție:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());
Acum, mesajul de eroare este mai prietenos și indică în mod clar către robustPasswordEncoder
:
Wanted but not invoked: robustPasswordEncoder.encode(<any string>);
Implementing Multiple Mock Interfaces
Sometimes, you may wish to create a mock that implements several interfaces. Mockito is able to do that easily, like so:
PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);
Listening Invocations
A mock can be configured to call an invocation listener every time a method of the mock was called. Inside the listener, you can find out whether the invocation produced a value or if an exception was thrown.
InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode("1");
In this example, we're dumping either the returned value or a stack trace to a system output stream. Our implementation does roughly the same as Mockito's org.mockito.internal.debugging.VerboseMockInvocationLogger
(don't use this directly, it's internal stuff). If logging invocations is the only feature you need from the listener, then Mockito provides a cleaner way to express your intent with the verboseLogging()
setting:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());
Take notice, though, that Mockito will call the listeners even when you're stubbing methods. Luați în considerare următorul exemplu:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode("1")).thenReturn("encoded1"); passwordEncoder.encode("1"); passwordEncoder.encode("2");
This snippet will produce an output similar to the following:
############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: "null" ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: "encoded1" (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode("2"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: "null"
Note that the first logged invocation corresponds to calling encode()
while stubbing it. It's the next invocation that corresponds to calling the stubbed method.
Alte setari
Mockito offers a few more settings that let you do the following:
- Enable mock serialization by using
withSettings().serializable()
. - Turn off recording of method invocations to save memory (this will make verification impossible) by using
withSettings().stubOnly()
. - Use the constructor of a mock when creating its instance by using
withSettings().useConstructor()
. When mocking inner non-static classes, add anouterInstance()
setting, like so:withSettings().useConstructor().outerInstance(outerObject)
.
If you need to create a spy with custom settings (such as a custom name), there's a spiedInstance()
setting, so that Mockito will create a spy on the instance you provide, like so:
UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name("coolService"));
When a spied instance is specified, Mockito will create a new instance and populate its non-static fields with values from the original object. That's why it's important to use the returned instance: Only its method calls can be stubbed and verified.
Note that, when you create a spy, you're basically creating a mock that calls real methods:
// creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));
When Mockito Tastes Bad
It's our bad habits that make our tests complex and unmaintainable, not Mockito. For example, you may feel the need to mock everything. This kind of thinking leads to testing mocks instead of production code. Mocking third-party APIs can also be dangerous due to potential changes in that API that can break the tests.
Though bad taste is a matter of perception, Mockito provides a few controversial features that can make your tests less maintainable. Sometimes stubbing isn't trivial, or an abuse of dependency injection can make recreating mocks for each test difficult, unreasonable or inefficient.
Clearing Invocations
Mockito allows for clearing invocations for mocks while preserving stubbing, like so:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);
Resort to clearing invocations only if recreating a mock would lead to significant overhead or if a configured mock is provided by a dependency injection framework and stubbing is non-trivial.
Resetting a Mock
Resetting a mock with reset()
is another controversial feature and should be used in extremely rare cases, like when a mock is injected by a container and you can't recreate it for each test.
Overusing Verify
Another bad habit is trying to replace every assert with Mockito's verify()
. It's important to clearly understand what is being tested: interactions between collaborators can be checked with verify()
, while confirming the observable results of an executed action is done with asserts.
Mockito Is about Frame of Mind
Using Mockito is not just a matter of adding another dependency, it requires changing how you think about your unit tests while removing a lot of boilerplate.
With multiple mock interfaces, listening invocations, matchers and argument captors, we've seen how Mockito makes your tests cleaner and easier to understand, but like any tool, it must be used appropriately to be useful. Now armed with the knowledge of Mockito's inner workings, you can take your unit testing to the next level.