Mockito

Mockito är ett mockramverk som används för att mocka objekt när man testar sin kod. Det är en vidareutveckling av EasyMock, och ett av de allra mest populära javabaserade biblioteken som finns, alltså inte bara bland testverktyg. Det finns med andra ord god anledning att bli kompis med det.

Mockito

400 200 C.A.G

På konferensen i Spanien körde jag (Stefan Nildén), tillsammans med Fredrik Dahlman och Anders Englesson, en dragning i Mockito med lite labbövningar för oss konsulter. Både java-utvecklare och testare deltog under de 3 timmar vi hade att jobba med. Många av oss utvecklare använder ju Mockito på daglig basis när vi skriver våra enhetstester, men det finns en del funktioner i Mockito som många av oss inte känner till eller använder mer sällan. Många av våra testare hade än mindre erfarenhet av Mockito och därför valde vi att göra dragningen ganska basic.

Om mockning

Att mocka innebär att man imiterar ett objekt för att kunna kontrollera dess beteende. Man ersätter helt enkelt objektet med en mock. Och till skillnad från ett vanligt anrop kommer inte den mockade klassens kod att köras, utan det är i stället mocken som anropas. Man har då full kontroll över dess beteende och kan kontrollera vad som ska hända.

 

 

 

 

 

 

Det finns ett flertal situationer när det kan vara bra att mocka något, eller mocka bort ett beroende. Oftast vill man mocka bort ett beroende till:

  • en komponent som ännu inte existerar, kan t ex vara under utveckling av ett annat team
  • en komponent som har långa svarstider, p g a I/O eller databasanrop etc
  • en komponent som vi inte kommer åt, p g a infrastrukturella orsaker

Men allra oftast skulle jag säga att man vill mocka bort beroenden till andra objekt för att kunna köra våra tester i en isolerad och kontrollerad miljö.

Om Mockito

Med enhetstester är tanken att man ska testa specifika klasser eller metoder utan att påverkas av, eller påverka omgivande beroenden till det man testar. Dessa beroenden vill man (oftast) mocka genom att skapa stubbar, sk test doubles, som ersätter ett beroenden och låter oss kontrollera interaktionen. Mockito har olika typer av test doubles som t ex Mock och Spy som ger bra stöd för detta.

 

I Mockito finns även några annoteringar som kan vara bra att känna till:

  • @Mock – markerar att ett objekt är en mock
  • @Spy – markerar att ett objekt är en spy och låter dig spionera på detta objekt
  • @InjectMocks – markerar att här ska objekt markerade med @Mock injectas
  • @Captor – markerar en ArgumentCaptor för att kunna kontrollera argument till metoder
  • verify – verifiera att metoder anropas x antal gånger

Om Mockar

För att känna lite på hur man kan mocka objekt i ett test kan vi utgå från ett enkelt exempel med en service som kan uppdatera en kund. Serviceklassen innehåller även en Dao som är tänkt att spara förändringar. I serviceklassen finns några if/else bara för att vi ska kunna testa att kontrollera beteendet vid anrop till dao-objektet.

 

public class CustomerService {
@Inject
private CustomerDao customerDao;

public void addCustomer(Customer customer) {
if(customerDao.exists(customer.getCustomerId())) {
return false;
}
customerDao.save(customer);
}

public void updateCustomer(Customer customer) {
if(customerDao.exists(customer.getCustomerId())){
customerDao.update(customer);
} else {
customerDao.save(customer);
}
}
}

 

class CustomerDao {
private List<Customer> customers = new ArrayList<>();

public void update(Customer customer) {
customers.remove(customer);
customers.add(customer);
}

public boolean exists(String customerId) {
return customers.stream()
.anyMatch(customer -> customer.getCustomerId().equals(customerId));
}

public void save(Customer customer) {
customers.add(customer);
}
}
När vi testar servicen vill vi inte vill att vår dao gör några anrop mot en databas utan vi vill testa servicen i en isolerad och kontrollerad miljö. Vi mockar därför bort customerDao i serviceklassen. Det kan man göra på två 2 sätt. Antingen genom:

