大将军手把手教你:Spring boot单元测试(一)中讲了spring boot开发环境的搭建
一.为何要做单元测试
二.单元测试的常用步骤
1.拿到代码之后,先把路径图画出来
2.设计测试用例,利用黑盒,白盒设计测试用例,等价类,路径覆盖,条件组合等方式设计测试用例。
3.根据测试用例进行单元测试的编码。
三.实战
1.现在spring boot插件
打开idea-Configure-plugins,搜索Spring Assistant,然后点击安装install,安装后重启IDE
2.新建项目选择Spring Assistant,server选择默认,点击next。
自定义项目信息,我这里修改了project name
点击下一步,选择项目类型,这里可以选择web,其实其他类型也可以。
这里明明已经选择了项目名称了,又让输入了一个项目名称,这里的项目是可以有多个的。
看下项目结构,你会发现src下main和test两个目录的文件夹结构基本是一致的,只是test的文件比main的文件名中多了一个test
这里main和test是一一对应的,大家在新建测试类的时候,要放在test文件夹下,报名就是功能类+test.java
repository 类中有自带的集成的方法有很多,不建议大家每个都进行单元测试,如果自己在repository中写的有自己的方法,可以写一下,如果只继承,什么都没有写的话是可以不测这个类的
如果需要测试需要怎么测呢
package org.example.repository;
import org.example.entity.Info;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
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 javax.transaction.Transactional;
import java.util.List;
@SpringBootTest ###装饰器
@RunWith(SpringRunner.class) ###一个容器
public class InfoRepositoryTest {
private @Autowired InfoRepository repository; ###通过Autowired的方式把repository引入进来
###这里before在测试前往数据库增加几条数据,类似py单元测试的setup
public @Before void dataPrepare() {
Info info = new Info();
info.setId(2L);
info.setOutput(2);
repository.save(info);
}
###这里注解是test,测试findAll 这个方法,同时注意,测试结果不要print,一定要通过assert打印出来,选择findAll右键运行,可查看到实际的结果
public @Test void findAll() {
List<Info> info = repository.findAll();
Assert.assertEquals(1, info.size());
Assert.assertNotNull(info.get(0));
Assert.assertTrue(info.get(0).getId().equals(2L));
Assert.assertTrue(info.get(0).getOutput().equals(2));
}
@Transactional
public @Test void save() {
Info info = new Info();
info.setId(1L);
info.setOutput(1);
repository.save(info);
Info r = repository.findOne(1L);
Assert.assertEquals(2, repository.findAll().size());
Assert.assertNotNull(r);
Assert.assertTrue(r.getId().equals(1L));
Assert.assertTrue(r.getOutput().equals(1));
//repository.delete(1L);
}
###结束后的处理在这里,删除了2行数据
public @After void dataRemove() {
repository.delete(2L);
}
}
如果是查询的测试,这样就可以了,如果是插入数据,比如上段代码中的save方法。
因为在单元测试这个类是没有事务控制的,相当于一句话就是一个事务,为了保证插入的正确性Transactional,这样这个方法就在一个事务里了。这里按照平常逻辑repository插入之后是要做一次删除的,对吧,但是在测试的事务里默认会对事务进行回滚,所以可以把repository.delete注释掉,不需要考虑清理数据了,如果这里不需要对事务进行回滚,可以@Rollback(false)
service :是有逻辑的,测试跟repository一样,包名保持一致,遵守命名规范就可以。在测试用例前后做好before和after的处理样例中的脚本是一个存和一个查
package org.example.service;
import org.example.entity.Info;
import org.example.repository.InfoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class InfoService {
private @Autowired InfoRepository repository;
public List<Info> findAll() {
return repository.findAll();
}
public Info save(Info info) {
return repository.save(info);
}
}
如果大家在执行测试的时候会遇到一些找不到测试类或者没有测试匹配的情况,是因为启动类检查下启动位置不太对或者不是严格按照命名规范命名的。这是可以指定启动类@SpringBootTest(classes={Application.class})
controller 测试,因为这里涉及的有逻辑,所以要先涉及测试用例,测试类的生成,定义在包名同包下就可以了。
建议大家用spring自带的http进行测试,这时要引入装饰器函数
@AutoConfigureMockMvc
@SpringBootTest
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc###需要加个装饰器
public class InfoControllerTest {
private @Autowired MockMvc mockMvc;###这里直接导入mockMvc就可以了
private @Autowired InfoRepository repository;
private @Autowired ObjectMapper mapper;
private List<List<String>> unit = new ArrayList<List<String>>(8) {
private static final long serialVersionUID = -7765488012354542450L;
###以下就是我准备的测试用例
{
add(Arrays.asList("2", "0", "4", "3"));
add(Arrays.asList("2", "1", "1", "2"));
add(Arrays.asList("1", "0", "3", "4"));
add(Arrays.asList("1", "1", "1", "1"));
add(Arrays.asList("3", "0", "3", "1"));
add(Arrays.asList("2147487630", "0", "4", ""));
add(Arrays.asList("a", "0", "4", ""));
add(Arrays.asList(null, "0", "4", ""));
} };
@Transactional
public @Test void executeTest() {
unit.forEach(u -> {
try {
if (StringUtils.hasLength(u.get(3))) {
###这里mvc提供的方法perform指定访问的url
mockMvc.perform(MockMvcRequestBuilders.get("/?inputA=" + u.get(0) + "&inputB=" + u.get(1) + "&inputX=" + u.get(2)))
.andExpect(MockMvcResultMatchers.status().isOk())###断言访问是成功的
.andExpect(MockMvcResultMatchers.content().string(u.get(3)))###content的返回值是多少
.andDo(MockMvcResultHandlers.print());###这里打印不是必须
return;
}
mockMvc.perform(MockMvcRequestBuilders.get("/?inputA=" + u.get(0) + "&inputB=" + u.get(1) + "&inputX=" + u.get(2)))
.andExpect(MockMvcResultMatchers.status().is4xxClientError())###因为代码逻辑是参数错误返回的是4XX,所以这里用了4XX做了断言
.andDo(MockMvcResultHandlers.print());
} catch (Exception e) {
e.printStackTrace();
}
});
注意:如果一个类的方法比较多,这样单个执行效率会很低,可以在class上右键执行类里所有的方法。
如果项目比较大,每执行一个用例都要把起spring环境,速度就会比较慢,把@SpringBootTest换成
@WebMvcTest(controllers = { InfoController.class }, secure = false),把想测试的contrullers列进去。secure为false则不测登录,因为controllers测试的时候只注入了controllers,没有注入service,所以在调用的时候会报错,这时候可以通过装饰器 @MockBean InfoService service;模拟services的返回,在下边测试的时候
BDDMockito.given(service.save(info)).willReturn(new Info());制定在调用services.save的时候会直接返回,这样就可以单独去测试某一个controller
如果某些测试是针对不同的测试文件,这里可以指定某个配置文件。
@ActiveProfiles("pro")
如果被测试数据比较复杂,也不用每个字段都做断言,可以用jsonPath进行断言。
像我们公司的代码要求代码的单元测试覆盖率达到百分之80,可以通过sonar进行扫描,sonar可以把你没有覆盖的行,类没有覆盖到,以及路径没有覆盖。