<div class="page">
<div class="cover text-center">
<img class="mx-auto" src=/itb/images/logo_mislata.png alt="logo">
# Mockito
<div class="text-end fit-content ms-auto my-3 mt-auto pt-3">
<p><strong>Autor:</strong> Joan Puigcerver Ibáñez</p>
<p><strong>Correu electrònic:</strong> j.puigcerveribanez@edu.gva.es</p>
<p><strong>Curs:</strong> 2024/2025</p>
</div>
<div>
<p class="fw-bold mb-0">Llicència: BY-NC-SA</p>
<p class="d-none d-md-block">(Reconeixement - No Comercial - Compartir Igual)</p>
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/deed.ca" target="_blank">
<img class="mx-auto" src="/itb/images/license.png" alt="Licence"/>
</a>
</div><!--license-->
</div><!--cover-->
</div><!--page-->
{:toc}
## Mockito
__Mockito__ és una llibreria de Java que s'utilitza per crear
__objectes simulats__ (_mock objects_) per als tests unitaris.
Mockito ofereix una sintaxi senzilla i fàcil d'usar per crear mock objects i
especificar el seu comportament durant les proves.
## Dependències
Per utilitzar Mockito en el nostre projecte de Java, primer hem d'afegir la llibreria Mockito.
Utilitzant Maven, es pot incloure la següent dependència al fitxer `pom.xml`:
- Si utilitzem `junit.jupiter`, podem utilitzar la versió compatible de Mockito:
```xml
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.2.0</version>
</dependency>
```
- En alguns frameworks, com `Spring Boot` ja ve incorporat i no cal incloure la dependència.
## Arquitectura del codi d'exemple
::: info
És la mateixa arquitectura que l'example presentat al
[Material: Objectes simulats](/itb/DAM-ED/UD5/materials/06_mock.html).
:::
Aquest exemple simula un projecte organitzat per capes, on
tenim la capa de __servei__ (`Service`) que interactua en la capa de __repositori__ (`Repository`).
```mermaid
flowchart LR
A[CarService] --> B
subgraph service
A --> C[IndicatorService]
end
subgraph repository
B[CarRepository]
end
```
### Entitat `Car`
```java
package ud5.examples.car.domain.entity;
/**
* A simple car class with a constructor and a method to accelerate.
*/
public class Car {
private final String plate;
private final String brand;
private final String model;
private double speed;
public Car(String plate, String brand, String model) {
this.plate = plate;
this.brand = brand;
this.model = model;
this.speed = 0.0;
}
public Car(String plate, String brand, String model, double speed) {
this.plate = plate;
this.brand = brand;
this.model = model;
this.speed = speed;
}
public String getPlate() {
return plate;
}
public String getBrand() {
return brand;
}
public String getModel() {
return model;
}
public double getSpeed() {
return speed;
}
public void accelerate(double speed) {
this.speed += speed;
}
}
```
### Interface `CarService`
```java
package ud5.examples.car.domain.service;
import ud5.examples.car.domain.entity.Car;
import ud5.examples.car.exception.ResourceNotFoundException;
import java.util.List;
public interface CarService {
List<Car> findAll();
Car findByPlate(String plate) throws ResourceNotFoundException;
void accelerate(Car car, double speed);
}
```
### Interface `CarRepository`
```java
package ud5.examples.car.repository;
import ud5.examples.car.domain.entity.Car;
import ud5.examples.car.exception.ResourceNotFoundException;
import java.util.List;
public interface CarRepository {
List<Car> findAll();
Car findByPlate(String plate) throws ResourceNotFoundException;
}
```
### Interface `IndicatorService`
```java
package ud5.examples.car.domain.service;
public interface IndicatorService {
void showMaxSpeedIndicator(boolean showNotification);
}
```
### Exception `ResourceNotFoundException`
```java
package ud5.examples.car.exception;
public class ResourceNotFoundException extends Exception {
public ResourceNotFoundException(String message){
super(message);
}
}
```
### Implementació del servei `CarServiceImpl`
En el següent exemple anem a provar la classe `CarServiceImpl`
que implementa la interfície `CarService`.
Aquesta classe depén de les classes `CarRepository` i `IndicatorService`.
```java
package ud5.examples.car.domain.service.impl;
import ud5.examples.car.domain.entity.Car;
import ud5.examples.car.domain.service.IndicatorService;
import ud5.examples.car.exception.ResourceNotFoundException;
import ud5.examples.car.domain.service.CarService;
import ud5.examples.car.repository.CarRepository;
import java.util.List;
public class CarServiceImpl implements CarService {
private final CarRepository carRepository;
private final IndicatorService indicatorService;
public CarServiceImpl(CarRepository carRepository, IndicatorService indicatorService) {
this.carRepository = carRepository;
this.indicatorService = indicatorService;
}
@Override
public List<Car> findAll() {
return carRepository.findAll();
}
@Override
public Car findByPlate(String plate) throws ResourceNotFoundException {
return carRepository.findByPlate(plate);
}
@Override
public void accelerate(Car car, double speed) {
car.accelerate(speed);
indicatorService.showMaxSpeedIndicator(car.getSpeed() > 120);
}
}
```
## Testing amb Mockito
Per realitzar les proves unitàries de la classe `CarServiceImpl`,
cal simular el comportament de les classes `CarRepository` i `IndicatorService`.
En aquest cas, utilitzarem Mockito per crear objectes simulats
i no cal crear cap classe `Mock` directament.
Crearem les proves unitàries de la classe `CarServiceImpl` amb Mockito
a la classe `CarServiceImplMockitoTest`.
```mermaid
flowchart LR
A[CarServiceImplMockitoTest] --> B
subgraph service
A --> C[IndicatorService]
end
subgraph repository
B[CarRepository]
end
B:::mock
C:::mock
classDef mock stroke:#f00,stroke-dasharray: 5 5
```
### @ExtendWith()
Si es va a utilitzar Mockito en una classe, cal indicar-ho
en la definició d'aquesta amb l'anotació `@ExtendWith(MockitoExtension.class)`.
::: warning
Depenent de les versions, de vegades cal utilizar `@RunWith()`
Més informació:
- [When to use @RunWith and when @ExtendWith](https://stackoverflow.com/questions/55276555/when-to-use-runwith-and-when-extendwith)
:::
```java
package ud5.examples.car.service;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class CarServiceImplMockitoTest {
// ...
}
```
### @Mock i @InjectMock
Els objectes simulats es creen i defineixen mitjançant l'anotació `@Mock`,
que equival a `Mockito.mock(ClassName.class)`.
```java
@Mock
private CarRepository carRepository;
```
és equivalent a:
```java
private CarRepository carRepository = Mockito.mock(CarRepository.class);
```
Aquests objectes simulats poden ser _injectats_ en altres clases, per modificar les crides
a els objectes que es realitzen en la implementació. L'injecció d'objectes simulats
es realitza amb l'annotació `@InjectMocks`
```java
@InjectMocks
private CarServiceImpl carService;
```
és equivalent a:
```java
private CarServiceImpl carService = new CarServiceImpl(carRepository, indicatorService);
```
::: info
Quan s'utilitza `@InjectMocks`, Mockito intenta injectar els objectes simulats
en els atributs de la classe que s'està testejant.
No és necessari que la classe on s'injecten els objectes simulats tinga
un constructor amb els objectes simulats com a paràmetres.
:::
```java
import org.mockito.InjectMocks;
import org.mockito.Mock;
@ExtendWith(MockitoExtension.class)
class CarServiceImplTest {
// Creem els objectes simulats
@Mock
private CarRepository carRepository;
@Mock
private IndicatorService indicatorService;
// Injectem els objectes simulats en la classe que volem provar
@InjectMocks
private CarServiceImpl carService;
//...
}
```
### When i Then
Els objectes simulats en un principi __no tenen cap funcionalitat__, per tant,
si crides a qualsevol mètode, no fan res. Per tant, el primer pas d'una prova
és definir el comportament de l'objecte simulat.
El mètode `Mockito.when` s'utilitza per definir el comportament d'una funció
quan es crida amb uns paràmetres determinats d'un objecte simulat.
Els mètodes `Mockito.then...` s'utilitzen per definir __el resultat__
de la crida al mètode de l'objecte simulat.
```java
when(mock.method(params)).thenReturn(value);
```
Una vegada definit el comportament de l'objecte simulat, ja es pot provar el mètode
dessitjat.
::: example
```java
@Test
void findByPlate_givenExistingCar_shouldReturnCar() throws ResourceNotFoundException {
Car expectedCar = new Car("1234ABC", "Seat", "Ibiza", 100000);
/* Definim el comportament de l'objecte Mock.
- Quan (when) cridem el mètode findByPlate() del repositori amb la matrícula del cotxe
- Aleshores (then), el repositori ens retornarà l'objecte cotxe
*/
when(carRepository.findByPlate("1234ABC")).thenReturn(expectedCar);
// Crida al mètode de la classe que volem provar
Car actualCar = carService.findByPlate("1234ABC");
assertSame(expectedCar, actualCar);
}
```
:::
Es poden definir molts tipus de comportament per als objectes simulats.
Aquests comportaments es defineixen amb els mètodes amb prefix `then`.
#### thenReturn()
Retorna un valor o un objecte.
```
when(...).thenReturn(obj);
```
#### thenThrow()
Llança una excepció.
```
when(...).thenThrow(CustomException.class);
```
::: example
```java
@Test
void findByPlate_givenNonExistingCar_shouldThrowException() throws ResourceNotFoundException {
/* Configurem el comportament de l'objecte Mock.
En aquest cas:
- Quan (when) al repositori li preguntem per la matricula del cotxe que no existeix
- Aleshores (then), llançarà una excepció
*/
when(carRepository.findByPlate("9999ZZZ")).thenThrow(
ResourceNotFoundException.class
);
// Llancem el mètode que volem provar.
assertThrows(
ResourceNotFoundException.class,
() -> carService.findByPlate("9999ZZZ"),
"Expected ResourceNotFound exception, but it was not thrown."
);
}
```
:::
### verify()
::: question
Fixeu-se en el mètode `accelerate(Car car, int speed)` de la classe `CarServiceImpl`.
```java
@Override
public void accelerate(Car car, double speed) {
car.accelerate(speed);
indicatorService.showMaxSpeedIndicator(car.getSpeed() > 120);
}
```
Com es pot comprovar si l'indicador de velocitat màxima s'ha mostrat correctament?
En el [Material: Objectes simulats](/itb/DAM-ED/UD5/materials/06_mock.html),
vam haver d'implementar el mètode `isMaxSpeedIndicatorShown()` en la classe `IndicatorServiceMock`
per comprovar-ho.
:::
En aquest cas es pot utilitzar el mètode `Mockito.verify()`, que comprova
si un mètode de l'objecte simulat ha segut cridat amb uns paràmetres determinats.
La sintàxi per comprovar que un objecte `mock` ha cridat al mètode `method` amb els paramètres
`params` és:
```
verify(mock).method(params);
```
Aquest mètode llança una `AssertionError` si el mètode no ha sigut cridat amb els
paràmetres indicats.
::: example
Provem que el mètode `accelerate(Car car, int speed)` de la classe `CarServiceImpl`
mostra o no l'indicador de velocitat màxima.
```java
@Test
void givenAcceleratedCarLessThanMaximumSpeed_shouldNotShowMaxSpeedIndicator() {
Car car = new Car("1234ABC", "Seat", "Ibiza");
// Internament es crida a indicatorServiceMock.showMaxSpeedIndicator(true)
carService.accelerate(car, 10.0);
verify(indicatorService).showMaxSpeedIndicator(false);
}
@Test
void givenAcceleratedCarGreaterThanMaximumSpeed_shouldShowMaxSpeedIndicator() {
Car car = new Car("1234ABC", "Seat", "Ibiza");
// Internament es crida a indicatorServiceMock.showMaxSpeedIndicator(true)
carService.accelerate(car, 130.0);
verify(indicatorService).showMaxSpeedIndicator(true);
}
```
:::
#### Nombre d'invocacions
Mockito també permet comprovar el nombre de vegades que s'ha cridat un mètode
d'un objecte simulat.
Cal especificar un dels següents mètodes en el mètode `verify()`:
- `never()`: Comprova que __no__ s'ha invocat el mètode amb els paràmetres indicats.
```
verify(mock, never()).method(params);
```
- `times(n)`: Comprova que s'ha invocat el mètode amb els paràmetres indicats
__exactament__ `n` vegades.
```
verify(mock, times(n)).method(params);
```
- `atLeast(n)`: Comprova que s'ha invocat el mètode amb els paràmetres indicats
__com a mínim__ `n` vegades.
```
verify(mock, atLeast(n)).method(params);
```
- `atMost(n)`: Comprova que s'ha invocat el mètode amb els paràmetres indicats
__com a màxim__ `n` vegades.
```
verify(mock, atMost(n)).method(params);
```
## Proves: CarServiceImplMockitoTest
```java
package unit.ud5.examples.car.domain.service.impl;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import ud5.examples.car.domain.entity.Car;
import ud5.examples.car.domain.service.IndicatorService;
import ud5.examples.car.domain.service.impl.CarServiceImpl;
import ud5.examples.car.exception.ResourceNotFoundException;
import ud5.examples.car.repository.CarRepository;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CarServiceImplMockitoTest {
// Es creen els objectes simulats
@Mock
private CarRepository carRepositoryMock;
@Mock
private IndicatorService indicatorServiceMock;
// S'injecten els objectes simulats a la implementació
@InjectMocks
private CarServiceImpl carService;
private final Car car1 = new Car("1234ABC", "Seat", "Ibiza");
private final Car car2 = new Car("4321ABC", "Renault", "Clio");
private final List<Car> cars = List.of(car1, car2);
@Nested
class FindAll {
@Test
void whenRepositoryReturnsEmptyList_shouldReturnEmptyList() {
// Es configura el comportament del carRepositoryMock
when(carRepositoryMock.findAll()).thenReturn(new ArrayList<>());
// Internament es crida a carRepositoryMock.findAll()
assertEquals(0, carService.findAll().size());
}
@Test
void whenRepositoryReturnsCars_shouldReturnAllCars() {
// Es configura el comportament del carRepositoryMock
when(carRepositoryMock.findAll()).thenReturn(cars);
// Internament es crida a carRepositoryMock.findAll()
assertEquals(2, carService.findAll().size());
}
}
@Nested
class FindByPlate {
@Test
void givenExistingCarWithPlate_shouldReturnCar() throws ResourceNotFoundException {
// Es configura el comportament del carRepositoryMock
when(carRepositoryMock.findByPlate("1234ABC")).thenReturn(car1);
// Internament es crida a carRepositoryMock.findByPlate("1234ABC")
Car resultCar = carService.findByPlate("1234ABC");
assertEquals(car1, resultCar);
}
@Test
void givenDifferentExistingCarWithPlate_shouldReturnCar() throws ResourceNotFoundException {
// Es configura el comportament del carRepositoryMock
when(carRepositoryMock.findByPlate("4321ABC")).thenReturn(car2);
// Internament es crida a carRepositoryMock.findByPlate("4321ABC")
Car resultCar = carService.findByPlate("4321ABC");
assertEquals(car2, resultCar);
}
@Test
void givenNonExistingCarWithPlate_shouldThrowResourceNotFoundException() throws ResourceNotFoundException {
// Es configura el comportament del carRepositoryMock
when(carRepositoryMock.findByPlate("0000ZZZ")).thenThrow(ResourceNotFoundException.class);
// Internament es crida a carRepositoryMock.findByPlate("0000ZZZ")
assertThrows(ResourceNotFoundException.class, () -> carService.findByPlate("0000ZZZ"));
}
}
@Nested
class Accelerate {
@Test
void shouldAccelerateCar() {
Car car = new Car("1234ABC", "Seat", "Ibiza");
carService.accelerate(car, 10.0);
assertEquals(10.0, car.getSpeed());
}
@Test
void givenAcceleratedCarLessThanMaximumSpeed_shouldNotShowMaxSpeedIndicator() {
Car car = new Car("1234ABC", "Seat", "Ibiza");
// Internament es crida a indicatorServiceMock.showMaxSpeedIndicator(false)
carService.accelerate(car, 10.0);
verify(indicatorServiceMock).showMaxSpeedIndicator(false);
}
@Test
void givenAcceleratedCarGreaterThanMaximumSpeed_shouldShowMaxSpeedIndicator() {
Car car = new Car("1234ABC", "Seat", "Ibiza");
// Internament es crida a indicatorServiceMock.showMaxSpeedIndicator(true)
carService.accelerate(car, 130.0);
verify(indicatorServiceMock).showMaxSpeedIndicator(true);
}
}
}
```
## Recursos i bibliografia
- https://www.youtube.com/watch?v=j9k3epjUgr8&ab_channel=ProgramandoenJAVA
- https://stackoverflow.com/questions/55276555/when-to-use-runwith-and-when-extendwith
- https://www.baeldung.com/mockito-annotations
- https://www.baeldung.com/mockito-verify