private CustomerDao customerDao = Mockito.mock(CustomerDao.class);

 

Eller genom att använda en annotering:

@Mock
private CustomerDao customerDao;
Båda sätten funkar lika bra och ger samma resultat, men om man använder sig av annoteringen är det viktigt att komma ihåg att låta Mockito initiera mockarna. Gör man inte det kommer man att få ett NullPointerException när man försöker använda sina mockar. Det kan man enkelt göra så här:

@BeforeEach
public void setup () {
MockitoAnnotations.initMock(this);
}

 

I vårt test skapar vi en metod där kontrollerar att vi metoden addCustomer() returnerar false när vi försöker lägga till en kund som redan existerar. Vi skulle såklart kunna lägga till en kund innan vi kör vårt test och sedan försöka lägga till samma kund igen. Vi kan också mocka metoden customerDao.exists() och se till att den returnerar false vid ett givet anrop. Så här kan det se ut:

 

class CustomerServiceTest {
@Mock
CustomerDao customerDao;
@InjectMocks
CustomerService service;

@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
}

@Test
void addCustomer_shouldReturnFalseWhenCustomerExists() {
when(customerDao.exists(“123456”)).thenReturn(true);
Customer customer = new Customer(“Ross”, “123456”);
boolean result = service.addCustomer(customer);
assertFalse(result);
}
}

 

I början av testklassen skapar vi en mock av CustomerDao som ersätter den riktiga CustomerDao. Eftersom CustomerDao injectas i CustomerService använder vi oss av Mockitos @InjectMocks för att markera att vår mock ska injectas i CustomerService. I metoden setUp ser vi till att Mockito initiera vår mock.

 

I testet kontrollerar vi vår mock och indikerar att när customerDao.exists() anropas med argumentet “123456”, så ska den returnera true. Alltså att kunden finns. När vi kör testet kommer customerDao att anropas utan mocken kommer direkt att returnera false åt oss och testet kommer att bli grönt.

 

Om ArgumentMatchers

Det kan vara bra att känna till begreppet argument matchers och vad man kan göra med det. Med en argument matcher kan man tala om för mocken att den bara ska trigga på specifika argument till en metod. Det kan vara en specifik sträng, vilken sträng som helst eller en sträng som startar med specifika tecken. Man kan även matcha specifika objekt som t ex en Customer. I det tidigare exemplet ville vi att endast när CustomerDao.exists() anropades med strängen “123456” skulle true returneras:

when(customerDao.exists(“123456”)).thenReturn(true);
Men vi kunde lika gärna matcha på vilken sträng som helst:

when(customerDao.exists(anyString())).thenReturn(true);

 

Eller endast på strängar som börjar med “031”:

when(customerDao.exists(startsWith(“031”))).thenReturn(true);

 

Om verify

Ofta testar man bara att en metod gör det den ska, dvs givet att man anropar en metod med argumentet X så ska man få tillbaka Y. Man struntar ofta i att testa implementationen i metoden som testas. Men ibland vill man faktiskt testa, eller verifiera, att rätt saker görs i en metod. T ex om man anropar CustomerService.updateCustomer() så ska även CustomerDao.update() anropas, men INTE CustomerDao.save() . Då kan man ta hjälp av Mockitos verify() för att verifiera detta. Så här kan det se ut:

public boolean updateCustomer(Customer customer) {
if(customerDao.exists(customer.getCustomerId())){
return customerDao.update(customer);
} else {
return customerDao.save(customer);
}
}

 

@Test
public void testUpdateCustomer_verifySaveIsCalled() {
when(daoMock.exists(anyString())).thenReturn(true);
service.updateCustomer(new Customer(“Ross”, “1122333”));

verify(daoMock, times(1)).update(any(Customer.class));
verify(daoMock, times(0)).save(any(Customer.class));
}
Eftersom times(1) är default kan man utelämna det och istället skriva:

verify(daoMock).update(any(Customer.class));

 

Spy

