前言
《论语》中孔子有言:“工欲善其事,必先利其器。”
今年7月,作者希望迎接更大的挑战,从高德地图数据转岗到共享出行后,接手并维护了几个Java后端项目。在熟悉业务和代码的过程中,快速地对原有项目进行单元测试用例的补充,使其单元测试覆盖率达到70%+甚至于100%。有同事问我:“你写单元测试为什么这么快?”我微微一笑:“工欲善其事,必先利其器。而我快速编写Java单元测试用例的技巧就是——JSON序列化。”
是的,做任何事情,都要讲究方式方法;只要方式方法对了,就会事半功倍。这里,作者系统性地总结了JSON序列化在编写Java单元测试用例中的使用技巧,希望能够让大家“读有所得、得有所思、思有所获”。
1. 冗长的单元测试代码
在编写单元测试用例的过程中,经常会出现以下冗长的单元测试代码。
1.1. 冗长的数据模拟代码
1.1.1. 模拟类属性值
在模拟类属性值时,会遇到以下的冗长代码:
Map<Long, String> languageMap = new HashMap<>(MapHelper.DEFAULT);
languageMap.put(1L, "Java");
languageMap.put(2L, "C++");
languageMap.put(3L, "Python");
languageMap.put(4L, "JavaScript");
... // 约几十行
Whitebox.setInternalState(developmentService, "languageMap", languageMap);
1.1.2. 模拟方法参数值
在模拟方法参数值时,会遇到以下的冗长代码:
List<UserCreateVO> userCreateList = new ArrayList<>();
UserCreateVO userCreate0 = new UserCreateVO();
userCreate0.setName("Changyi");
userCreate0.setTitle("Java Developer");
... // 约几十行
userCreateList.add(userCreate0);
UserCreateVO userCreate1 = new UserCreateVO();
userCreate1.setName("Tester");
userCreate1.setTitle("Java Tester");
... // 约几十行
userCreateList.add(userCreate1);
... // 约几十条
userService.batchCreate(userCreateList);
1.1.3. 模拟方法返回值
在模拟方法返回值时,会遇到以下的冗长代码:
Long companyId = 1L;
List<UserDO> userList = new ArrayList<>();
UserDO user0 = new UserDO();
user0.setId(1L);
user0.setName("Changyi");
user0.setTitle("Java Developer");
... // 约几十行
userList.add(user0);
UserDO user1 = new UserDO();
user1.setId(2L);
user1.setName("Tester");
user1.setTitle("Java Tester");
... // 约几十行
userList.add(user1);
... // 约几十条
Mockito.doReturn(userList).when(userDAO).queryByCompanyId(companyId);
1.2. 冗长的数据验证代码
1.2.1. 验证方法返回值
在验证方法返回值时,会遇到以下的冗长代码:
Long companyId = 1L;
List<UserVO> userList = userService.queryByCompanyId(companyId);
UserVO user0 = userList.get(0);
Assert.assertEquals("name不一致", "Changyi", user0.getName());
Assert.assertEquals("title不一致", "Java Developer", user0.getTitle());
... // 约几十行
UserVO user1 = userList.get(1);
Assert.assertEquals("name不一致", "Tester", user1.getName());
Assert.assertEquals("title不一致", "Java Tester", user1.getTitle());
... // 约几十行
... // 约几十条
1.2.2. 验证方法参数值
在验证方法参数值时,会遇到以下的冗长代码:
ArgumentCaptor<List<UserDO>> userCreateListCaptor = CastUtils.cast(ArgumentCaptor.forClass(List.class));
Mockito.verify(userDAO).batchCreate(userCreateListCaptor.capture());
List<UserDO> userCreateList = userCreateListCaptor.getValue();
UserDO userCreate0 = userCreateList.get(0);
Assert.assertEquals("name不一致", "Changyi", userCreate0.getName());
Assert.assertEquals("title不一致", "Java Developer", userCreate0.getTitle());
... // 约几十行
UserDO userCreate1 = userCreateList.get(1);
Assert.assertEquals("name不一致", "Tester", userCreate1.getName());
Assert.assertEquals("title不一致", "Java Tester", userCreate1.getTitle());
... // 约几十行
... // 约几十条
2. 采用JSON序列化简化
常言道:“眼见为实,耳听为虚。”下面,就通过JSON序列化来简化上面的单元测试用例代码,让大家先睹为快。
2.1. 简化数据模拟代码
对于数据模拟,首先需要先加载JSON资源文件为字符串,然后通过JSON反序列化字符串为数据对象,最后用于模拟类属性值、方法参数值和方法返回值。这样,就精简了原来冗长的赋值语句。
2.1.1. 模拟类属性值
利用JSON反序列化,简化模拟类属性值代码如下:
String text = ResourceHelper.getResourceAsString(getClass(), path + "languageMap.json");
Map<Long, String> languageMap = JSON.parseObject(text, new TypeReference<Map<Long, String>>() {});
Whitebox.setInternalState(mobilePhoneService, "languageMap", languageMap);
其中,JSON资源文件languageMap.json的内容如下:
{1:"Java",2:"C++",3:"Python",4:"JavaScript"...}
2.1.2. 模拟方法参数值
利用JSON反序列化,简化模拟方法参数值代码如下:
String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateList.json");
List<UserCreateVO> userCreateList = JSON.parseArray(text, UserCreateVO.class);
userService.batchCreate(userCreateList);
其中,JSON资源文件userCreateList.json的内容如下:
[{"name":"Changyi","title":"Java Developer"...},{"name":"Tester","title":"Java Tester"...},...]
2.1.3. 模拟方法返回值
利用JSON反序列化,简化模拟方法返回值代码如下:
Long companyId = 1L;
String text = ResourceHelper.getResourceAsString(getClass(), path + "userList.json");
List<UserDO> userList = JSON.parseArray(text, UserDO.class);
Mockito.doReturn(userList).when(userDAO).queryByCompanyId(companyId);
其中,JSON资源文件userList.json的内容如下:
[{"id":1,"name":"Changyi","title":"Java Developer"...},{"id":2,"name":"Tester","title":"Java Tester"...},...]
2.2. 简化数据验证代码
对于数据验证,首先需要先加载JSON资源文件为字符串,然后通过JSON序列化数据对象为字符串,最后验证两字符串是否一致。这样,就精简了原来冗长的验证语句。
2.2.1. 验证方法返回值
利用JSON序列化,简化验证方法返回值代码如下:
Long companyId = 1L;
List<UserVO> userList = userService.queryByCompanyId(companyId);
String text = ResourceHelper.getResourceAsString(getClass(), path + "userList.json");
Assert.assertEquals("用户列表不一致", text, JSON.toJSONString(userList));
其中,JSON资源文件userList.json的内容如下:
[{"name":"Changyi","title":"Java Developer"...},{"name":"Tester","title":"Java Tester"...},...]
2.2.2. 验证方法参数值
利用JSON序列化,简化验证方法参数值代码如下:
ArgumentCaptor<List<UserDO>> userCreateListCaptor = CastUtils.cast(ArgumentCaptor.forClass(List.class));
Mockito.verify(userDAO).batchCreate(userCreateListCaptor.capture());
String text = ResourceHelper.getResourceAsString(getClass(), path + "userCreateList.json");
Assert.assertEquals("用户创建列表不一致", text, JSON.toJSONString(userCreateListCaptor.getValue()));
其中,JSON资源文件userCreateList.json的内容如下:
[{"name":"Changyi","title":"Java Developer"...},{"name":"Tester","title":"Java Tester"...},...]
3. 测试用例及资源命名
俗话说:“没有规矩,不成方圆。”所以,为了更好地利用JSON序列化技巧,首先对测试用例和资源文件进行规范化命名。
3.1. 测试类命名
按照行业惯例,测试类的命名应以被测试类名开头并以Test结尾。比如:UserService(用户服务类)的测试类需要命名为UserServiceTest(用户服务测试类)。
单元测试类应该放在被测试类的同一工程的"src/test/java"目录下,并且要放在被测试类的同一包下。注意,单元测试类不允许写在业务代码目录下,否则在编译时没法过滤这些测试用例。
3.2. 测试方法命名
按照行业规范,测试方法命名应以test开头并以被测试方法结尾。比如:batchCreate(批量创建)的测试方法需要命名为testBatchCreate(测试:批量创建),queryByCompanyId(根据公司标识查询)的测试方法需要命名为testQueryByCompanyId(测试:根据公司标识查询)。
当一个方法对应多个测试用例时,就需要创建多个测试方法,原有测试方法命名已经不能满足需求了。有人建议在原有的测试方法命名的基础上,添加123等序号表示不同的用例。比如:testBatchCreate1(测试:批量创建1)、testBatchCreate2(测试:批量创建2)……但是,这种方法不能明确每个单元测试的用意。
这里,作者建议在原有的测试方法命名的基础上,添加”With+条件“来表达不同的测试用例方法。
1.按照结果命名:
-
- testBatchCreateWithSuccess(测试:批量创建-成功);
- testBatchCreateWithFailure(测试:批量创建-失败);
- testBatchCreateWithException(测试:批量创建-异常);
2.按照参数命名:
-
- testBatchCreateWithListNull(测试:批量创建-列表为NULL);
- testBatchCreateWithListEmpty(测试:批量创建-列表为空);
- testBatchCreateWithListNotEmpty(测试:批量创建-列表不为空);
3.按照意图命名:
-
- testBatchCreateWithNormal(测试:批量创建-正常);
- testBatchCreateWithGray(测试:批量创建-灰度);
- testBatchCreateWithException(测试:批量创建-异常);
当然,还有形成其它的测试方法命名方式,也可以把不同的测试方法命名方式混用,只要能清楚地表达出这个测试用例的涵义即可。
3.3. 测试类资源目录命名
这里,作者建议的资源目录命名方式为——以test开头且以被测试类名结尾。比如:UserService(用户服务类)的测试资源目录可以命名为testUserService。
那么,这个资源目录应该放在哪儿了?作者提供了2个选择: