1.Intro
Spring MVC is the one of most popular framework for REST services in java. Let's see how can we test our Spring MVC rest serves.2. Project structure
I created simple gradle spring boot project. It has beside main app starter just one rest controller (UserController), two domain entities (Greeting, User) and test: unit and integration.3. build.gradle
Beside spring-boot dependencies I just added Lombok to reduce some boilerplate code for domain objects.buildscript { ext { springBootVersion = '2.1.1.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'java'apply plugin: 'eclipse'apply plugin: 'org.springframework.boot'apply plugin: 'io.spring.dependency-management' group = 'com.demien'version = '0.0.1-SNAPSHOT'sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { implementation('org.springframework.boot:spring-boot-starter-web') compileOnly('org.projectlombok:lombok') testImplementation('org.springframework.boot:spring-boot-starter-test') }
4. Main application starter
Nothing interesting is here. Just SpringApplication.runpackage com.demien.sprmvc; import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplicationpublic class SprmvcApplication { public static void main(String[] args) { SpringApplication.run(SprmvcApplication.class, args); } }
5. Domain objects
Thanks to Lombok, my domain objects are really tiny!package com.demien.sprmvc.domain;
import java.util.Date; import lombok.AllArgsConstructor;import lombok.Getter;import lombok.NoArgsConstructor;import lombok.Setter; @Getter@Setter@NoArgsConstructor@AllArgsConstructorpublic class Greeting { private String message; private Date dt;}
package com.demien.sprmvc.domain; import lombok.AllArgsConstructor;import lombok.Getter;import lombok.NoArgsConstructor;import lombok.Setter; @Getter@Setter@AllArgsConstructor@NoArgsConstructorpublic class User { private String name;}
6. Rest controller
Finally we've reached something interesting - the controller we are going to test.I created several methods: rest endpoints, from simple to complex:
- "hello" - GET which is jest returning text result
- "hello-with-object" - GET but it's returning java object. So object should be serialized and returned as JSON
- "hello-with-parameter" - GET which has a path paameter
- "helo-post" - POST which is receiving java object and returning java object as well. Of course these objects will be serialized to JSON.
package com.demien.sprmvc.controller; import java.net.URI;import java.util.Date; import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import com.demien.sprmvc.domain.Greeting;import com.demien.sprmvc.domain.User; @Controllerpublic class UserController { private static final String helloWorldTemplate = "Hello World, %s!"; private int id = 1; @RequestMapping(value = "/hello") public @ResponseBody String hello() { return "Hello world!"; } @GetMapping("/hello-with-object") public @ResponseBody Greeting helloWithObject() { return new Greeting("Hello World", new Date()); } @GetMapping("/hello-with-parameter/name/{name}") public @ResponseBody Greeting helloWithParameter(@PathVariable String name) { return new Greeting(String.format(helloWorldTemplate, name), new Date()); } @PostMapping("/hello-post") public ResponseEntity<?> postTest(@RequestBody User user) { Greeting result = new Greeting(String.format(helloWorldTemplate, user.getName()), new Date()); URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").buildAndExpand(id++).toUri(); return ResponseEntity.created(location).body(result); } }
7. Unit test
And now let's check if our controller works as expected. For unit tests we're using MockMVC. And also we need ObjectMapper for JSON serialization.Please pay attention on this annotation:
@WebMvcTest(UserController.class)
- our mockMvc will be created for UserController class.
package com.demien.sprmvc.controller; import static org.hamcrest.Matchers.containsString;import static org.hamcrest.Matchers.equalTo;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;import org.springframework.http.MediaType;import org.springframework.test.context.junit4.SpringRunner;import org.springframework.test.web.servlet.MockMvc;import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import com.demien.sprmvc.domain.User;import com.fasterxml.jackson.databind.ObjectMapper; @RunWith(SpringRunner.class) @WebMvcTest(UserController.class) public class UserControllerTest { @Autowired private MockMvc mvc; @Autowired private ObjectMapper mapper; @Test public void helloTest() throws Exception { mvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) .andExpect(content().string(equalTo("Hello world!"))); } @Test public void helloWithObjectTest() throws Exception { mvc.perform(MockMvcRequestBuilders.get("/hello-with-object").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andExpect(content().string(containsString("Hello World"))); } @Test public void helloWithParameterTest() throws Exception { mvc.perform(MockMvcRequestBuilders.get("/hello-with-parameter/name/Buddy").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()).andExpect(content().string(containsString("Hello World, Buddy"))); } @Test public void postTest() throws Exception { User user = new User("Joe"); String userJson = mapper.writeValueAsString(user); mvc.perform( MockMvcRequestBuilders.post("/hello-post").content(userJson).contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isCreated()).andExpect(content().string(containsString("Hello World, Joe"))); } }
Results:
8. Integration test
For integration test we can not use any mocks - just real rest services. So we have to start our application@SpringBootTest(classes = SprmvcApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
and test needed rest enpoints using TestRestTemplate.
package com.demien.sprmvc; import static org.hamcrest.MatcherAssert.assertThat;import static org.hamcrest.Matchers.containsString;import static org.hamcrest.Matchers.equalTo; import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.boot.test.web.client.TestRestTemplate;import org.springframework.boot.web.server.LocalServerPort;import org.springframework.http.ResponseEntity;import org.springframework.test.context.junit4.SpringRunner; import com.demien.sprmvc.domain.Greeting;import com.demien.sprmvc.domain.User; @RunWith(SpringRunner.class) @SpringBootTest(classes = SprmvcApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class SprmvcApplicationIT { private static final String LOCAL_HOST = "http://localhost:"; @LocalServerPort private int port; private TestRestTemplate template = new TestRestTemplate(); @Test public void helloTest() throws Exception { ResponseEntity<String> response = template.getForEntity(createURL("/hello"), String.class); assertThat(response.getBody(), equalTo("Hello world!")); } private String createURL(String uri) { return LOCAL_HOST + port + uri; } @Test public void helloWithObjectTest() throws Exception { ResponseEntity<String> response = template.getForEntity(createURL("/hello-with-object"), String.class); assertThat(response.getBody(), containsString("Hello World")); } @Test public void helloWithParameterTest() throws Exception { ResponseEntity<String> response = template.getForEntity(createURL("/hello-with-parameter/name/Buddy"), String.class); assertThat(response.getBody(), containsString("Hello World, Buddy")); } @Test public void postTest() throws Exception { User userBean = new User("Joe"); ResponseEntity<Greeting> response = template.postForEntity(createURL("/hello-post"), userBean, Greeting.class); Greeting result = response.getBody(); assertThat(result.getMessage(), containsString("Hello World, Joe")); } }
Results:
9. The end
Unit and integration tests for out rest controller are in place, so we're good :)Full source code can be downloaded from here.