Vill man interagera med det riktiga objektet istället för att använda sig av en mock kan man använda sig av mockitos Spy-funktionalitet. Skillnaden mellan en mock och en spy är att en mock egentligen bara är ett tomt skal av den typ av klass man vill mocka, alltså inte en instans av klassen. Man behöver specificera hur mocken ska bete sig och vad den ska returnera. En spy å andra sidan lägger sig som en wrapper runt en riktig instans av den klass man testar. Koden i instansen kommer att köras men all interaktion med den registreras och kan därmed verifieras.

 

 

 

 

 

 

 

Att välja en spy istället för en mock förhindrar dock inte att man kan mocka vissa metoder i den klass man kör spy på för att undvika att koden körs i dessa metoder. Det kan t ex bero på att man vill att undvika sidoeffekter i en viss metod, men för övrigt vill man att köra igenom all kod.

För att få till en spy i sitt test deklarerar men den klass man vill spionera på med en annotering:

@Spy

private CustomerDao daoSpy;

 

Själva testet skulle kunna se ut så här:

@Test
void testUpdateCustomer() {
service.addCustomer(new Customer(NAME, CUSTOMER_ID));
service.updateCustomer(new Customer(UPDATED_NAME, CUSTOMER_ID));
verify(daoSpy, times(2)).exists(CUSTOMER_ID);
verify(daoSpy).update(any(Customer.class));
verify(daoSpy, times(1)).save(any(Customer.class));

Optional<Customer> customer = service.getCustomer(CUSTOMER_ID);
assertTrue(customer.isPresent());
assertEquals(UPDATED_NAME, customer.get().getName());
}

 

Om man kör testet i debuggern eller skriver ut några loggrader i CustomerDao, som man spionerar på, kan man se att koden verkligen körs. Jämför gärna med ett test där du i stället mockar CustomerDao. Där kommer inte koden i CustomerDao att köras eftersom man endast interagerar med sin mock.

 

Men om man vill mocka en metod i sin spy kan man använda sig av Mockitos doReturn:

doReturn(true).when(daoSpy).exists(CUSTOMER_ID);

 

Nu kommer inte CustomerDao.exists() att köras utan endast returnera true. Jämför med:

when(daoSpy.exists(CUSTOMER_ID)).thenReturn(true);

Som även den kommer att returnera true men också kommer att köra koden i CustomerDao.exists().

 

ArgumentCaptor

Ibland vill man testa att en metod anropas med specifika argument. Men hur gör man det om vi inte har kontroll över vilka argument som skickas in i metoden? ArgumentCaptor kan hjälpa oss med det.

Klassen Car har en metod för att fylla upp tanken på bilen:

public void fillUp() {
fuelTank.setFuel(FuelTank.MAX);
}

Vill vi testa att FuelTank.setFuel() anropas med argumentet 100. Men eftersom vi inte har kontroll över argumentet som skickas in i fuelTank.setFuel(), det är ju en statisk variabel som skapas inne i fillUp(), så kan vi använda ArgumentCaptor.capture().

 

@Test
public void shouldExecuteTheRealObjects() {
ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);

Car car = new Car(engineSpy, fuelSpy);
car.fillUp();

verify(fuelSpy).setFuel(argumentCaptor.capture());
assertEquals(FuelTank.MAX, argumentCaptor.getValue().intValue());

Med hjälp av verify kan vi fånga argumentet som skickas in i setFuel() och kan även lägga in en assert för att kontrollera att det verkligen är argumentet 100 som skickades in.

 

Vad man ska mocka

Tja, här finns det olika bud. En del vill mocka bort alla beroenden i testklassen, medan andra vill mocka mindre. Något man kan tänka på innan man mockar loss är varför man egentligen skriver sina tester. Svaret på den frågan är väl att vi till syvende och sist skriver våra tester för att testa produktionskod och blir det för mycket mockar testar vi ju istället en massa mockar. Så mocka lagom där ute.

//Stefan Nildén, Javaspecialist på C.A.G Contactor

X
X