Mockito
I forrige kapittel repeterte vi hvordan man skriver enhetstester. Likevel er dette ikke alltid hensiktsmessig i vår applikasjon, fordi testing av en controller krever testing av service, og testing av service forutsetter testing av repository.
Denne avhengighetskjeden reiser spørsmålet: Må vi teste alle moduler og deres avhengigheter, eller kan vi simulere deres oppførsel — med andre ord, mocke dem?
Mockito: Introduksjon
Mockito er et populært bibliotek for Java som brukes til å lage mock-objekter i enhetstester.
Det lar utviklere simulere oppførselen til komplekse eksterne avhengigheter (som databaser, tjenester eller API-er) for å isolere og teste logikken til en spesifikk komponent (vanligvis en klasse eller metode) uten å interagere med disse eksterne systemene.
Dette gjør enhetstesting mer forutsigbar og raskere siden det ikke er behov for reelle eksterne ressurskall under testing.
Mock-objekter i Mockito
Dette gjør det mulig for utviklere å sikre at komponenten som testes (enheten) oppfører seg korrekt uten å være avhengig av reelle avhengigheter som databaser eller tredjeparts tjenester.
En klasse kan annoteres med @Mock for å opprette en mock-versjon. Denne annotasjonen brukes ofte sammen med @InjectMocks for å automatisk injisere avhengigheter i klassen som testes.
@Mock
private MyService myServiceMock;
Dette mock-objektet kan deretter injiseres som en avhengighet i et annet objekt ved å bruke @InjectMocks-annotasjonen. Denne annotasjonen injiserer automatisk mock-objektene i klassen som testes.
@InjectMocks
private MyController myController;
Viktige metoder i Mockito
Mockito tilbyr et bredt utvalg av metoder for å håndtere mock-objekter, hver med et spesifikt formål i testprosessen. Nedenfor er de mest viktige metodene sammen med deres forklaringer.
Testing av kontrollere med Mockito
Testing av kontrollere innebærer ofte bruk av mocks for tjenester som kalles innenfor kontrolleren. I denne sammenhengen spiller Mockito en viktig rolle ved å isolere kontrollerens logikk fra tjenestelaget.
Kort oppsummering av videoen
Vi tester kontrollere ved å bruke MockMvc klassen, men hva tilbyr den og hva er fordelene ved å bruke den?
@Autowired
private MockMvc mockMvc;
Med MockMvc kan du simulere ulike HTTP-forespørsler (GET, POST, PUT, osv.), sende parametere, headere, og verifisere responser, statuskoder, headere og responsinnhold, noe som gjør enhetstesting av kontrollere mye enklere.
For eksempel kan du bruke metoder som perform() for å utføre forespørselen, andExpect() for å verifisere de forventede resultatene, og content() for å sjekke innholdet i responsen.
Test.java
1234mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk())
I tillegg kan du kjedekoble påstander for å kontrollere HTTP-status med status().isOk(), verifisere JSON-respons-strukturer med jsonPath(), og mer.
Test.java
1234.andExpect(jsonPath("$.id").value(bookResponseDTO.getId())) .andExpect(jsonPath("$.name").value(bookResponseDTO.getName())) .andExpect(jsonPath("$.author").value(bookResponseDTO.getAuthor())) .andExpect(jsonPath("$.price").value(bookResponseDTO.getPrice()));
Disse verktøyene muliggjør omfattende testing av controllerens oppførsel uten å faktisk utføre ekte HTTP-kall.
Viktige annotasjoner
Annotasjonen @WebMvcTest(BookController.class) brukes for testing av BookController, og laster kun de komponentene som er nødvendige for den spesifikke controlleren. Den utelater resten av Spring-infrastrukturen, slik at du kan fokusere på å teste controlleren i seg selv.
@WebMvcTest(BookController.class)
public class BookControllerTest
I tillegg oppretter @MockBean-annotasjonen brukt på bookService-feltet en mock-versjon av tjenesten, slik at du kan simulere dens oppførsel. Dette bidrar til å isolere controlleren fra dens avhengigheter og gir mulighet til å fokusere på testing av logikken.
@MockBean
private BookService bookService;
Tester for updateBook-metoden
Vi har skrevet to tester for updateBook-metoden som dekker alle mulige tilfeller for denne metoden. I den første testen verifiserer vi at alt var vellykket og at vår entitet ble oppdatert.
BookControllerTest.java
1234567891011121314151617@Test void testUpdateBook_whenBookExists_shouldReturnUpdatedBook() throws Exception { String bookId = "1"; when(bookService.updateBook(bookId, bookRequestDTO)).thenReturn(bookResponseDTO); mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(bookResponseDTO.getId())) .andExpect(jsonPath("$.name").value(bookResponseDTO.getName())) .andExpect(jsonPath("$.author").value(bookResponseDTO.getAuthor())) .andExpect(jsonPath("$.price").value(bookResponseDTO.getPrice())); verify(bookService).updateBook(bookId, bookRequestDTO); }
Denne testen verifiserer en vellykket oppdatering av en book dersom den finnes i databasen. updateBook()-metoden til tjenesten kalles med bokens Id og et bookRequestDTO-objekt, hvorpå den returnerer et bookResponseDTO-objekt som representerer den oppdaterte boken.
Sjekk av unntakshåndtering
Det finnes også en test hvor et unntak oppstår i updateBook-metoden, og det er nødvendig å verifisere hvordan kontrolleren oppfører seg i en slik situasjon.
BookControllerTest.java
12345678910111213141516@Test void testUpdateBook_whenBookNotFound_shouldReturnApiException() throws Exception { String bookId = "1"; String errorMessage = "ID not found"; when(bookService.updateBook(bookId, bookRequestDTO)) .thenThrow(new ApiException(errorMessage, HttpStatus.NOT_FOUND)); mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.error").value(errorMessage)); verify(bookService).updateBook(bookId, bookRequestDTO); }
Denne testen kontrollerer kontrollerens oppførsel ved forsøk på å oppdatere en bok som ikke finnes i databasen. updateBook()-metoden til tjenesten kalles først med bokens Id og et bookRequestDTO-objekt.
I stedet for et vellykket resultat, kaster tjenesten en ApiException, som indikerer at boken med den gitte Id ikke ble funnet.
Tjenestetesting med Mockito
Ved testing av en tjeneste er det viktig å isolere den fra avhengigheter som repositories eller eksterne API-er. Mockito gjør det mulig å lage mock-objekter for disse avhengighetene og definere deres oppførsel i testene.
Kort oppsummering av videoen
Vi starter med testing av metoden når den oppdaterer data vellykket, det vil si at Id er gyldig. Vi bruker selvfølgelig Mockito for å mocke repositoryets oppførsel.
BookServiceTest.java
123456789101112131415161718192021222324252627282930313233@Test void testUpdateBook_whenBookExists_shouldUpdateAndReturnBook() { BookRequestDTO bookRequestDTO = new BookRequestDTO(); bookRequestDTO.setName("Updated Name"); bookRequestDTO.setAuthor("Updated Author"); bookRequestDTO.setPrice("150"); Book bookWithRepository = new Book(); bookWithRepository.setId("1"); bookWithRepository.setName("Original Name"); bookWithRepository.setAuthor("Original Author"); bookWithRepository.setPrice("100"); Book updateBook = new Book(); updateBook.setId("1"); updateBook.setName("Updated Name"); updateBook.setAuthor("Updated Author"); updateBook.setPrice("150"); when(bookRepository.findById("1")).thenReturn(Optional.of(bookWithRepository)); when(bookRepository.save(bookWithRepository)).thenReturn(updateBook); BookResponseDTO result = bookService.updateBook("1", bookRequestDTO); assertNotNull(result); assertEquals("1", result.getId()); assertEquals("Updated Name", result.getName()); assertEquals("Updated Author", result.getAuthor()); assertEquals("150", result.getPrice()); verify(bookRepository).findById("1"); verify(bookRepository).save(bookWithRepository); }
Metoden bookRepository.findById("1") er mocked til å returnere en eksisterende Book fra repositoryet, og bookRepository.save(bookWithRepository) er mocked til å returnere den oppdaterte boken etter at endringene er lagret.
Metoden updateBook() for tjenesten kalles for å oppdatere boken basert på bookRequestDTO, og returnerer en BookResponseDTO.
assertNotNull(result) sikrer at resultatet ikke er null, noe som indikerer en vellykket oppdatering, mens assertEquals() kontrollerer at ID, name, author og price for den oppdaterte boken samsvarer med forventede verdier.
assertNotNull(result);
assertEquals("1", result.getId());
assertEquals("Updated Name", result.getName());
assertEquals("Updated Author", result.getAuthor());
assertEquals("150", result.getPrice());
Til slutt sørger verify() for at findById() og save() i repository ble kalt med de riktige parameterne.
verify(bookRepository).findById("1");
verify(bookRepository).save(bookWithRepository);
Sjekke et unntak i tjenesten
Denne testen verifiserer at tjenesten kaster en ApiException hvis Book som skal oppdateres ikke finnes i databasen.
BookServiceTest.java
1234567891011121314151617181920@Test void testUpdateBook_whenBookNotFound_shouldThrowApiException() { BookRequestDTO bookRequestDTO = new BookRequestDTO(); bookRequestDTO.setName("Updated Name"); bookRequestDTO.setAuthor("Updated Author"); bookRequestDTO.setPrice("150"); String idTest = "999"; when(bookRepository.findById(idTest)).thenReturn(Optional.empty()); ApiException apiException = assertThrows(ApiException.class, () -> { bookService.updateBook(idTest, bookRequestDTO); }); assertEquals("Not found book by id: " + idTest, apiException.getMessage()); assertEquals(HttpStatus.NOT_FOUND, apiException.getHttpStatus()); verify(bookRepository, never()).save(any(Book.class)); }
Først opprettes et BookRequestDTO objekt med data for oppdateringen, og en testbok-Id som ikke finnes i databasen "999" tildeles. Metoden bookRepository.findById(idTest) blir mock'et til å returnere Optional.empty(), noe som indikerer at det ikke finnes noen bok med den Id-en.
when(bookRepository.findById(idTest)).thenReturn(Optional.empty());
Testen bruker assertThrows()-metoden for å kontrollere at når kallet bookService.updateBook(idTest, bookRequestDTO) utføres, blir en ApiException kastet.
Deretter verifiserer assertEquals()-metodene at unntaksmeldingen samsvarer med forventet tekst og at feilstatusen er lik HttpStatus.NOT_FOUND.
ApiException apiException = assertThrows(ApiException.class, () -> {
bookService.updateBook(idTest, bookRequestDTO);
});
assertEquals("Not found book by id: " + idTest, apiException.getMessage());
assertEquals(HttpStatus.NOT_FOUND, apiException.getHttpStatus());
Metoden verify(bookRepository, never()).save(any(Book.class)) sikrer at boklagrings-metoden save() ikke ble kalt, siden boken ikke ble funnet og oppdateringen ikke ble utført.
verify(bookRepository, never()).save(any(Book.class));
Sammendrag
Mockito er et bibliotek for å lage mock-objekter som lar deg simulere oppførselen til avhengigheter og isolere koden som testes. Dette forenkler testing, gjør den raskere, mer stabil og uavhengig av eksterne ressurser.
Takk for tilbakemeldingene dine!
Spør AI
Spør AI
Spør om hva du vil, eller prøv ett av de foreslåtte spørsmålene for å starte chatten vår
Awesome!
Completion rate improved to 3.45
Mockito
Sveip for å vise menyen
I forrige kapittel repeterte vi hvordan man skriver enhetstester. Likevel er dette ikke alltid hensiktsmessig i vår applikasjon, fordi testing av en controller krever testing av service, og testing av service forutsetter testing av repository.
Denne avhengighetskjeden reiser spørsmålet: Må vi teste alle moduler og deres avhengigheter, eller kan vi simulere deres oppførsel — med andre ord, mocke dem?
Mockito: Introduksjon
Mockito er et populært bibliotek for Java som brukes til å lage mock-objekter i enhetstester.
Det lar utviklere simulere oppførselen til komplekse eksterne avhengigheter (som databaser, tjenester eller API-er) for å isolere og teste logikken til en spesifikk komponent (vanligvis en klasse eller metode) uten å interagere med disse eksterne systemene.
Dette gjør enhetstesting mer forutsigbar og raskere siden det ikke er behov for reelle eksterne ressurskall under testing.
Mock-objekter i Mockito
Dette gjør det mulig for utviklere å sikre at komponenten som testes (enheten) oppfører seg korrekt uten å være avhengig av reelle avhengigheter som databaser eller tredjeparts tjenester.
En klasse kan annoteres med @Mock for å opprette en mock-versjon. Denne annotasjonen brukes ofte sammen med @InjectMocks for å automatisk injisere avhengigheter i klassen som testes.
@Mock
private MyService myServiceMock;
Dette mock-objektet kan deretter injiseres som en avhengighet i et annet objekt ved å bruke @InjectMocks-annotasjonen. Denne annotasjonen injiserer automatisk mock-objektene i klassen som testes.
@InjectMocks
private MyController myController;
Viktige metoder i Mockito
Mockito tilbyr et bredt utvalg av metoder for å håndtere mock-objekter, hver med et spesifikt formål i testprosessen. Nedenfor er de mest viktige metodene sammen med deres forklaringer.
Testing av kontrollere med Mockito
Testing av kontrollere innebærer ofte bruk av mocks for tjenester som kalles innenfor kontrolleren. I denne sammenhengen spiller Mockito en viktig rolle ved å isolere kontrollerens logikk fra tjenestelaget.
Kort oppsummering av videoen
Vi tester kontrollere ved å bruke MockMvc klassen, men hva tilbyr den og hva er fordelene ved å bruke den?
@Autowired
private MockMvc mockMvc;
Med MockMvc kan du simulere ulike HTTP-forespørsler (GET, POST, PUT, osv.), sende parametere, headere, og verifisere responser, statuskoder, headere og responsinnhold, noe som gjør enhetstesting av kontrollere mye enklere.
For eksempel kan du bruke metoder som perform() for å utføre forespørselen, andExpect() for å verifisere de forventede resultatene, og content() for å sjekke innholdet i responsen.
Test.java
1234mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk())
I tillegg kan du kjedekoble påstander for å kontrollere HTTP-status med status().isOk(), verifisere JSON-respons-strukturer med jsonPath(), og mer.
Test.java
1234.andExpect(jsonPath("$.id").value(bookResponseDTO.getId())) .andExpect(jsonPath("$.name").value(bookResponseDTO.getName())) .andExpect(jsonPath("$.author").value(bookResponseDTO.getAuthor())) .andExpect(jsonPath("$.price").value(bookResponseDTO.getPrice()));
Disse verktøyene muliggjør omfattende testing av controllerens oppførsel uten å faktisk utføre ekte HTTP-kall.
Viktige annotasjoner
Annotasjonen @WebMvcTest(BookController.class) brukes for testing av BookController, og laster kun de komponentene som er nødvendige for den spesifikke controlleren. Den utelater resten av Spring-infrastrukturen, slik at du kan fokusere på å teste controlleren i seg selv.
@WebMvcTest(BookController.class)
public class BookControllerTest
I tillegg oppretter @MockBean-annotasjonen brukt på bookService-feltet en mock-versjon av tjenesten, slik at du kan simulere dens oppførsel. Dette bidrar til å isolere controlleren fra dens avhengigheter og gir mulighet til å fokusere på testing av logikken.
@MockBean
private BookService bookService;
Tester for updateBook-metoden
Vi har skrevet to tester for updateBook-metoden som dekker alle mulige tilfeller for denne metoden. I den første testen verifiserer vi at alt var vellykket og at vår entitet ble oppdatert.
BookControllerTest.java
1234567891011121314151617@Test void testUpdateBook_whenBookExists_shouldReturnUpdatedBook() throws Exception { String bookId = "1"; when(bookService.updateBook(bookId, bookRequestDTO)).thenReturn(bookResponseDTO); mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isOk()) .andExpect(jsonPath("$.id").value(bookResponseDTO.getId())) .andExpect(jsonPath("$.name").value(bookResponseDTO.getName())) .andExpect(jsonPath("$.author").value(bookResponseDTO.getAuthor())) .andExpect(jsonPath("$.price").value(bookResponseDTO.getPrice())); verify(bookService).updateBook(bookId, bookRequestDTO); }
Denne testen verifiserer en vellykket oppdatering av en book dersom den finnes i databasen. updateBook()-metoden til tjenesten kalles med bokens Id og et bookRequestDTO-objekt, hvorpå den returnerer et bookResponseDTO-objekt som representerer den oppdaterte boken.
Sjekk av unntakshåndtering
Det finnes også en test hvor et unntak oppstår i updateBook-metoden, og det er nødvendig å verifisere hvordan kontrolleren oppfører seg i en slik situasjon.
BookControllerTest.java
12345678910111213141516@Test void testUpdateBook_whenBookNotFound_shouldReturnApiException() throws Exception { String bookId = "1"; String errorMessage = "ID not found"; when(bookService.updateBook(bookId, bookRequestDTO)) .thenThrow(new ApiException(errorMessage, HttpStatus.NOT_FOUND)); mockMvc.perform(put("/books/{id}", bookId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(bookRequestDTO))) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.error").value(errorMessage)); verify(bookService).updateBook(bookId, bookRequestDTO); }
Denne testen kontrollerer kontrollerens oppførsel ved forsøk på å oppdatere en bok som ikke finnes i databasen. updateBook()-metoden til tjenesten kalles først med bokens Id og et bookRequestDTO-objekt.
I stedet for et vellykket resultat, kaster tjenesten en ApiException, som indikerer at boken med den gitte Id ikke ble funnet.
Tjenestetesting med Mockito
Ved testing av en tjeneste er det viktig å isolere den fra avhengigheter som repositories eller eksterne API-er. Mockito gjør det mulig å lage mock-objekter for disse avhengighetene og definere deres oppførsel i testene.
Kort oppsummering av videoen
Vi starter med testing av metoden når den oppdaterer data vellykket, det vil si at Id er gyldig. Vi bruker selvfølgelig Mockito for å mocke repositoryets oppførsel.
BookServiceTest.java
123456789101112131415161718192021222324252627282930313233@Test void testUpdateBook_whenBookExists_shouldUpdateAndReturnBook() { BookRequestDTO bookRequestDTO = new BookRequestDTO(); bookRequestDTO.setName("Updated Name"); bookRequestDTO.setAuthor("Updated Author"); bookRequestDTO.setPrice("150"); Book bookWithRepository = new Book(); bookWithRepository.setId("1"); bookWithRepository.setName("Original Name"); bookWithRepository.setAuthor("Original Author"); bookWithRepository.setPrice("100"); Book updateBook = new Book(); updateBook.setId("1"); updateBook.setName("Updated Name"); updateBook.setAuthor("Updated Author"); updateBook.setPrice("150"); when(bookRepository.findById("1")).thenReturn(Optional.of(bookWithRepository)); when(bookRepository.save(bookWithRepository)).thenReturn(updateBook); BookResponseDTO result = bookService.updateBook("1", bookRequestDTO); assertNotNull(result); assertEquals("1", result.getId()); assertEquals("Updated Name", result.getName()); assertEquals("Updated Author", result.getAuthor()); assertEquals("150", result.getPrice()); verify(bookRepository).findById("1"); verify(bookRepository).save(bookWithRepository); }
Metoden bookRepository.findById("1") er mocked til å returnere en eksisterende Book fra repositoryet, og bookRepository.save(bookWithRepository) er mocked til å returnere den oppdaterte boken etter at endringene er lagret.
Metoden updateBook() for tjenesten kalles for å oppdatere boken basert på bookRequestDTO, og returnerer en BookResponseDTO.
assertNotNull(result) sikrer at resultatet ikke er null, noe som indikerer en vellykket oppdatering, mens assertEquals() kontrollerer at ID, name, author og price for den oppdaterte boken samsvarer med forventede verdier.
assertNotNull(result);
assertEquals("1", result.getId());
assertEquals("Updated Name", result.getName());
assertEquals("Updated Author", result.getAuthor());
assertEquals("150", result.getPrice());
Til slutt sørger verify() for at findById() og save() i repository ble kalt med de riktige parameterne.
verify(bookRepository).findById("1");
verify(bookRepository).save(bookWithRepository);
Sjekke et unntak i tjenesten
Denne testen verifiserer at tjenesten kaster en ApiException hvis Book som skal oppdateres ikke finnes i databasen.
BookServiceTest.java
1234567891011121314151617181920@Test void testUpdateBook_whenBookNotFound_shouldThrowApiException() { BookRequestDTO bookRequestDTO = new BookRequestDTO(); bookRequestDTO.setName("Updated Name"); bookRequestDTO.setAuthor("Updated Author"); bookRequestDTO.setPrice("150"); String idTest = "999"; when(bookRepository.findById(idTest)).thenReturn(Optional.empty()); ApiException apiException = assertThrows(ApiException.class, () -> { bookService.updateBook(idTest, bookRequestDTO); }); assertEquals("Not found book by id: " + idTest, apiException.getMessage()); assertEquals(HttpStatus.NOT_FOUND, apiException.getHttpStatus()); verify(bookRepository, never()).save(any(Book.class)); }
Først opprettes et BookRequestDTO objekt med data for oppdateringen, og en testbok-Id som ikke finnes i databasen "999" tildeles. Metoden bookRepository.findById(idTest) blir mock'et til å returnere Optional.empty(), noe som indikerer at det ikke finnes noen bok med den Id-en.
when(bookRepository.findById(idTest)).thenReturn(Optional.empty());
Testen bruker assertThrows()-metoden for å kontrollere at når kallet bookService.updateBook(idTest, bookRequestDTO) utføres, blir en ApiException kastet.
Deretter verifiserer assertEquals()-metodene at unntaksmeldingen samsvarer med forventet tekst og at feilstatusen er lik HttpStatus.NOT_FOUND.
ApiException apiException = assertThrows(ApiException.class, () -> {
bookService.updateBook(idTest, bookRequestDTO);
});
assertEquals("Not found book by id: " + idTest, apiException.getMessage());
assertEquals(HttpStatus.NOT_FOUND, apiException.getHttpStatus());
Metoden verify(bookRepository, never()).save(any(Book.class)) sikrer at boklagrings-metoden save() ikke ble kalt, siden boken ikke ble funnet og oppdateringen ikke ble utført.
verify(bookRepository, never()).save(any(Book.class));
Sammendrag
Mockito er et bibliotek for å lage mock-objekter som lar deg simulere oppførselen til avhengigheter og isolere koden som testes. Dette forenkler testing, gjør den raskere, mer stabil og uavhengig av eksterne ressurser.
Takk for tilbakemeldingene dine!