W tym wpisie przyjrzymy się trzem sposobom na tworzenie mocków z użyciem frameworka Mockito, a mianowicie .mock() i @Mock.
Mockito.mock()
Metoda Mockito.mock()
tworzy obiekt typu mock na podstawie klasy albo interfejsu, jaki podamy w argumencie. Następnie możemy ustawić kiedy, jakie wartości ma ów obiekt zwracać, albo weryfikować, jakie metody zostały wykonane.
@Test public void testGetNoRoomsWhenWrongSize() { //given List<Room> rooms = new ArrayList<>(); rooms.add(new Room("101", Arrays.asList(BedType.DOUBLE))); rooms.add(new Room("102", Arrays.asList(BedType.SINGLE))); rooms.add(new Room("103", Arrays.asList(BedType.DOUBLE, BedType.SINGLE))); RoomRepository roomRepository = Mockito.mock(RoomRepository.class); ReservationService reservationService = Mockito.mock(ReservationService.class); Mockito.when(roomRepository.findAll()).thenReturn(rooms); RoomService roomService = new RoomService(roomRepository, reservationService); //when List<Room> result = roomService.getRoomsForSize(-1); //then assertEquals(0, result.size()); }
W ten sposób możemy tworzyć mocki zarówno na poziomie metod testowych (testGetNoRoomsWhenWrongSize
w tym wypadku) jak i pól (stanu) całej klasy testowej.
@Mock
Adnotacja @Mock
jest używana jako skrót dla Mockito.mock()
. Możemy jej używać tylko na poziomie pól klasy albo przy wstrzykiwaniu mocków jako parametrów metody testowej. Musimy też oznaczyć klasę testową dodatkowo adnotacją @ExtendWith(MockitoExtension.class)
Poniższy kod
public class RoomServiceTest { private RoomRepository roomRepository = Mockito.mock(RoomRepository.class); private ReservationService reservationService = Mockito.mock(ReservationService.class); @Test public void testGetNoRoomsWithWrongSize() { //given List<Room> rooms = new ArrayList<>(); rooms.add(new Room("101", Arrays.asList(BedType.DOUBLE))); rooms.add(new Room("102", Arrays.asList(BedType.SINGLE))); rooms.add(new Room("103", Arrays.asList(BedType.DOUBLE, BedType.SINGLE))); Mockito.when(roomRepository.findAll()).thenReturn(rooms); RoomService roomService = new RoomService(roomRepository, reservationService); //when List<Room> result = roomService.getRoomsForSize(-1); //then assertEquals(0, result.size()); } }
Możemy zamienić na
@ExtendWith(MockitoExtension.class) public class RoomServiceTest { @Mock private RoomRepository roomRepository; @Mock private ReservationService reservationService; @Test public void testGetNoRoomsWithWrongSize() { //given List<Room> rooms = new ArrayList<>(); rooms.add(new Room("101", Arrays.asList(BedType.DOUBLE))); rooms.add(new Room("102", Arrays.asList(BedType.SINGLE))); rooms.add(new Room("103", Arrays.asList(BedType.DOUBLE, BedType.SINGLE))); Mockito.when(roomRepository.findAll()).thenReturn(rooms); RoomService roomService = new RoomService(roomRepository, reservationService); //when List<Room> result = roomService.getRoomsForSize(4); //then assertEquals(0, result.size()); } }
W tym wypadku musimy pamiętać o resetowaniu mocków przed każdym testem. Alternatywnym, lepszym rozwiązaniem jest wstrzyknięcie ich jako parametrów do metody testującej. Wówczas do metody wstrzykiwane są za każdym razem nowo utworzone mocki.
@ExtendWith(MockitoExtension.class) public class RoomServiceTest { @Test public void testGetNoRoomsWithWrongSize(@Mock RoomRepository roomRepository, @Mock ReservationService reservationService) { //given List<Room> rooms = new ArrayList<>(); rooms.add(new Room("101", Arrays.asList(BedType.DOUBLE))); rooms.add(new Room("102", Arrays.asList(BedType.SINGLE))); rooms.add(new Room("103", Arrays.asList(BedType.DOUBLE, BedType.SINGLE))); Mockito.when(roomRepository.findAll()).thenReturn(rooms); RoomService roomService = new RoomService(roomRepository, reservationService); //when List<Room> result = roomService.getRoomsForSize(-1); //then assertEquals(0, result.size()); } }
Dodatkowo, dzięki MockitoExtension będziemy informowani (za pomocą wyjątku. UnnecessaryStubbingException
) jeśli zamockowana metoda (np. Mockito.when(roomRepository.findAll()).thenReturn(rooms)
) jest nieużywana. Dzięki temu nasze testy jednostkowe pozbędą się niepotrzebnego kodu, albo wręcz dowiemy się o błędnym ich działaniu.
@MockBean
MockBean
używane jest tylko w frameworku Spring.
Tworzy ona mocka danego komponentu springowego ORAZ wstrzykuje go do kontekstu Spring. Jest to o tyle ważne, że w aplikacjach używających frameworka Spring możemy pisać standardowe testy jednostkowe, mockując za pomocą Mockito wszelkie komponenty (np. serwisy i repozytoria, ale też na przykład filtry) tak jak standardowe klasy (tak jak robie to w powyższych przykładach) .
Możemy też uruchamiać testy, które będą korzystać z całego kontekstu springowego, zadziała wówczas automatyczne wstrzykiwanie i inne, sprignowe „zabawki”. Testy z kontekstem są natomiast zdecydowanie wolniejsze, bo muszą wszystko to uruchomić i zestawić w całość.
Tak, więc adnotacja @MockBean
jest używana przy testach kontekstowych w frameworku Spring, na poziomie pól klasy testowej. Dobrym przykładem są testy kontrolera, gdzie siła rzeczy potrzebujemy całego „grubego” Springa i wstrzykniętych z kontekstu dodatkowych obiektów.
@WebMvcTest(controllers = RestRoomController.class) @AutoConfigureMockMvc(addFilters = false) public class RestRoomControllerTest { @Autowired private MockMvc mockMvc; @MockBean private ReservationService reservationService; @Autowired private ObjectMapper mapper; @WithMockUser(username = "pawelcwik", roles = {"RECEPTION"} ) @Test public void getFreeRoomsHappyPath() throws Exception { //given String url = "/api/getFreeRooms?from=2022-03-12&to=2022-03-13&size=2"; LocalDate fromDate = LocalDate.parse("2022-03-12"); LocalDate toDate = LocalDate.parse("2022-03-13"); int size = 2; Room r = new Room("101", new ArrayList<>()); r.setId(101); Mockito.when(reservationService.getAvailableRooms(fromDate, toDate, size)).thenReturn(Arrays.asList(r)); MockHttpServletRequestBuilder request = get(url); //when MvcResult result = mockMvc.perform(request).andReturn(); //then MockHttpServletResponse response = result.getResponse(); CollectionType dtoCollection = mapper.getTypeFactory().constructCollectionType(List.class, RoomAvailableDTO.class); List<RoomAvailableDTO> results = mapper.readValue(response.getContentAsString(), dtoCollection); assertTrue(response.getStatus() == HttpStatus.OK.value()); assertTrue(response.getContentType().equals("application/json")); assertTrue(results.size()==1); assertTrue(results.get(0).getNumber().equals("101")); Mockito.verify(reservationService, Mockito.times(1)) .getAvailableRooms(fromDate, toDate, size); } }
Adnotacja @WithMockUser
służy do ustawiania kontekstu bezpieczeństwa w jakim dany test ma zostać wykonany, więc jest to inny temat niż mockowanie zależności (ale jeśli chcesz bym o nim trochę napisał – daj znać w komentarzach albo mailowo).
By być na bieżąco i mieć realny wpływ na tematykę tworzonych artykułów zapraszam do dołączenia do mojego newslettera.