最近在网易云课堂上看一些视频,给大家推荐一个讲Spring Boot的视频https://study.163.com/course/courseMain.htm?courseId=1005213034,老师讲的很不错。在学习的时候我也会做一些笔记,方便日后巩固。
对这个系列感兴趣的可以看我之前写的博客:
使用spring-test和junit进行单元测试
Assert — JUnit的断言
- 判断某条件是否为真 Assert.assertTrue(条件表达式);
- 判断某条件是否为假 Assert.assertFalse(条件表达式);
- 判断两个变量值是否相同 Assert.assertEquals(var1, var2);
- 判断两个变量值是否不相同 Assert.assertNotEquals(var1, var2);
- 判断两个数组是否相同 Assert.assertArrayEquals(数组1, 数组2);
- 直接测试失败Assert.fail() Assert.fail(message)
如果判断两个变量是否相同,建议使用Assert.assertEquals,因为
Assert.assertTrue不支持非基础类型
Assert vs. assert
- Assert是JUnit的断言类, 全名是org.junit.Assert
- Assert提供了很多静态方法,例如assertTrue, assertFalse, assertNotNull, assertNull, assertEquals, assertNotEquals等
- assert是java关键字,使用方法有两种,表达式为false时,jvm会退出;
- assert 表达式; assert 表达式 : “表达式不成立后的提示信息”;
- assert关键字内表达式是否被检查成立依赖jvm的参数,默认是关闭的
概念
要进行测试,首先要理解三个概念
- 被测模块:需要被测试的模块
- 驱动模块:调用被测模块的模块
- 桩模块:驱动模块需要对传入的数据做一些处理再传给下级模块,若需要对这些被处理的数据进行处理,就得使用桩模块。
桩模块的使用场景:
- 替代尚未开发完毕的子模块
- 替代对环境依赖较大的子模块(例如数据访问层)
有一个框架可以帮助我们运用桩模块,它就是Mockito
,如果要使用它,需要在.pom文件里加入:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
最后还有一个概念需要了解一下,这就是TDD
TDD
- 先写测试用例,后写实现代码
- 重构现有代码时特别好用
用mockito做桩模块来测试业务逻辑层
这一节会用实际的代码来看一下测试用例,在test的包下,我们可以看到项目自带了一个最简单的测试用例:
package cn.luxiaofen.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestApplicationTests {
@Test
public void contextLoads() {
}
}
在上面这个测试用例中,虽然contextLoads()方法体中什么也没有,但是如果spring boot的配置不正确的话,这个方法是不能运行成功的。接下来我们新建一个测试类,来看一些更复杂的测试用例:
@RunWith(SpringRunner.class)
@SpringBootTest
public class TvSeriesServiceTest {
@Autowired
TvSeriesDao tvSeriesDao;
@Autowired
TvSeriesService tvSeriesService;
@Test
public void getAllWithoutMockit() {
List<TvSeries> res = tvSeriesService.getAllTvSeries();
//这里的测试结果依赖连接数据库内的记录,很难写一个判断是否成功的条件,甚至无法执行
//下面的testGetAll()方法,使用了mock出来的dao作为桩模块,避免了这一情形
Assert.assertTrue(res.size()>0);
}
}
上面的这个测试方法依赖于数据库的情况,数据库里的数据不是一直不变的,所以很难去制定一个测试通过的标准,断言就会很难写。在一个团队中第一个人写的这个测试用例通过后,也许第二个人去测试就不能通过了,也不能知道是什么原因,这时候就起不到测试用例原来的效果了。
要解决这种情况,就要用到之前提到的桩模块,把DAO层作为一个Mock Bean。
//测试类的成员变量
@MockBean
TvSeriesDao tvSeriesDao;
我们可以自行设置这个mock bean的内容,从而使的测试前提不受到真实情况的影响。
@Test
public void testGetAll() {
//新建一个list来充当数据库里的记录
List<TvSeries> list = new ArrayList<>();
TvSeries tvSeries = new TvSeries();
String name = "LoveManchester";
tvSeries.setName(name);
list.add(tvSeries);
//下面这句话表示当调用getAllTvSeries()方法时,返回上述的list,这时测试结果就与数据库内的情况无关了
Mockito.when(tvSeriesDao.getAll()).thenReturn(list);
List<TvSeries> res = tvSeriesService.getAllTvSeries();
//获取到的结果应和最初的list相同
Assert.assertEquals(res.size(), list.size());
Assert.assertEquals(name, res.get(0).getName());
}
我们可以在service层中再增加一些方法用以测试传进方法中的参数:
public TvSeries updateTvSeries(TvSeries tvSeries) {
if (log.isTraceEnabled()) {
log.trace("update tvSeries service start");
}
tvSeriesDao.update(tvSeries);
return tvSeries;
}
对应的DAO层中的方法为:
public int update(TvSeries tvSeries);
下面的测试用例用来检测传进方法的参数:
@Test
public void testUpdateTvSeries() {
String newName = "Person Of Interest";
//BitSet用来测试桩模块是否被执行
BitSet mockExecute = new BitSet();
//doAnswer用来判断执行的方法和方法的参数,doAnswer一般和when配合使用,当条件满足时,执行对应的Answer的answer方法,
//如果answer方法抛出异常,那么测试不通过。这个方法意味着当执行dao层的update方法时会去检验该方法的参数,这个参数应该和newName相同
Mockito.doAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
//获取update()方法的参数
Object[] args = invocationOnMock.getArguments();
TvSeries arg = (TvSeries) args[0];
Assert.assertEquals(newName,arg.getName());
mockExecute.set(0);//方法正确执行时
return null;
}
}).when(tvSeriesDao).update(any(TvSeries.class));
TvSeries tvSeries = new TvSeries();
tvSeries.setName(newName);
tvSeriesService.updateTvSeries(tvSeries);
//方法正确执行时0位get的值为true
Assert.assertTrue(mockExecute.get(0));
}
用mockMvc来测试web控制层和业务逻辑层
之前的内容都是在测试service层,那么有没有办法来测试web控制层呢,也是有办法的,我们可以用MockMVC
来实现。
- 和TvSeriesServiceTests相比,这个测试类上多了@AutoConfigureMockMvc注解,这是初始化一个mvc环境用于测试
@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class AppTests { //TODO: 一些测试方法
- 写一个测试获取全部节目列表的方法
@Test public void testGetAll() throws Exception{ List<TvSeries> list = new ArrayList<>(); TvSeries tvSeries = new TvSeries(); tvSeries.setName("POI"); list.add(tvSeries); //这些桩模块的加载可参考TvSeriesServiceTest中的例子 Mockito.when(tvSeriesDao.getAll()).thenReturn(list); //下面这个是相当于在启动项目后,执行 GET /tvseries,被测模块是web控制层,因为web控制层会调用业务逻辑层, // 所以业务逻辑层也会被测试 //业务逻辑层调用了被mock出来的数据访问层桩模块。 //如果想仅仅测试web控制层,(例如业务逻辑层尚未编码完毕),可以mock一个业务逻辑层的桩模块 mockMvc.perform(MockMvcRequestBuilders.get("/tvseries")).andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string(Matchers.containsString("POI"))); //上面这几句和字面意思一致,期望状态是200,返回值包含POI三个字面,桩模块返回的一个电视剧名字是POI,如果测试正确是包含这三个字母的。 }
下面再看一个调用POST方法的例子,这个测试用例用来测试添加一个tvseries的方法:
@Test
public void testAddSeries() throws Exception {
BitSet bitSet = new BitSet(1);
bitSet.set(0,false);
//下面的两个doAnswer方法用来验证插入到数据中的参数是否和我们传入进去的相等
//bitSet验证桩模块是否被执行过
Mockito.doAnswer((Answer<Object>) invocation -> {
Object[] args = invocation.getArguments();
TvSeries tvSeries = (TvSeries) args[0];
Assert.assertEquals(tvSeries.getName(),"可爱的湖南人");
tvSeries.setId(118);
bitSet.set(0,true);
return null;
}).when(tvSeriesDao).insert(Mockito.any(TvSeries.class));
Mockito.doAnswer((Answer<Object>) invocation -> {
Object[] args = invocation.getArguments();
TvCharacter tvCharacter = (TvCharacter) args[0];
//应该是json中传递过来的剧中角色名字
Assert.assertEquals(tvCharacter.getName(),"CaiYishu");
Assert.assertEquals(118, tvCharacter.getTvSeriesId());
bitSet.set(0,true);
return null;
}).when(tvCharacterDao).insert(Mockito.any(TvCharacter.class));
String jsonData = "{\"name\":\"可爱的湖南人\",\"seasonCount\":1,\"originalRelease\":\"1996-01-18\"," +
"\"tvCharacters\":[{\"id\":1,\"name\":\"CaiYishu\"}]}";
//模拟一个MVC环境,用POST方法传入一个JSON消息,将结果打印出来并验证状态是否为200
this.mockMvc.perform(MockMvcRequestBuilders.post("/tvseries").contentType(MediaType.APPLICATION_JSON).
content(jsonData)).andDo
(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk());
Assert.assertTrue(bitSet.get(0));
}
写这个单测之前需要将Controller、service、和dao中的方法同步更新。下面再来看一个测试上传文件的方法:
- 首先需要在controller对之前的上传文件方法做一些修改,这里我们把文件上传的路径设置成了类的field:
//通过@Value将外部的值动态注入到Bean中 @Value("${SpringBootTest.uploadFolder:target/files}") String uploadFolder;
@PostMapping(value = "/{id}/photos",consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public Map<String, String> addPhoto(@PathVariable int id, @RequestParam("photo")MultipartFile imgFile) throws Exception { if (log.isTraceEnabled()) { log.trace("接受到文件"+id+"收到文件:"+imgFile.getOriginalFilename()); } //保存文件 File folder = new File(uploadFolder); if(!folder.exists()) { folder.mkdirs(); } String fileName = imgFile.getOriginalFilename(); assert fileName != null; if (fileName.endsWith(".jpg")) { FileOutputStream fileOutputStream = new FileOutputStream(new File(folder,fileName)); IOUtils.copy(imgFile.getInputStream(),fileOutputStream); fileOutputStream.close(); Map<String, String> result = new HashMap<>(); result.put("photo", fileName); return result; }else { throw new RuntimeException("不支持的格式,仅支持jpg格式"); } }
- 需要在test/resource文件夹下放一张测试上传的图片,并命名为testfileupload.jpg
@Test public void testFileUpload() throws Exception{ String fileFolder = "/target/files"; File folder = new File(fileFolder); if (!folder.exists()) { folder.mkdirs(); } // 下面这句可以设置bean里面通过@Value获得的数据 ReflectionTestUtils.setField(tvSeriesController,"uploadFolder",folder. getAbsolutePath()); //用来获取资源 InputStream inputStream = getClass().getResourceAsStream("/testfileupload.jpg"); if(inputStream == null) { throw new RuntimeException("需要先在src/test/resources目录下放置一张jpg文件,名为testfileupload.jpg然后运行测试"); } //模拟一个文件上传的请求 MockMultipartFile imgFile = new MockMultipartFile("photo","/testfileupload.jpg","image/jpeg",IOUtils.toByteArray(inputStream) ); ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.multipart("/tvseries/1/photos") .file(imgFile)).andExpect(MockMvcResultMatchers.status().isOk()); //解析返回的JSON ObjectMapper objectMapper = new ObjectMapper();//Jackson框架 Map<String,Object> map = objectMapper.readValue(resultActions.andReturn().getResponse().getContentAsString(),new TypeReference<Map<String,Object>>(){}); String fileName = (String) map.get("photo"); File f2 = new File(folder,fileName); //返回的文件名,应该已经保存在fileFolder文件夹下 Assert.assertTrue(f2.exists()); }
Case Study
在这次写单测的时候发现了一个问题,一直报一个错误java:找不到符号
。反复检查并未发现错误,找不到的符号是一个普通的insert()方法。在网上查阅资料后,发现是因为在改动tvseriesDao文件后没有编译,使用右侧maven工具对文件单独编译即可解决。
网友评论