Saturday, January 5, 2019

Spring MVC: unit and integration tests

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.run

package 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.

No comments:

Post a Comment