From 0ddd30a24502cb84c52c02a025e6cdf180276e55 Mon Sep 17 00:00:00 2001 From: djmil Date: Wed, 26 Jul 2023 16:10:33 +0200 Subject: [PATCH] Testing: mocking --- README.md | 88 ++++++++++++++++++- .../djmil/hellomvc/GreetingController.java | 11 ++- .../java/djmil/hellomvc/GreetingService.java | 12 +++ src/main/resources/templates/greeting.html | 1 + .../hellomvc/CreetingControllerTests.java | 41 +++++++++ .../java/djmil/hellomvc/WebLayerTest.java | 27 ------ 6 files changed, 150 insertions(+), 30 deletions(-) create mode 100644 src/main/java/djmil/hellomvc/GreetingService.java create mode 100644 src/test/java/djmil/hellomvc/CreetingControllerTests.java delete mode 100644 src/test/java/djmil/hellomvc/WebLayerTest.java diff --git a/README.md b/README.md index 75620d0..028b329 100644 --- a/README.md +++ b/README.md @@ -269,4 +269,90 @@ public class WebLayerTest { } ``` -The test assertion is the same as in the previous case. However, in this test, Spring Boot instantiates only the web layer rather than the whole context. In an application with multiple controllers, you can even ask for only one to be instantiated by using, for example, `@WebMvcTest(HomeController.class)`. \ No newline at end of file +The test assertion is the same as in the previous case. However, in this test, Spring Boot instantiates only the web layer rather than the whole context. In an application with multiple controllers, you can even ask for only one to be instantiated by using, for example, `@WebMvcTest(HomeController.class)`. + +## Testing with mocks + +So far, our `GreetingController` is simple and has no dependencies. We could make it more realistic by introducing an extra component, say another service which will harbor some business logic, in our case - calculate length of a URL `name` parameter. + +`src/main/java/djmil/hellomvc/GreetingService.java` +```java +package djmil.hellomvc; + +import org.springframework.stereotype.Service; +import org.springframework.ui.Model; + +@Service +public class GreetingService { + + public int nameLength(String name) { + return name.length(); + } + +} +``` + +In SpringBoot [`@Service`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/stereotype/Service.html) annotation is usually used to mark business logic classes. Doing so allows SpringBoot IoC to handle dependencies between different classes. Here, we have to update `GreetingsControler` to use `GrretingsSrvice` to handle `/greeting` request: + +```java +@Controller +public class GreetingController { + + private final GreetingService greetigService; + + public GreetingController(GreetingService greetigService) { + this.greetigService = greetigService; + } + + @GetMapping("/greeting") + public String greeting(@RequestParam(name="name", required=false, defaultValue="World") String name, Model model) { + + model.addAttribute("name", name); + model.addAttribute("nameLength", greetigService.nameLength(name)); + // NOTE: Here we can request some data from our RESTful backend as well + + return "greeting"; // view tempalte + } + +} +``` + +Spring IoC will insert instance of `GreetingService` as a constructor parameter of `GreetingController`, which we store as a private class member for later use. + +Also, to make full use of newly created service, update `src\main\resources\templates\greeting.html`: + +```html +

+

+``` + +Run the tests, and notice that only `GreetingControllerTest` has failed. + +### Mocking + +To fix the test, we have to provide mock implementation of a `GreetingService` class inside `GreetingControllerTest`: + +```java +import static org.mockito.Mockito.when; + +@WebMvcTest(GreetingController.class) +public class CreetingControllerTests { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private GreetingService service; + + @Test + public void greetingShouldReturnMessageFromService() throws Exception { + when(service.nameLength("World")).thenReturn(7); + this.mockMvc.perform(get("/greeting")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Length of a given name is 7 characters."))); + } +} +``` + +Notice the `@MockBean` annotation, which is essentially used to obtain an interface of a mocked class. Later, `when` function used to define an actual mock. \ No newline at end of file diff --git a/src/main/java/djmil/hellomvc/GreetingController.java b/src/main/java/djmil/hellomvc/GreetingController.java index c39d8ee..3a467dc 100644 --- a/src/main/java/djmil/hellomvc/GreetingController.java +++ b/src/main/java/djmil/hellomvc/GreetingController.java @@ -8,13 +8,20 @@ import org.springframework.web.bind.annotation.RequestParam; @Controller public class GreetingController { + private final GreetingService greetigService; + + public GreetingController(GreetingService greetigService) { + this.greetigService = greetigService; + } + @GetMapping("/greeting") public String greeting(@RequestParam(name="name", required=false, defaultValue="World") String name, Model model) { - // NOTE: Here we can request some data from our RESTful backend as well model.addAttribute("name", name); + model.addAttribute("nameLength", greetigService.nameLength(name)); + // NOTE: Here we can request some data from our RESTful backend as well - return "greeting"; // <<-- template + return "greeting"; // view tempalte } } \ No newline at end of file diff --git a/src/main/java/djmil/hellomvc/GreetingService.java b/src/main/java/djmil/hellomvc/GreetingService.java new file mode 100644 index 0000000..f5be18b --- /dev/null +++ b/src/main/java/djmil/hellomvc/GreetingService.java @@ -0,0 +1,12 @@ +package djmil.hellomvc; + +import org.springframework.stereotype.Service; + +@Service +public class GreetingService { + + public int nameLength(String name) { + return name.length(); + } + +} diff --git a/src/main/resources/templates/greeting.html b/src/main/resources/templates/greeting.html index 19839bc..f9fb5a3 100644 --- a/src/main/resources/templates/greeting.html +++ b/src/main/resources/templates/greeting.html @@ -6,6 +6,7 @@

+

diff --git a/src/test/java/djmil/hellomvc/CreetingControllerTests.java b/src/test/java/djmil/hellomvc/CreetingControllerTests.java new file mode 100644 index 0000000..c3e4cc8 --- /dev/null +++ b/src/test/java/djmil/hellomvc/CreetingControllerTests.java @@ -0,0 +1,41 @@ +package djmil.hellomvc; + +import static org.hamcrest.Matchers.containsString; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(GreetingController.class) +public class CreetingControllerTests { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private GreetingService service; + + // @Test + // public void shouldReturnDefaultMessage() throws Exception { + // this.mockMvc.perform(get("/greeting")) + // .andDo(print()) + // .andExpect(status().isOk()) + // .andExpect(content().string(containsString("Hello, World"))); + // } + + @Test + public void greetingShouldReturnMessageFromService() throws Exception { + when(service.nameLength("World")).thenReturn(7); + this.mockMvc.perform(get("/greeting")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("Length of a given name is 7 characters."))); + } +} \ No newline at end of file diff --git a/src/test/java/djmil/hellomvc/WebLayerTest.java b/src/test/java/djmil/hellomvc/WebLayerTest.java deleted file mode 100644 index cce88bc..0000000 --- a/src/test/java/djmil/hellomvc/WebLayerTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package djmil.hellomvc; - -import static org.hamcrest.Matchers.containsString; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.test.web.servlet.MockMvc; - -@WebMvcTest -public class WebLayerTest { - - @Autowired - private MockMvc mockMvc; - - @Test - public void shouldReturnDefaultMessage() throws Exception { - this.mockMvc.perform(get("/greeting")) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(content().string(containsString("Hello, World"))); - } -} \ No newline at end of file