单元测试实战
2365字约8分钟
2024-06-25
项目中内容分为测试用到的 demo
代码以及单元测试相关代码,基础内容请移步 单元测试
1、引入依赖
依赖
<!-- 单元测试相关依赖 start -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.dbunit</groupId>
<artifactId>dbunit</artifactId>
<version>2.5.0</version>
</dependency>
<dependency>
<groupId>com.github.springtestdbunit</groupId>
<artifactId>spring-test-dbunit</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
<scope>runtime</scope>
</dependency>
<!-- 单元测试相关依赖 end -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2、Demo 代码
统一结果返回实体类
统一结果返回实体类
package com.marui.unit.testing.common;
import lombok.Data;
import java.io.Serializable;
/**
* @ClassName ResultDTO
* @Desciption 统一结果返回
* @Author MaRui
* @address https://maruinotes.com
* @Date 2022/2/10 9:46
* @Version 1.0
*/
@Data
public class ResultDTO<T> implements Serializable {
/**
* 业务处理结果编码
*/
private String code;
/**
* 业务处理结果信息
*/
private String msg;
/**
* 业务处理响应数据
*/
private T data;
public ResultDTO() {
}
public ResultDTO(String code) {
this.code = code;
}
public ResultDTO(String code, String msg) {
this.code = code;
this.msg = msg;
}
public ResultDTO(String code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public static <T> ResultDTO<T> ok() {
return new ResultDTO("000000", "Success");
}
public static <T> ResultDTO<T> ok(T data) {
return new ResultDTO("000000", "Success", data);
}
public static <T> ResultDTO<T> ok(String code, String msg, T data) {
return new ResultDTO(code, msg, data);
}
public static <T> ResultDTO<T> error() {
return new ResultDTO("500", "未知异常,请联系管理员");
}
public static <T> ResultDTO<T> error(String msg) {
return new ResultDTO("500", msg);
}
public static <T> ResultDTO<T> error(String code, String msg) {
return new ResultDTO(code, msg);
}
}
Controller 层
Controller 层
package com.marui.unit.testing.controller;
import com.marui.unit.testing.common.ResultDTO;
import com.marui.unit.testing.entity.User;
import com.marui.unit.testing.service.UserService;
import lombok.Data;
import org.springframework.web.bind.annotation.*;
/**
* @ClassName UserController
* @Desciption 用户控制层
* @Author MaRui
* @Date 2023/2/15 16:42
* @Version 1.0
*/
@Data
@RestController
@RequestMapping("/user")
public class UserController {
private final UserService userService;
/**
* 添加用户
* @param user 用户信息
* @return 添加后的用户信息
*/
@PostMapping(value = "/add")
public ResultDTO add(@RequestBody User user) {
User resultUser = userService.add(user);
return resultUser != null ? ResultDTO.ok(user) : ResultDTO.error();
}
/**
* 删除用户
* @param id 用户ID
* @return
*/
@DeleteMapping(value = "/delete")
public ResultDTO delete(@RequestParam Long id) {
userService.delete(id);
return ResultDTO.ok();
}
/**
* 修改用户
* @param user 修改用户数据
* @return
*/
@PatchMapping(value = "/update")
public ResultDTO update(@RequestBody User user) {
userService.update(user);
return ResultDTO.ok();
}
/**
* 查询用户
* @param id 用户ID
* @return 用户信息
*/
@GetMapping(value = "/getOne")
public ResultDTO<User> getOne(@RequestParam Long id) {
User user = userService.getOne(id);
return ResultDTO.ok(user);
}
}
Service 层
Service 层
package com.marui.unit.testing.service;
import com.marui.unit.testing.entity.User;
/**
* @ClassName UserService
* @Desciption 用户业务逻辑接口层
* @Author MaRui
* @Date 2023/2/15 17:06
* @Version 1.0
*/
public interface UserService {
/**
* 添加用户
* @param user 用户信息
* @return
*/
User add(User user);
/**
* 删除用户
* @param id 用户ID
*/
void delete(Long id);
/**
* 修改用户
* @param user 用户信息
*/
void update(User user);
/**
* 根据ID查询用户
* @param id 用户ID
* @return
*/
User getOne(Long id);
}
package com.marui.unit.testing.service.impl;
import com.marui.unit.testing.entity.User;
import com.marui.unit.testing.mapper.UserMapper;
import com.marui.unit.testing.service.DataCheckService;
import com.marui.unit.testing.service.UserService;
import lombok.Data;
import org.springframework.stereotype.Service;
/**
* @ClassName UserServiceImpl
* @Desciption 用户业务逻辑实现层
* @Author MaRui
* @Date 2023/2/15 17:06
* @Version 1.0
*/
@Data
@Service
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final DataCheckService dataCheckService;
@Override
public User add(User user) {
// 用户信息校验
if (!dataCheckService.checkUser(user)) {
return null;
}
userMapper.insert(user);
return user;
}
@Override
public void delete(Long id) {
userMapper.deleteById(id);
}
@Override
public void update(User user) {
userMapper.updateById(user);
}
@Override
public User getOne(Long id) {
return userMapper.selectById(id);
}
}
package com.marui.unit.testing.service.impl;
import com.marui.unit.testing.entity.User;
import com.marui.unit.testing.service.DataCheckService;
import org.springframework.stereotype.Service;
/**
* @ClassName DataCheckServiceImpl
* @Desciption 数据校验业务逻辑层
* @Author MaRui
* @Date 2023/2/15 18:06
* @Version 1.0
*/
@Service
public class DataCheckServiceImpl implements DataCheckService {
@Override
public Boolean checkUser(User user) {
// 业务逻辑校验.....
return null;
}
}
package com.marui.unit.testing.service.impl;
import com.marui.unit.testing.entity.User;
import com.marui.unit.testing.service.DataCheckService;
import org.springframework.stereotype.Service;
/**
* @ClassName DataCheckServiceImpl
* @Desciption 数据校验业务逻辑层
* @Author MaRui
* @Date 2023/2/15 18:06
* @Version 1.0
*/
@Service
public class DataCheckServiceImpl implements DataCheckService {
@Override
public Boolean checkUser(User user) {
// 业务逻辑校验.....
return null;
}
}
Mapper 层
Mapper 层
package com.marui.unit.testing.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.marui.unit.testing.entity.User;
/**
* @ClassName UserMapper
* @Desciption 用户 mapper 层
* @Author MaRui
* @Date 2023/2/15 17:11
* @Version 1.0
*/
public interface UserMapper extends BaseMapper<User> {
}
实体类
实体类
package com.marui.unit.testing.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.io.Serializable;
/**
* @ClassName User
* @Desciption 用户信息
* @Author MaRui
* @Date 2023/2/15 16:44
* @Version 1.0
*/
@Data
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/** ID */
@TableId
private Long id;
/** 用户姓名 */
private String name;
}
3、单元测试配置
表数据配置
表数据配置文件 user_setup_01.xml
,用于单个单元测试类数据的添加。
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<user id="10000001" name="MaRui_1" />
<user id="10000002" name="MaRui_2" />
<user id="10000003" name="MaRui_3" />
</dataset>
表约束
因为是使用的 H2
内存数据库,需要将表创建 SQL
配置好,用于初始化表结构,配置在 schema-test.sql
文件中。
drop table if exists `user`;
create table `user` (
id bigint not null auto_increment,
name varchar(25),
primary key(`id`)
);
项目配置文件
单元测试项目配置文件 application-test.yml
,单元测试启动时用到的配置文件。
项目配置文件
spring:
datasource:
platform: h2
# mem 表示使用的是 H2 的内存模式;testdb 为库名;database_to_upper=false:这个参数是用来指定 H2 在创建数据库与表时,不会默认转换为大写,因为 H2 对大小写敏感,会自动将英文转换为大写。在第一次创建数据库时就必须指定此参数,否则会出现不生效的情况。
url: jdbc:h2:mem:test;MODE=MYSQL;DATABASE_TO_UPPER=false;
# 用户名默认 sa
username: sa
# 密码默认为空
password:
# 配置数据库的建表语句,建表语句需要符合 H2 的建表语句规则,在 idea 中可以通过修改 sql 语法来实现自动识别装换。
schema: classpath:db/schema-test.sql
# 初始化数据,不需要可以注释掉,需要的话在 data-test.sql 中添加 insert SQL 脚本
# data: classpath:db/data-test.sql
initialization-mode: always
# org.h2.Driver
driverClassName: org.h2.Driver
druid:
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
# 日志配置
logging:
level:
com.marui.unit.testing: debug
org.springframework: info
3、单元测试代码
MockMvc 初始化,后续其他测试类直接继承
package com.marui.unit.testing.controller;
import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.marui.unit.testing.UnitTestingApplication;
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.boot.test.mock.mockito.MockitoTestExecutionListener;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@WebAppConfiguration
@ActiveProfiles("test")
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {UnitTestingApplication.class})
@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, DbUnitTestExecutionListener.class, MockitoTestExecutionListener.class})
public abstract class AbstractTest {
@Autowired
protected WebApplicationContext webApplicationContext;
protected MockMvc mockMvc;
@Before()
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
}
UserController 单元测试代码
UserController 单元测试代码
package com.marui.unit.testing.controller;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.github.springtestdbunit.annotation.DatabaseOperation;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.marui.unit.testing.entity.User;
import com.marui.unit.testing.mapper.UserMapper;
import com.marui.unit.testing.service.DataCheckService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import static org.junit.Assert.*;
/**
* @ClassName UserControllerTest
* @Desciption
* @Author MaRui
* @Date 2023/2/15 16:46
* @Version 1.0
*/
@Slf4j
public class UserControllerTest extends AbstractTest {
@Autowired
private UserMapper userMapper;
@MockBean
private DataCheckService dataCheckService;
/**
* 用户新增:用户数据校验不通过
* @throws Exception
*/
@Test
@DatabaseSetup(value = "/dataset/user_setup_01.xml", type = DatabaseOperation.CLEAN_INSERT)
public void testAdd1() throws Exception {
// 构造数据
User user = new User();
user.setName("马锐");
String requestJson = JSON.toJSONString(user);
// Mock checkUser() 方法结果
Mockito.when(dataCheckService.checkUser(user)).thenReturn(Boolean.FALSE);
// 接口调用
MvcResult result = mockMvc
.perform(MockMvcRequestBuilders
.post("/user/add")
.content(requestJson)
.contentType(MediaType.APPLICATION_JSON)
.accept("application/json;charset=UTF-8"))
.andDo(MockMvcResultHandlers.print()).andReturn();
JSONObject resultObject = JSONObject.parseObject(result.getResponse().getContentAsString());
log.info("返回结果输出:{}", resultObject);
// 断言:比较状态码
assertEquals("500", resultObject.getString("code"));
}
/**
* 用户新增:用户数据校验通过,新增成功
* @throws Exception
*/
@Test
@DatabaseSetup(value = "/dataset/user_setup_01.xml", type = DatabaseOperation.CLEAN_INSERT)
public void testAdd2() throws Exception {
// 构造数据
String userName = "马锐";
User user = new User();
user.setName(userName);
String requestJson = JSON.toJSONString(user);
// Mock checkUser() 方法结果
Mockito.when(dataCheckService.checkUser(user)).thenReturn(Boolean.TRUE);
// 接口调用
MvcResult result = mockMvc
.perform(MockMvcRequestBuilders
.post("/user/add")
.content(requestJson)
.contentType(MediaType.APPLICATION_JSON)
.accept("application/json;charset=UTF-8"))
.andDo(MockMvcResultHandlers.print()).andReturn();
JSONObject resultObject = JSONObject.parseObject(result.getResponse().getContentAsString());
log.info("返回结果输出:{}", resultObject);
// 获取返回中的数据
String dataStr = resultObject.getString("data");
User userResult = JSON.parseObject(dataStr, User.class);
// 根据插入后返回的数据id,查询数据库中数据
User queryUser = userMapper.selectById(userResult.getId());
// 断言:比较状态码
assertEquals("000000", resultObject.getString("code"));
// 断言:数据库中存在插入的数据
assertNotNull(queryUser);
// 断言:数据库中数据与插入数据一致
assertEquals(user.getName(), queryUser.getName());
}
/**
* 用户删除:删除成功
* @throws Exception
*/
@Test
@DatabaseSetup(value = "/dataset/user_setup_01.xml", type = DatabaseOperation.CLEAN_INSERT)
public void testDelete1() throws Exception {
// 构造数据
Long userId = 10000001L;
// 接口调用
MvcResult result = mockMvc
.perform(MockMvcRequestBuilders
.delete("/user/delete?id=" + userId)
.contentType(MediaType.APPLICATION_JSON)
.accept("application/json;charset=UTF-8"))
.andDo(MockMvcResultHandlers.print()).andReturn();
JSONObject resultObject = JSONObject.parseObject(result.getResponse().getContentAsString());
log.info("返回结果输出:{}", resultObject);
// 根据 userId 查询数据是否删除
User user = userMapper.selectById(userId);
// 断言:比较状态码
assertEquals("000000", resultObject.getString("code"));
// 断言:userId 在数据库中不存在
assertNull(user);
}
/**
* 用户修改:修改成功
* @throws Exception
*/
@Test
@DatabaseSetup(value = "/dataset/user_setup_01.xml", type = DatabaseOperation.CLEAN_INSERT)
public void testUpdate1() throws Exception {
// 构造数据
Long userId = 10000002L;
String userName = "马锐";
User user = new User();
user.setId(userId);
user.setName(userName);
String requestJson = JSON.toJSONString(user);
// 接口调用
MvcResult result = mockMvc
.perform(MockMvcRequestBuilders
.patch("/user/update")
.content(requestJson)
.contentType(MediaType.APPLICATION_JSON)
.accept("application/json;charset=UTF-8"))
.andDo(MockMvcResultHandlers.print()).andReturn();
JSONObject resultObject = JSONObject.parseObject(result.getResponse().getContentAsString());
log.info("返回结果输出:{}", resultObject);
// 根据 userId 查询修改后的数据
User updatedUser = userMapper.selectById(userId);
// 断言:比较状态码
assertEquals("000000", resultObject.getString("code"));
// 断言:userId 在数据库中存在
assertNotNull(updatedUser);
// 断言:修改的数据和修改后数据库中数据一致
assertEquals(user, updatedUser);
}
/**
* 用户查询:查询成功
* @throws Exception
*/
@Test
@DatabaseSetup(value = "/dataset/user_setup_01.xml", type = DatabaseOperation.CLEAN_INSERT)
public void testGetOne1() throws Exception {
// 构造数据
Long userId = 10000003L;
User user = new User();
user.setId(userId);
user.setName("MaRui_3");
// 接口调用
MvcResult result = mockMvc
.perform(MockMvcRequestBuilders
.get("/user/getOne?id=" + userId)
.contentType(MediaType.APPLICATION_JSON)
.accept("application/json;charset=UTF-8"))
.andDo(MockMvcResultHandlers.print()).andReturn();
JSONObject resultObject = JSONObject.parseObject(result.getResponse().getContentAsString());
log.info("返回结果输出:{}", resultObject);
// 获取返回中的数据
String dataStr = resultObject.getString("data");
User userResult = JSON.parseObject(dataStr, User.class);
// 断言:比较状态码
assertEquals("000000", resultObject.getString("code"));
// 断言:修改的数据和修改后数据库中数据一致
assertEquals(user, userResult);
}
}