系列文章目录
[第一章 springboot+junit5 练习,由浅入深(一)](https://editor.csdn.net/md?not_checkout=1&articleId=120584779)文章目录
前言
一直以来,我的开发模式都是:需求分析——>code实现——>编写单元测试,通过对测试驱动开发 (TDD)的概念理解,我决定尝试新的开发模式:需求分析——>设计单元测试——>code功能实现。
一、需求分析
首先,我参考了一个简单的留言版网站后端需求,如下:
-
用户可以在网站上注册
- 需要填写 username, password, email。
- username需要检查:不可为空,只能使用字母和数字,长度在5~20之间,不能与已有用户名重复
- password需要检查:不可为空,长度在8~20之间,至少包含一个大写、一个小写、一个数字、一个特殊符号
- email需要检查:不可为空,格式要正确,不能与已有email重复。为简单起见,不需要发送邮件确认
-
用户可以在网站上登录
- 使用username+password,或者email+password 登录
- 提供”remember me”功能,登录后一个月内不需要重新登录
- 如果未勾选”remember me”,则关闭浏览器后再次访问会提示注册或登录
- 用户登录后,可以获取用户信息
-
用户登录后,可以发表留言
- 留言长度在3~200字之间,可以为中文
- 会记录留言发表时间
-
技术要求
- 后端提供的Restful API
二、搭建系统
为了简单方便,我打算使用SpringBoot + Sqlite ,开发工具用IDEA
1. 搭建框架
IDEA搭建SpringBoot非常简单,网上一搜一大把,我就不重复造*了,这里贴张目录结构图:
- Maven配置:
<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>
这里特别说明下为什么要排除org.junit.vintage项,因为本次练习的Junit5,使用的是junit-jupiter-engine,org.junit.vintage(junit-vintage-engine)是Junit4的使用的引擎,若你在Junit5中使用了junit-vintage-engine会报错。
2. 设计Controller和Service
根据需求,创建用户模块REST接口UserController和用户模块服务UserService,该模块提供注册,登录,获取当前登录用户信息的简单功能。代码设计如下:
- 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;
}
三、设计单元测试
好了,在实现具体的功能前,可以先根据现有的框架和接口,来设计初步的单元测试。在IDEA中,可以快速生成类的测试代码,如下图,点击右键——选择Go To——Test:
然后输入测试类名,默认是测试的类名后面加Test,如下图,打勾的也可以后面加上
生成测试类如下:
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() {//每个测试方法调用前,都会先调用该方法
}
@AfterEach
void tearDown() {//每个测试方法调用后,都会再调用该方法
}
@Test
void register() {
}
@Test
void login() {
}
@Test
void getUserInfo() {
}
}
先设计注册功能的测试,注册需求如下:
用户可以在网站上注册
需要填写 username, password, email。
- username需要检查:不可为空,只能使用字母和数字,长度在5~20之间,不能与已有用户名重复
- password需要检查:不可为空,长度在8~20之间,至少包含一个大写、一个小写、一个数字、一个特殊符号
- email需要检查:不可为空,格式要正确,不能与已有email重复。为简单起见,不需要发送邮件确认
填充的测试代码如下:
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\":\"注册成功!\"}";
mockMvc.perform(
MockMvcRequestBuilders.request(HttpMethod.POST, url)
// 设置返回值类型为json utf-8,否则默认为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());
}
}
这里解释一下使用@WebMvcTest注解,该注解是可以只针对参数中的controller进行实例化,而不用启动整个SpringBoot,很适合在没有实现具体功能时进行测试。
再介绍下MockMvc,mock是比较常用的测试框架,主要作用是对对象进行模拟,而MockMvc是专门针对Http交互进行模拟,上面代码中就是通过mockMvc.perform()
来模拟发起http请求。
然后执行register()测试方法,会得到如下报错信息:
java.lang.IllegalStateException: Failed to load ApplicationContext
……No qualifying bean of type 'com.practice.comments.service.UserService' available
报错的意思是没有符合UserService的实现类,原因很明显,我根本没有实现UserService的功能,也没有在当前测试类中进行实例化。所以还需要通过@mockbean模拟一个UserService。
在UserControllerTest中加入以下代码
@MockBean //模拟一个springBean
UserService userService;
然后再次执行register()测试方法,会得到如下报错信息:
java.lang.AssertionError: Response content
Expected: a string containing "{\"status\":\"0\",\"content\":\"注册成功!\"}"
but: was ""
Expected :a string containing "{\"status\":\"0\",\"content\":\"注册成功!\"}"
Actual :""
该报错是断言不匹配,即没有返回我期望的结果,原因是因为UserService是模拟的,若不模拟返回结果,则默认返回空或空字符串或0。所以需要提前模拟userService的返回值:
修改register()方法如下:
@Test
void register() throws Exception {
String url = "/register";
String contentJson = "{\"userName\":\"test1\",\"userPassword\":\"123abc\",\"email\":\"123@345.com\"}";
String resultJson="{\"status\":\"0\",\"content\":\"注册成功!\"}";
//模拟userService的返回值。any()表示是任意参数
when(userService.register(any())).thenReturn(resultJson);
mockMvc.perform(
MockMvcRequestBuilders.request(HttpMethod.POST, url)
// 设置返回值类型为json utf-8,否则默认为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());
}
这样,任何时候调用userService.register
方法,都会直接返回"{\"status\":\"0\",\"content\":\"注册成功!\"}"
。
再次执行register(),测试通过了。
四、完善测试
上一节我简单了设计了注册成功的测试,接下来,需要根据需求,设计失败分支的测试,
代码修改如下:
@DisplayName("通过cvs提供测试数据,参数化测试注册功能")
@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)
// 设置返回值类型为json utf-8,否则默认为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());
}
这里用到了Junit5的新特性:“参数化测试”,该特性可以让我们运行单个测试多次,且使得每次运行仅仅是参数不同而已。
需要在测试方法添加参数,把@Test
注解替换成:@ParameterizedTest
和@CsvFileSource(resources = "外部文件相对路径")
。
在resource下新建csv文档,用Excel打开,按参数顺序进行列填充,每行一组测试:
-
csv
这样,我们用户注册功能的测试就设计完成了,后续只要实现具体的功能,就可以利用这个测试类反复进行测试了(记得将模拟的sprinbBean为替换实现的哦~)。
总结
本章我们通过SpringBoot和Junit5,展示了分层测试和参数化测试的功能特性,使得代码优雅效率。下一章,我们将结合其他场景,介绍不同的测试方法。