1 为什么需要单元测试
在平时的开发当中,一个项目往往包含了大量的方法,可能有成千上万个。如何去保证这些方法产生的结果是我们想要的呢?
传统解决方案:Postman 发报文,System.out打印debug日志,或者眼睛看返回报文
1、眼睛看结果是否正确,瞅瞎不说,也太不智能.我们是高智商程序员,能让代码解决的事情,绝不能靠人工去解决.
2、postman 只能对controller进行测试。controller要正确,前提是service,dao都正确。发现问题太晚,解决成本高。
3、对于一些交易系统,由于交易主键的存在,每次都要更改参数后,再进行测试,效率太低。
4、无法对内部的函数功能做测试
5、postman的测试案例与项目工程不再一起,这些案例只能自己一个人用,无法团队共享
这时,就轮到单元测试闪亮出场了。
测试代码和工程代码在同一工程文件中,便于维护和传承。
使用断言自动检测结果
测试粒度小,可以小到每个函数
测试模块间相互依赖小。开发完一个模块,就可以测试一个模块。妈妈再也不用担心我犯大错了。
2 如何做单元测试
2.1 单元测试框架基础
业界单元测试一般采用基于JUNIT和MockMvc 框架进行, 其中JUNIT 是通用测试框架,主要进行Dao层和Service层测试。MockMvc主要进行Controller层测试.
2.1.1 Junit基本概念
Junit的理解其实很简单,用起来也很方便。关键在于理解几个注解。
- @Test:将一个普通方法修饰成一个测试方法,以前我们运行程序,需要main方法,而现在,可以直接运行一个被@Test修饰的方法。
- @Test(excepted=xx.class): xx.class表示异常类,表示测试的方法抛出此异常时,认为是正常的测试通过的
- @Test(timeout=毫秒数) :测试方法执行时间是否符合预期
- @BeforeClass: 会在所有的Class方法执行前被执行,static方法
- @AfterClass:会在所有的Class方法执行之后进行执行,static方法。通常用于清理工作。
- @Before:会在每一个@Test测试方法被运行前执行一次
- @After:会在每一个@Test测试方法运行后被执行一次。通常用于资源关闭。
- @Ignore:所修饰的测试方法会被测试运行器忽略
- @RunWith:可以更改测试运行器org.junit.runner.Runner。
总结一下,运行顺序为 BeforeClass---->Before----->Test----->Ater----->AfterClass
2.2 Springboot框架单元测试实战
2.2.1 Maven 引包
pom.xml文件中添加spring-boot-starter-test 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
该依赖为springboot 的stater,会引入一系列相关包。
2.2.2 新建测试源码目录
- test/java 存放test *.java文件
- test/resourse 存放yml配置文件
新建好目录后,需要在project structrue中Mark这两个资源目录,否则工程会无法正常识别加载这些文件。
2.2.3 编写单元测试案例
建议:为了让工程目录友好,可以根据不同的服务,新建不同的pakage。Dao、Service和Controller各新建一个*.java文件。
2.2.3.1 编写测试基类
package com.qinghaihu.springbootmybatis;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
@SpringBootTest
@Transactional
@Rollback
@RunWith(SpringRunner.class)
public class SpringbootUnitTest {
}
解释
1、@SpringbootTest @RunWith(SpringJUnit4ClassRunner.class)
让测试在Spring容器环境下执行测试
2、@Transactional 开启事务,支持工程代码中配置了的注解式事务
3、@Rollback 设置测试后回滚,默认属性为true,即回滚。保证测试完后,自测数据不污染数据库,从而保证测试案例可以重复执行。
2.2.3.2 编写三层模型测试代码
Springboot项目采用经典三层模型搭建,即dao,service,controller。开发顺序通常是这样,测试顺序也是一般按照这个顺序进行。
从单元测试实践经验来说,dao层单元测试通常不会强制要求,而service和controller则必须进行。
2.2.3.2.1 Service层 测试
package com.qinghaihu.springbootmybatis.user;
import com.qinghaihu.springbootmybatis.SpringbootUnitTest;
import com.qinghaihu.springbootmybatis.dao.SysUserDao;
import com.qinghaihu.springbootmybatis.entity.SysUser;
import com.qinghaihu.springbootmybatis.service.SysUserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.*;
/**
* @ClassName UserServiceTest
* @Description TODO
* @Author:Zhang Lianzhong
* @Date 2020/4/11 5:12 下午
* @Version 1.0
**/
public class UserServiceTest extends SpringbootUnitTest {
@Autowired
SysUserService sysUserService;
@Autowired
SysUserDao sysUserDao;
@Test
public void testInsert(){
SysUser sysUser = new SysUser();
String id = "5";
sysUser.setId(id);
sysUser.setUsername("qinghaihudream");
sysUser.setPassword("88888887");
sysUser.setCreatetime(new Date());
sysUser.setUserdesc("奔跑吧程序猿");
sysUser.setUserimg("/user/images/qinghaihu.jpg");
sysUser.setUserstate(-1);
boolean insertResult = sysUserService.insertUserInfo(sysUser);
assertTrue(insertResult);
SysUser queryedUser = sysUserDao.selectByPrimaryKey(id);
assertEquals(queryedUser.getUsername(),sysUser.getUsername());
}
@Test
public void testSelect(){
SysUser sysUser = new SysUser();
String id = "5";
sysUser.setId(id);
sysUser.setUsername("qinghaihudream");
sysUser.setPassword("88888887");
sysUser.setCreatetime(new Date());
sysUser.setUserdesc("奔跑吧程序猿");
sysUser.setUserimg("/user/images/qinghaihu.jpg");
sysUser.setUserstate(-1);
int insertResult = sysUserDao.insert(sysUser);
assertEquals(insertResult,1);
SysUser queryedUser = sysUserService.selectById(id);
assertEquals(queryedUser.getUsername(),sysUser.getUsername());
}
@Test
public void testUpdate() {
//准备测试数据
String id = "4";
String orgUserDesc = "你好程序猿";
String updateUserDesc = "奔跑吧程序猿";
SysUser orgSysUser = new SysUser();
orgSysUser.setId(id);
orgSysUser.setUsername("qinghaihu");
orgSysUser.setPassword("88889998");
orgSysUser.setCreatetime(new Date());
orgSysUser.setUserdesc(orgUserDesc);
orgSysUser.setUserimg("/user/images/qinghaihu.jpg");
orgSysUser.setUserstate(-1);
int insertCount = sysUserDao.insert(orgSysUser);
assertEquals(insertCount,1);
SysUser updateSysUser = new SysUser();
updateSysUser.setId(id);
updateSysUser.setUsername("qinghaihu");
updateSysUser.setPassword("88889998");
updateSysUser.setCreatetime(new Date());
updateSysUser.setUserdesc(updateUserDesc);
updateSysUser.setUserimg("/user/images/qinghaihu.jpg");
updateSysUser.setUserstate(-1);
boolean updateResult = sysUserService.updateUserInfo(updateSysUser);
assertTrue(updateResult);
//检查数据是否更新
SysUser queryedUser = sysUserService.selectById(id);
assertEquals(queryedUser.getUserdesc(),updateUserDesc);
}
@Test
public void testdelete() {
SysUser sysUser = new SysUser();
String id = "5";
sysUser.setId(id);
sysUser.setUsername("qinghaihudream");
sysUser.setPassword("88888887");
sysUser.setCreatetime(new Date());
sysUser.setUserdesc("奔跑吧程序猿");
sysUser.setUserimg("/user/images/qinghaihu.jpg");
sysUser.setUserstate(-1);
boolean insertResult = sysUserService.insertUserInfo(sysUser);
assertTrue(insertResult);
boolean deleteResult = sysUserService.deleteById(id);
assertTrue(deleteResult);
//检查数据是否删除
SysUser queryedUser = sysUserService.selectById(id);
assertNull(queryedUser);
}
}
2.2.3.2.2 Controller层测试
package com.qinghaihu.springbootmybatis.user;
import com.qinghaihu.springbootmybatis.SpringbootUnitTest;
import com.qinghaihu.springbootmybatis.dao.SysUserDao;
import com.qinghaihu.springbootmybatis.entity.SysUser;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import java.util.Date;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
/**
* @ClassName UserControllerTest
* @Description TODO
* @Author:Zhang Lianzhong
* @Date 2020/4/11 6:13 下午
* @Version 1.0
**/
public class UserControllerTest extends SpringbootUnitTest {
@Autowired
SysUserDao sysUserDao;
@Autowired
protected WebApplicationContext wac;
private MockMvc mvc;
@Before
public void setUp() throws Exception {
mvc = MockMvcBuilders.webAppContextSetup(wac).build(); //初始化MockMvc对象
}
@Test
public void testQuery() throws Exception {
//准备测试数据
String id = "4";
String userName = "qinghaihu";
SysUser orgSysUser = new SysUser();
orgSysUser.setId(id);
orgSysUser.setUsername(userName);
orgSysUser.setPassword("88889998");
orgSysUser.setCreatetime(new Date());
orgSysUser.setUserdesc("奔跑吧程序猿");
orgSysUser.setUserimg("/user/images/qinghaihu.jpg");
orgSysUser.setUserstate(-1);
int insertCount = sysUserDao.insert(orgSysUser);
assertEquals(insertCount,1);
RequestBuilder request = null;
/*
* 1、MockMvcRequestBuilders.post("XXX")构造一个post请求。
* 2、mockMvc.perform执行一个请求。
* 3、ResultActions.param添加请求传值
* 4、ResultActions.accept(MediaType.TEXT_HTML_VALUE)设置返回类型
* 5、ResultActions.andExpect添加执行完成后的断言。
* 6、ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情
* 比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息。
* 7、ResultActions.andReturn表示执行完成后返回相应的结果。
*/
//post提交一个people
request = post("/SysUser/query")
.param("id",id);
String json = mvc.perform(request)
.andExpect(status().isOk())
.andExpect(jsonPath("$.username",is(userName)))
.andDo(print())
.andReturn().getResponse().getContentAsString();
}
}
通过@Autowired WebApplicationContext wac:注入web环境的ApplicationContext容器
在@Before修饰的方法中通过MockMvcBuilders.webAppContextSetup(wac).build()创建一个MockMvc 进行测试。