<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