Writing unit tests can help developers write high-quality code, improve code quality, reduce bugs, and facilitate refactoring.Spring Boot provides utilities and annotations to help us test applications. Opening unit tests in Spring Boot requires only the introduction of spring-boot-starter-test, which includes some of the mainstream test libraries.This article mainly introduces the unit testing based on Service and Controller.
Introduce spring-boot-starter-test:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
-
JUnit, a standard unit test Java application;
-
Spring Test & Spring Boot Test, which supports unit testing of Spring Boot applications;
-
Mockito, a Java mocking framework that simulates any Spring-managed Bean, such as data returned by a third-party system's Service interface in a unit test, without actually calling a third-party system;
-
AssertJ, a smooth assertion library, also provides more ways to compare expected values with test returns.
-
Hamcrest, the matching object of the library (also known as a constraint or predicate);
-
JsonPath, which provides XPath-like symbols to obtain JSON data fragments;
-
JSONassert, a library of JSON object or JSON string assertions.
A standard Spring Boot test unit should have the following code structure:
import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; /** * @author hekang * @date 2020/01/03 */ @RunWith(SpringRunner.class) @SpringBootTest public class UserControllerTest { }
Knowledge preparation
JUnit4 comment
JUnit4 contains several important notes: @BeforeClass, @AfterClass, @Before ,@After and @Test .Where @BeforeClass and @AfterClass run at the beginning and end of each class load, they must be static methods; whereas @BeforeClass and @AfterClass run Before and After the start and end of each test method.See the following example:
/** * @author hekang * @date 2020/01/03 */ @RunWith(SpringRunner.class) @SpringBootTest public class UserControllerTest { /** * Run at the beginning of class loading */ @BeforeClass public static void beforeClassTest() { System.out.println("before class test Run at the beginning of class loading."); } /** * Run before each test method starts */ @Before public void beforeTest() { System.out.println("before test Run before each test method starts."); } /** * Test Method 1 */ @Test public void Test1() { System.out.println("test1"); Assert.assertEquals(2, 1 + 1); } /** * Test Method 2 */ @Test public void Test2() { System.out.println("test2"); Assert.assertEquals(4, 2 + 2); } /** * Run after the end of each test method */ @After public void afterTest() { System.out.println("after test Run after each test method has finished."); } /** * Run at end of class loading */ @AfterClass public static void afterClassTest() { System.out.println("after class test Run at the end of class loading."); } }
The run output is as follows:
before class test Run at the beginning of class loading. before test Run before each test method starts. test1 after test Run after each test method has finished. before test Run before each test method starts. test2 after test Run after each test method has finished. after class test Run at the end of class loading.
The output above shows the runtime of each comment.
Assert
In the code above, we used the assert port method provided by the Assert class, and the following lists some of the commonly used assert methods:
-
assertEquals("message",A,B), which determines whether the A and B objects are equal, calls the equals() method when comparing the two objects.
-
assertSame("message",A,B), determines whether the A object is the same as the B object, using the==operator.
-
assertTrue("message",A), to determine if condition A is true.
-
assertFalse("message",A), to determine if condition A is not true.
-
assertNotNull("message",A), determines if the A object is not null.
-
assertArrayEquals("message",A,B), to determine if the A and B arrays are equal.
MockMvc
MockMvc technology is required for ontroller testing below.MockMvc, literally, refers to a simulated MVC, that is, it can simulate an MVC environment, send a request to the Controller and get a response.
In a unit test, you need to initialize before using MockMvc, as follows:
import org.junit.Before; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; /** * @author hekang * @date 2020/01/03 */ @RunWith(SpringRunner.class) @SpringBootTest public class UserControllerTest { private MockMvc mockMvc; @Autowired private WebApplicationContext wac; /** * Execute setup simulation Mvc before test method starts */ @Before public void setupMockMvc() { mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); } }
MockMvc simulates MVC requests
Simulate a get request:
@RestController public class UserController { @GetMapping("/getUser") public void getUser(Long id) { System.out.println("user ID: " + id); } }
@Test public void getUserTest() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/getUser?id={id}", "1")); }
Simulate a post request:
@PostMapping("/updateUser/{id}") public void updateUser(@PathVariable("id") Long id) { System.out.println("Modify User ID: " + id); }
@Test public void postUserTest() throws Exception { mockMvc.perform(MockMvcRequestBuilders.post("/updateUser/{id}", "1")); }
Simulated request parameters:
// Simulate sending a message parameter with a value of hello mockMvc.perform(MockMvcRequestBuilders.get("/hello").param("message", "hello")); // Simulate submitting a checkbox value with a name of hobby and values of sleep and eat mockMvc.perform(MockMvcRequestBuilders.get("/saveHobby").param("hobby", "sleep", "eat"));
You can also build parameters directly using MultiValueMap:
MultiValueMap<String, String> params = new LinkedMultiValueMap<String, String>(); params.add("name", "mrbird"); params.add("hobby", "sleep"); params.add("hobby", "eat"); mockMvc.perform(MockMvcRequestBuilders.get("/hobby/save").params(params));
Simulate send JSON parameters:
String jsonStr = "{\"username\":\"Dopa\",\"passwd\":\"ac3af72d9f95161a502fd326865c2f15\",\"status\":\"1\"}"; mockMvc.perform(MockMvcRequestBuilders.post("/user/save").content(jsonStr.getBytes()));
In practical tests, it is cumbersome and error-prone to manually write such a long JSON format string. You can serialize a Java object using the Jackson technology that comes with Spring Boot, as follows:
User user = new User(); user.setUsername("Dopa"); user.setPasswd("ac3af72d9f95161a502fd326865c2f15"); user.setStatus("1"); String userJson = mapper.writeValueAsString(user); mockMvc.perform(MockMvcRequestBuilders.post("/user/save").content(userJson.getBytes()));
Where mapper is a com.fasterxml.jackson.databind.ObjectMapper object.
Simulate Session and Cookie:
mockMvc.perform(MockMvcRequestBuilders.get("/index").sessionAttr(name, value)); mockMvc.perform(MockMvcRequestBuilders.get("/index").cookie(new Cookie(name, value)));
Set the requested Content-Type:
mockMvc.perform(MockMvcRequestBuilders.get("/index").contentType(MediaType.APPLICATION_JSON_UTF8));
Set the return format to JSON:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1).accept(MediaType.APPLICATION_JSON));
Simulate HTTP request header:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1).header(name, values));
MockMvc process returns results
Expect successful invocation, i.e. HTTP Status 200:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1)) .andExpect(MockMvcResultMatchers.status().isOk());
Expected return is application/json:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1)) .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON));
Check what returns a value in the JSON data:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1)) .andExpect(MockMvcResultMatchers.jsonPath("$.username").value("mrbird"));
JsonPath is used here, $represents the root node of JSON.More information about jsonPath is available https://github.com/json-path/JsonPath.
Determine if the Controller method returns a view:
mockMvc.perform(MockMvcRequestBuilders.post("/index")) .andExpect(MockMvcResultMatchers.view().name("index.html"));
Compare Model s:
mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1)) .andExpect(MockMvcResultMatchers.model().size(1)) .andExpect(MockMvcResultMatchers.model().attributeExists("password")) .andExpect(MockMvcResultMatchers.model().attribute("username", "mrbird"));
Compare forward or redirect:
mockMvc.perform(MockMvcRequestBuilders.get("/index")) .andExpect(MockMvcResultMatchers.forwardedUrl("index.html")); // perhaps mockMvc.perform(MockMvcRequestBuilders.get("/index")) .andExpect(MockMvcResultMatchers.redirectedUrl("index.html"));
Compare the returned content using content():
// Return content is hello mockMvc.perform(MockMvcRequestBuilders.get("/index")) .andExpect(MockMvcResultMatchers.content().string("hello")); // The returned content is XML and is the same as xmlCotent mockMvc.perform(MockMvcRequestBuilders.get("/index")) .andExpect(MockMvcResultMatchers.content().xml(xmlContent)); // The returned content is JSON and is the same as jsonContent mockMvc.perform(MockMvcRequestBuilders.get("/index")) .andExpect(MockMvcResultMatchers.content().json(jsonContent));
Output response results:
mockMvc.perform(MockMvcRequestBuilders.get("/index")) .andDo(MockMvcResultHandlers.print());