Catalogue of series articles
[Chapter 1 springboot+junit5 exercise, from simple to deep (I)]( https://editor.csdn.net/md?not_checkout=1&articleId=120584779 )preface
All along, my development mode has been: requirements analysis - > code implementation - > writing unit tests. Through the understanding of the concept of Test Driven Development (TDD), I decided to try a new development mode: requirements analysis - > design unit tests - > code function implementation.
1, Demand analysis
First of all, I refer to the back-end requirements of a simple message board website, as follows:
-
Users can register on the website
- Username, password and email are required.
- username needs to be checked: it cannot be empty. Only letters and numbers can be used. The length is between 5 and 20 and cannot be duplicate with the existing user name
- password needs to be checked: it cannot be empty, its length is between 8 and 20, and it contains at least one uppercase, one lowercase, one number and one special symbol
- Email needs to be checked: it cannot be empty, the format must be correct, and cannot be duplicate with existing email. For simplicity, you do not need to send an email confirmation
-
Users can log in on the website
- Log in using username+password, or email+password
- "remember me" function is provided. You do not need to log in again within one month after logging in
- If "remember me" is not checked, you will be prompted to register or log in again after closing the browser
- After logging in, you can get user information
-
After logging in, users can post messages
- The message length is between 3 ~ 200 words, which can be in Chinese
- The posting time of the message will be recorded
-
technical requirement
- Restful API provided by the backend
2, Build system
For simplicity and convenience, I plan to use SpringBoot + Sqlite, and the development tool uses IDEA
1. Build the framework
IDEA is very simple to build SpringBoot. After searching a lot on the Internet, I won't build wheels again. Here is a directory structure diagram:
- Maven configuration:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.xerial</groupId> <artifactId>sqlite-jdbc</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.31</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.13</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> </dependencies>
Here, I would like to explain why the org.junit.vintage item should be excluded, because junit-jupiter-engine is used in JUnit 5 in this exercise, and org.junit.vintage (junit-vintage-engine) is the engine used by JUnit 4. If you use junit-vintage-engine in JUnit 5, an error will be reported.
2. Design Controller and Service
According to the requirements, create the user module REST interface UserController and user module service UserService. This module provides simple functions of registering, logging in and obtaining the current logged in user information. The code design is as follows:
- Controller:
import com.practice.comments.dto.LoginDTO; import com.practice.comments.dto.RegisterDTO; import com.practice.comments.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController @EnableAutoConfiguration public class UserController { @Autowired UserService userService; @PostMapping("/register") public String register(@RequestBody RegisterDTO registerDTO){ return userService.register(registerDTO); } @PostMapping("/logon") public String login(@RequestBody LoginDTO loginDTO){ return userService.login(loginDTO); } @GetMapping("/userInfo") public String getUserInfo(){ return userService.getUserInfo(); } }
- Service:
import com.practice.comments.dto.LoginDTO; import com.practice.comments.dto.RegisterDTO; import org.springframework.stereotype.Service; @Service public interface UserService { String register(RegisterDTO registerDTO); String login(LoginDTO loginDTO); String getUserInfo(); }
- DTO:
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class loginDTO { private String userName; private String userPassword; private boolean rememberMe; }
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class RegisterDTO { private String userName; private String userPassword; private String email; }
3, Design unit test
Well, before implementing specific functions, you can design preliminary unit tests according to the existing framework and interfaces. In IDEA, you can quickly generate the test code of the class, as shown in the following figure. Right click - select Go To - Test:
Then enter the Test class name. By default, Test is added after the Test class name, as shown in the figure below. If it is checked, it can also be added after it
The generated test classes are as follows:
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class UserControllerTest { @BeforeEach void setUp() {//Each test method is called before it is called } @AfterEach void tearDown() {//After each test method is called, it will be called again } @Test void register() { } @Test void login() { } @Test void getUserInfo() { } }
First, design the test of registration function. The registration requirements are as follows:
-
Users can register on the website
-
Username, password and email are required.
- username needs to be checked: it cannot be empty. Only letters and numbers can be used. The length is between 5 and 20 and cannot be duplicate with the existing user name
- password needs to be checked: it cannot be empty, its length is between 8 and 20, and it contains at least one uppercase, one lowercase, one number and one special symbol
- Email needs to be checked: it cannot be empty, the format must be correct, and cannot be duplicate with existing email. For simplicity, you do not need to send an email confirmation
The filled test codes are as follows:
import com.practice.comments.service.UserService; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; 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.http.HttpMethod; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import static org.mockito.Mockito.*; import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; @WebMvcTest(controllers = UserController.class) class UserControllerTest { private static final String APPLICATION_JSON_CHARSET_UTF_8 = "application/json;charset=UTF-8"; @Autowired MockMvc mockMvc; @Test void register() throws Exception { String url = "/register"; String contentJson = "{\"userName\":\"test1\",\"userPassword\":\"123abc\",\"email\":\"123@345.com\"}"; String resultJson="{\"status\":\"0\",\"content\":\"Registration succeeded!\"}"; mockMvc.perform( MockMvcRequestBuilders.request(HttpMethod.POST, url) // Set the return value type to json utf-8, otherwise it defaults to ISO-8859-1 .accept(APPLICATION_JSON_CHARSET_UTF_8) .contentType(APPLICATION_JSON_CHARSET_UTF_8).content(contentJson)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content() .string(containsString(resultJson))) .andDo(print()); } }
Here is an explanation of using the @ WebMvcTest annotation. This annotation can only instantiate the controller in the parameter without starting the whole SpringBoot. It is very suitable for testing when no specific function is implemented.
Then let's introduce MockMvc. MockMvc is a commonly used testing framework, which is mainly used to simulate objects, while MockMvc is specially used to simulate HTTP interaction. In the above code, mockMvc.perform() is used to simulate the initiation of HTTP requests.
Then execute the register() test method, and you will get the following error message:
java.lang.IllegalStateException: Failed to load ApplicationContext ......No qualifying bean of type 'com.practice.comments.service.UserService' available
An error report means that there is no implementation class conforming to UserService. The reason is obvious. I have neither implemented the function of UserService nor instantiated it in the current test class. Therefore, you also need to simulate a UserService through @ mockbean.
Add the following code to UserControllerTest
@MockBean //Simulate a springBean UserService userService;
Then execute the register() test method again, and you will get the following error message:
java.lang.AssertionError: Response content Expected: a string containing "{\"status\":\"0\",\"content\":\"Registration succeeded!\"}" but: was "" Expected :a string containing "{\"status\":\"0\",\"content\":\"Registration succeeded!\"}" Actual :""
The error is that the assertions do not match, that is, the expected results are not returned because userservice is simulated. If the returned results are not simulated, null or empty string or 0 will be returned by default. Therefore, you need to simulate the return value of userservice in advance:
Modify register() as follows:
@Test void register() throws Exception { String url = "/register"; String contentJson = "{\"userName\":\"test1\",\"userPassword\":\"123abc\",\"email\":\"123@345.com\"}"; String resultJson="{\"status\":\"0\",\"content\":\"Registration succeeded!\"}"; //Simulate the return value of userService. any() indicates that it is an arbitrary parameter when(userService.register(any())).thenReturn(resultJson); mockMvc.perform( MockMvcRequestBuilders.request(HttpMethod.POST, url) // Set the return value type to json utf-8, otherwise it defaults to ISO-8859-1 .accept(APPLICATION_JSON_CHARSET_UTF_8) .contentType(APPLICATION_JSON_CHARSET_UTF_8).content(contentJson)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content() .string(containsString(resultJson))) .andDo(print()); }
In this way, whenever the userService.register method is called, it will directly return "{\" status\":\"0\",\"content \ ": \" successful registration! \ "}".
Execute register() again and the test passes.
4, Perfect test
In the previous section, I simply designed the test of successful registration. Next, I need to design the test of failed branches according to the requirements,
The code is modified as follows:
@DisplayName("adopt cvs Provide test data and parameterized test registration function") @ParameterizedTest @CsvFileSource(resources = "/Wrong_Register.csv") void register(String contentJson, String resultJson) throws Exception { String url = "/register"; mockPerform(url, contentJson, resultJson); } private void mockPerform(String url, String contentJson, String resultJson) throws Exception { mockMvc.perform( MockMvcRequestBuilders.request(HttpMethod.POST, url) // Set the return value type to json utf-8, otherwise it defaults to ISO-8859-1 .accept(APPLICATION_JSON_CHARSET_UTF_8) .contentType(APPLICATION_JSON_CHARSET_UTF_8).content(contentJson)) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content() .string(containsString(resultJson))) .andDo(print()); }
Here we use the new feature of JUnit 5: "parametric test", which allows us to run a single test multiple times, and makes each run just different parameters.
You need to add parameters to the Test method and replace the @ Test annotation with: @ ParameterizedTest and @ CsvFileSource(resources = "relative path of external file").
Create a new csv document under resource, open it with Excel, and fill in the columns according to the parameter order. Each row has a group of tests:
- csv
In this way, the test of our user registration function is designed. In the future, as long as we implement specific functions, we can use this test class to test repeatedly (remember to replace the simulated springbbean with the implemented one).
summary
In this chapter, we show the functional features of layered testing and parametric testing through SpringBoot and Junit5, making the code elegant and efficient. In the next chapter, we will introduce different test methods in combination with other scenarios.