测试驱动开发实践4————testSave之新增文档分类
【内容指引】
1.确定“新增文档分类”的流程及所需的参数
2.根据业务规则设计测试用例
3.为测试用例赋值并驱动开发
一、确定“新增文档分类”的流程及所需的参数
假定本项目由五部分组成:客户端、Zuul微服务网关、“项目管理”微服务、“团队管理”微服务和“文档管理”微服务。微服务网关是客户端和微服务之间的桥梁。客户端和微服务之间参数的传递模型如下:

1.在客户端Post提交Form表单,需要提供“项目”(projectId)、“分类名称”(name)、“排序”(sequence)和“操作者”(operator)这四个参数,其中“分类名称”和“排序”通过输入控件提供参数,“项目”通过下拉框控件、隐藏控件或session提供参数,“操作者”通过Session或cookie提供参数;
2.请求先经过Zuul微服务网关,网关调用“项目管理”微服务,对projectId参数的有效性进行校验,然后通过“团队管理”微服务对“操作者”的身份及该操作者对此项目添加文档分类的权限进行判断,如拥有权限,则将请求转发到“文档管理”微服务。所以在“文档管理”微服务中不需要再次对“项目”和“操作者”做校验;
3.在“文档管理”微服务中需要对“分类名称”和“排序”做输入校验,并且对“分类名称”在该项目中唯一性做逻辑校验。
综上,在“文档管理”微服务中需要接收四个参数:projectId,operator,name和sequence。
二、根据业务规则设计测试用例
设计测试用例常用技巧
等价类划分法:将测试的范围划分成几个互不相交的子集,它们的并集是全集。从每个子集选出若干个有代表性的值作为测试用例;
边界值分析法:针对各种边界情况设计测试用例。选出的测试用例,应选取正好等于、刚刚大于、刚刚小于边界的值;
错误推测法:根据经验或直觉推测程序中可能存在的各种错误,从而有针对性编写检查这些错误的测试用例的方法;
判定表法:又称为策略表,基于策略表的测试,是功能测试中最严密的测试方法。该方法适合于逻辑判断复杂的场景,通过穷举条件获得结果,对结果在进行优化合并,会得到一个判断清晰的策略表;
正交实验法:在各因素互相独立的情况下,设计出一种特殊的表格,找出能以少数替代全面的测试用例。
最佳实践
1.列出所有可能输入的参数项,对于无须校验的参数,在其它测试用例中直接赋正确参数值即可,无须设计测试用例;
2.针对每个需要校验的参数各个击破,先考虑输入校验,再考虑逻辑校验;
2.1 输入校验
首选“等价类划分法”设计测试用例,辅以“边界值分析法“。
2.1.1 合法等价类先设计合法中间值,然后设计合法边界值(Min,Min+,Max,Max-);
2.1.2 非法等价类中也可使用非法边界值(Min-,Max+)、空值和其它数据类型的参数值设计测试用例;
2.2 逻辑校验
2.2.1 考虑是否需要数据唯一性的判断;
2.2.2 如果逻辑复杂,可用“判定表法”设计测试用例;
3.如有必要,在上述测试用例基础上根据经验使用“错误推测法”和“正交表法”设计测试用例。
可借助XMind进行陈列:

最终我们设计出如下测试用例:
用例1:全部参数使用合法中间值
ProjectId=1L;
name="测试新增文档分类一";
sequence="10";
operator="1L";
用例2:name采用合法边界值Min:name="测";
(其它参数沿用用例1的合法中间值)
用例3:name采用合法边界值Min+:name="测试";
用例4:name采用合法边界值Max:name="测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试";
用例5:name采用合法边界值Max:name="测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测";
用例6:name采用非法等价类:空值;
用例7:name采用非法边界值Max+:name="测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测超长";
用例8:name同项目下唯一性逻辑校验:name=“文档分类一”(采用SetUp()中相同的值);
用例9:sequence采用合法边界值Min:sequence=1;
用例10:sequence采用合法边界值Min+:sequence=2;
用例11:sequence采用合法边界值Max:sequence=Integer.MAX_VALUE;
用例12:sequence采用合法边界值Max-:sequence=Integer.MAX_VALUE-1;
用例13:sequence采用非法等价类:空值;
用例14:sequence采用非法边界值Min-:sequence=0;
用例15:sequence采用非法边界值:sequence=-1;
用例16:sequence采用非法边界值Max+:sequence=Integer.MAX_VALUE+1;
用例17:sequence采用非法等价类:abc(字符);
三、为测试用例赋值并驱动开发
首先打开测试方法testSave,这个方法中会依次测试新增文档分类和修改文档分类的逻辑,定位在“测试新增文档分类”处:

首先,我们完成第一个任务“//TODO 列出新增文档分类测试用例清单”。将上面列出的“测试用例清单文档”写入多行注释中,作为测试清单。以后还有可能往这个清单中增加新的测试用例。让测试用例代码成为有价值的开发文档;
/** * 测试新增文档分类 */ /** * 列出新增文档分类测试用例清单 * 用例1:全部参数使用合法中间值 ProjectId=1L; name="测试新增文档分类一"; sequence="10"; operator="1L"; 用例2:name采用合法边界值Min:name="测"; (其它参数沿用用例1的合法中间值) 用例3:name采用合法边界值Min+:name="测试"; 用例4:name采用合法边界值Max:name="测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试"; 用例5:name采用合法边界值Max:name="测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测"; 用例6:name采用非法等价类:空值; 用例7:name采用非法边界值Max+:name="测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测超长"; 用例8:name同项目下唯一性逻辑校验:name=“文档分类一”(采用SetUp()中相同的值); 用例9:sequence采用合法边界值Min:sequence=1; 用例10:sequence采用合法边界值Min+:sequence=2; 用例11:sequence采用合法边界值Max:sequence=Integer.MAX_VALUE; 用例12:sequence采用合法边界值Max-:sequence=Integer.MAX_VALUE-1; 用例13:sequence采用非法等价类:空值; 用例14:sequence采用非法边界值Min-:sequence=0; 用例15:sequence采用非法边界值:sequence=-1; 用例16:sequence采用非法边界值Max+:sequence=Integer.MAX_VALUE+1; 用例17:sequence采用非法等价类:abc(字符); */
“云开发”平台生成的初始化代码中已经为我们设计了一个”测试新增文档分类“的测试模版,由“测试用例赋值”、“模拟请求”及“测试断言”组成。代码如下:
测试用例赋值
/**---------------------测试用例赋值开始---------------------**/ //TODO 将下面的null值换为测试参数 Category category = new Category(); category.setProjectId(null); category.setName(null); category.setSequence(null); Long operator = null; Long id = 4L; /**---------------------测试用例赋值结束---------------------**/
模拟请求
this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) )
测试断言
// 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"category" .andExpect(content().string(containsString("category"))) // 检查返回的数据节点 .andExpect(jsonPath("$.category.categoryId").value(id)) .andExpect(jsonPath("$.category.projectId").value(category.getProjectId())) .andExpect(jsonPath("$.category.name").value(category.getName())) .andExpect(jsonPath("$.category.sequence").value(category.getSequence())) .andExpect(jsonPath("$.category.creationTime").isNotEmpty()) .andExpect(jsonPath("$.category.creatorUserId").value(operator)) .andExpect(jsonPath("$.category.lastModificationTime").isEmpty()) .andExpect(jsonPath("$.category.lastModifierUserId").value(0)) .andExpect(jsonPath("$.category.isDeleted").value(false)) .andExpect(jsonPath("$.category.deletionTime").isEmpty()) .andExpect(jsonPath("$.category.deleterUserId").value(0)) .andReturn();
每一个测试用例的测试代码均由“测试用例赋值+模拟请求+测试断言组成”,测试用例赋值不同,模拟请求的参数和测试断言就应相应调整。
1.全部参数使用合法中间值:
第一个新增文档分类的测试用例代码,就在原测试模版的基础上修改即可。修改后代码:
// 用例1:全部参数使用合法中间值 /**---------------------测试用例赋值开始---------------------**/ Category category = new Category(); category.setProjectId(1L); category.setName("用例1文档分类"); category.setSequence(10); Long operator = 1L; Long id = 8L; /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"category" .andExpect(content().string(containsString("category"))) // 检查返回的数据节点 .andExpect(jsonPath("$.category.categoryId").value(id)) .andExpect(jsonPath("$.category.projectId").value(category.getProjectId())) .andExpect(jsonPath("$.category.name").value(category.getName())) .andExpect(jsonPath("$.category.sequence").value(category.getSequence())) .andExpect(jsonPath("$.category.creationTime").isNotEmpty()) .andExpect(jsonPath("$.category.creatorUserId").value(operator)) .andExpect(jsonPath("$.category.lastModificationTime").isEmpty()) .andExpect(jsonPath("$.category.lastModifierUserId").value(0)) .andExpect(jsonPath("$.category.isDeleted").value(false)) .andExpect(jsonPath("$.category.deletionTime").isEmpty()) .andExpect(jsonPath("$.category.deleterUserId").value(0)) .andReturn();
代码解说
// 用例1:全部参数使用合法中间值 /**---------------------测试用例赋值开始---------------------**/ Category category = new Category(); category.setProjectId(1L); category.setName("用例1文档分类"); category.setSequence(10); Long operator = 1L; Long id = 8L; /**---------------------测试用例赋值结束---------------------**/
给operator赋值为1,因为是Long型,所以写为“1L”。
为id赋值为“8L”。为什么输入为“8”?这里需要解释一下:
在CategoryControllerTest类运行时,先会执行testList方法,接着执行testSave方法。在执行testList方法前执行了setUp方法,其中添加了一条数据,id为“1”。接着,在testList方法中添加了5条数据,所以testList方法执行完时,User的数据库表主键id变为“6”了,虽然执行完testList方法后这6条数据都因为事务回滚清空了,但是id值“1-6”已被占用了。接着准备执行testSave方法前又执行了一次setUp方法,再次添加了一条数据,id变为“7”。所以,在testSave中添加的第一条数据的主键id值应为“8”,因为是Long型字段,所以赋值为“8L”。如果在setUp或testList中插入了更多数据,那么这个值也应相应调整,原理已说明。
this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) )
这段代码是利用mockMvc模拟post访问"/category/create"这个微服务Rest控制器接口,模拟表单提交了四个参数"projectId"、"name"、“sequence”和“operator”,值已经在上面的测试用例赋值中。
// 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"category" .andExpect(content().string(containsString("category"))) // 检查返回的数据节点 .andExpect(jsonPath("$.category.categoryId").value(id)) .andExpect(jsonPath("$.category.projectId").value(category.getProjectId())) .andExpect(jsonPath("$.category.name").value(category.getName())) .andExpect(jsonPath("$.category.sequence").value(category.getSequence())) .andExpect(jsonPath("$.category.creationTime").isNotEmpty()) .andExpect(jsonPath("$.category.creatorUserId").value(operator)) .andExpect(jsonPath("$.category.lastModificationTime").isEmpty()) .andExpect(jsonPath("$.category.lastModifierUserId").value(0)) .andExpect(jsonPath("$.category.isDeleted").value(false)) .andExpect(jsonPath("$.category.deletionTime").isEmpty()) .andExpect(jsonPath("$.category.deleterUserId").value(0)) .andReturn();
其中:
.andDo(print())
这个是用来将请求及返回结果打印到控制台中,方便测试人员查看及分析。
// 检查状态码为200 .andExpect(status().isOk())
这个是基本的检查,正确的请求返回的状态码应为“200”,如果是“404”或其它值,就代表有问题。
// 检查内容有"category" .andExpect(content().string(containsString("category")))
如果新增数据成功,那么应返回category的实例json数据,其中含有category(和领域类名称相同)这个节点。如果表单验证通不过,则返回“formErrors”节点,如果发生异常,则返回“errorMessage”节点。
// 检查返回的数据节点 .andExpect(jsonPath("$.category.categoryId").value(id)) .andExpect(jsonPath("$.category.projectId").value(category.getProjectId())) .andExpect(jsonPath("$.category.name").value(category.getName())) .andExpect(jsonPath("$.category.sequence").value(category.getSequence())) .andExpect(jsonPath("$.category.creationTime").isNotEmpty()) .andExpect(jsonPath("$.category.creatorUserId").value(operator)) .andExpect(jsonPath("$.category.lastModificationTime").isEmpty()) .andExpect(jsonPath("$.category.lastModifierUserId").value(0)) .andExpect(jsonPath("$.category.isDeleted").value(false)) .andExpect(jsonPath("$.category.deletionTime").isEmpty()) .andExpect(jsonPath("$.category.deleterUserId").value(0)) .andReturn();
这是对返回的json数据的进一步判断,其中:
categoryId的值应该等于前面定义的id值"8";
projectId、name和sequence:返回值应该等于前面赋的参数值;
creationTime:创建时间应该有值,所以可以用“.isNotEmpty()”来断言;
creatorUserId:创建者ID,应该等于前面给操作员参数赋的值1(operator);
lastModificationTime:最近修改时间,注册时未修改,所以应保存为null,因此返回值可用“isEmpty()”断言;
lastModifierUserId:最近修改者,应返回默认值“0”;
isDeleted:新增的数据应该是未删除的,所以该字段应返回“false”;
deletionTime:删除时间应保存为null,所以返回值可用“isEmpty()”断言;
deleterUserId:删除者,应返回默认值“0”;
执行测试
写完测试代码后,我们运行下单元测试,结果如下:

异常定位在“测试修改文档分类”代码中,说明第一个新增文档分类测试用例已通过。
现在为新增文档分类写第二个测试用例代码。
仅为name赋值,由于是合法边界值,所以主键ID加1。
// 用例2:name采用合法边界值Min:name="测"; /**---------------------测试用例赋值开始---------------------**/ category.setName("测"); id++; /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"category" .andExpect(content().string(containsString("category"))) // 检查返回的数据节点 .andExpect(jsonPath("$.category.categoryId").value(id)) .andExpect(jsonPath("$.category.projectId").value(category.getProjectId())) .andExpect(jsonPath("$.category.name").value(category.getName())) .andExpect(jsonPath("$.category.sequence").value(category.getSequence())) .andExpect(jsonPath("$.category.creationTime").isNotEmpty()) .andExpect(jsonPath("$.category.creatorUserId").value(operator)) .andExpect(jsonPath("$.category.lastModificationTime").isEmpty()) .andExpect(jsonPath("$.category.lastModifierUserId").value(0)) .andExpect(jsonPath("$.category.isDeleted").value(false)) .andExpect(jsonPath("$.category.deletionTime").isEmpty()) .andExpect(jsonPath("$.category.deleterUserId").value(0)) .andReturn();
同理,将用例2测试代码拷贝修改为用例3-用例5,每次给id赋值加1:
// 用例3:name采用合法边界值Min+:name="测试"; /**---------------------测试用例赋值开始---------------------**/ category.setName("测试"); id++; /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"category" .andExpect(content().string(containsString("category"))) // 检查返回的数据节点 .andExpect(jsonPath("$.category.categoryId").value(id)) .andExpect(jsonPath("$.category.projectId").value(category.getProjectId())) .andExpect(jsonPath("$.category.name").value(category.getName())) .andExpect(jsonPath("$.category.sequence").value(category.getSequence())) .andExpect(jsonPath("$.category.creationTime").isNotEmpty()) .andExpect(jsonPath("$.category.creatorUserId").value(operator)) .andExpect(jsonPath("$.category.lastModificationTime").isEmpty()) .andExpect(jsonPath("$.category.lastModifierUserId").value(0)) .andExpect(jsonPath("$.category.isDeleted").value(false)) .andExpect(jsonPath("$.category.deletionTime").isEmpty()) .andExpect(jsonPath("$.category.deleterUserId").value(0)) .andReturn(); // 用例4:name采用合法边界值Max:name="测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试"; /**---------------------测试用例赋值开始---------------------**/ category.setName("测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试"); id++; /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"category" .andExpect(content().string(containsString("category"))) // 检查返回的数据节点 .andExpect(jsonPath("$.category.categoryId").value(id)) .andExpect(jsonPath("$.category.projectId").value(category.getProjectId())) .andExpect(jsonPath("$.category.name").value(category.getName())) .andExpect(jsonPath("$.category.sequence").value(category.getSequence())) .andExpect(jsonPath("$.category.creationTime").isNotEmpty()) .andExpect(jsonPath("$.category.creatorUserId").value(operator)) .andExpect(jsonPath("$.category.lastModificationTime").isEmpty()) .andExpect(jsonPath("$.category.lastModifierUserId").value(0)) .andExpect(jsonPath("$.category.isDeleted").value(false)) .andExpect(jsonPath("$.category.deletionTime").isEmpty()) .andExpect(jsonPath("$.category.deleterUserId").value(0)) .andReturn(); // 用例5:name采用合法边界值Max:name="测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测"; /**---------------------测试用例赋值开始---------------------**/ category.setName("测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测"); id++; /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"category" .andExpect(content().string(containsString("category"))) // 检查返回的数据节点 .andExpect(jsonPath("$.category.categoryId").value(id)) .andExpect(jsonPath("$.category.projectId").value(category.getProjectId())) .andExpect(jsonPath("$.category.name").value(category.getName())) .andExpect(jsonPath("$.category.sequence").value(category.getSequence())) .andExpect(jsonPath("$.category.creationTime").isNotEmpty()) .andExpect(jsonPath("$.category.creatorUserId").value(operator)) .andExpect(jsonPath("$.category.lastModificationTime").isEmpty()) .andExpect(jsonPath("$.category.lastModifierUserId").value(0)) .andExpect(jsonPath("$.category.isDeleted").value(false)) .andExpect(jsonPath("$.category.deletionTime").isEmpty()) .andExpect(jsonPath("$.category.deleterUserId").value(0)) .andReturn();
运行单元测试,上述用例均通过测试。接下来,我们添加非法等价类:空值。我们期望非空的错误能被检查出来,返回formErrors,code应为“NotBlank”,客户端可利用返回的错误信息提示给用户。由于预期添加数据失败,所以这里就不需要让主键ID加1了,代码如下:
// 用例6:name采用非法等价类:空值; /**---------------------测试用例赋值开始---------------------**/ category.setName(""); /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"formErrors" .andExpect(content().string(containsString("formErrors"))) // 检查返回的数据节点 .andExpect(content().string(containsString("\"code\" : \"NotBlank\""))) .andReturn();
同理,我们为name的非法等价类用例添加用例7:
// 用例7:name采用非法边界值Max+:name="测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测超长"; /**---------------------测试用例赋值开始---------------------**/ category.setName("测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测试新增文档分类测超长"); /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"formErrors" .andExpect(content().string(containsString("formErrors"))) // 检查返回的数据节点 .andExpect(content().string(containsString("\"code\" : \"Length\""))) .andReturn();
运行单元测试,用例6和用例7均通过了测试。原因是,从“云开发”平台初始化Category领域类代码时已经给name字段加好了如下注解:
/** * 分类名称 */ @NotBlank(groups={CheckCreate.class, CheckModify.class}) @Length(min = 1, max = 50, groups={CheckCreate.class, CheckModify.class}) //@Pattern(regexp = "", groups={CheckCreate.class, CheckModify.class}) @Column(nullable = false, name = "name", length = 50) private String name = "";
这段代码的含义是:添加和修改name字段时会启用“@NotBlank”和“@Length”校验。
现在继续写单元测试用例代码,模拟“同一项目下文档分类名称已存在(违反唯一性)”的情况,前面我们在setUp()方法中装配了一条数据,分类名称为“文档分类一”(projectId=1)的分类且添加成功。现在数据库中已存在分类名称为“文档分类一”的数据,我们故意再添加一条这样的数据,期望能引起报错。由于这个数据并没有违反分类名称输入的基本校验,而是违反唯一性,错误提示为“该项目下已存在同名文档分类!”,这个需要通过逻辑校验,所以应返回异常:errorMessage。我们给这个异常一个编码:10001(这个编码可以根据自己的规则去编写,但是不同异常的错误编码不能相同),所以我们期望返回的结果中包含""errorMessage" : "[10001]",测试用例代码如下:
// 用例8:name同项目下唯一性逻辑校验:name=“文档分类一”(采用SetUp()中相同的值); /**---------------------测试用例赋值开始---------------------**/ category.setName("文档分类一"); /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"errorMessage" .andExpect(content().string(containsString("\"errorMessage\" : \"[10001]"))) .andReturn();
运行单元测试,测试结果显示,添加数据成功,并未检测到数据重复的异常。对于逻辑校验而言,代码应写在服务实现层:

我们需要在新增文档分类的服务实现层代码中加入逻辑判断代码:
if(category.getCategoryId()==null){ //判断同一项目里是否有同名文档分类 List<Category> list = categoryRepository.findByProjectIdAndNameAndIsDeletedFalse(category.getProjectId(),category.getName()); if(list.size() > 0){ throw new BusinessException(ErrorCode.Category_Name_Exists); } category.setCreatorUserId(Long.valueOf(request.getParameter("operator"))); return categoryRepository.save(category);

相应增加一个Dao接口和错误码:


同时,我们需要在i18n的messages配置中配置好相应的错误提示多国语言包(实现异常提示信息的本地化),语言包配置位置为资源文件夹下的"i18n/messages":

其中,中文做了64位转码,如果直接使用中文,有可能客户端出现乱码。这里介绍一个转码工具:CodeText,这是Mac系统下的一个转码工具,Windows系统下也可以找找类似工具。

英文语言包配置如下:

中文语言包和默认语言包设置一致.
现在运行单元测试,确实返回了我们期望的异常信息,测试通过了:
现在编写排序字段的测试用例代码:
用例9添加的是合法边界值1,所以主键ID应加一。同时请注意,由于对name做了唯一性逻辑校验,所以name取值为一个从未使用的值,这里使用用例序号做前缀:
// 用例9:sequence采用合法边界值Min:sequence=1; /**---------------------测试用例赋值开始---------------------**/ category.setName("用例9文档分类"); category.setSequence(1); id++; /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"category" .andExpect(content().string(containsString("category"))) // 检查返回的数据节点 .andExpect(jsonPath("$.category.categoryId").value(id)) .andExpect(jsonPath("$.category.projectId").value(category.getProjectId())) .andExpect(jsonPath("$.category.name").value(category.getName())) .andExpect(jsonPath("$.category.sequence").value(category.getSequence())) .andExpect(jsonPath("$.category.creationTime").isNotEmpty()) .andExpect(jsonPath("$.category.creatorUserId").value(operator)) .andExpect(jsonPath("$.category.lastModificationTime").isEmpty()) .andExpect(jsonPath("$.category.lastModifierUserId").value(0)) .andExpect(jsonPath("$.category.isDeleted").value(false)) .andExpect(jsonPath("$.category.deletionTime").isEmpty()) .andExpect(jsonPath("$.category.deleterUserId").value(0)) .andReturn();
同样方法,对排序的其它合法边界值测试用例进行编写:
// 用例10:sequence采用合法边界值Min+:sequence=2; /**---------------------测试用例赋值开始---------------------**/ category.setName("用例10文档分类"); category.setSequence(2); id++; /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"category" .andExpect(content().string(containsString("category"))) // 检查返回的数据节点 .andExpect(jsonPath("$.category.categoryId").value(id)) .andExpect(jsonPath("$.category.projectId").value(category.getProjectId())) .andExpect(jsonPath("$.category.name").value(category.getName())) .andExpect(jsonPath("$.category.sequence").value(category.getSequence())) .andExpect(jsonPath("$.category.creationTime").isNotEmpty()) .andExpect(jsonPath("$.category.creatorUserId").value(operator)) .andExpect(jsonPath("$.category.lastModificationTime").isEmpty()) .andExpect(jsonPath("$.category.lastModifierUserId").value(0)) .andExpect(jsonPath("$.category.isDeleted").value(false)) .andExpect(jsonPath("$.category.deletionTime").isEmpty()) .andExpect(jsonPath("$.category.deleterUserId").value(0)) .andReturn(); // 用例11:sequence采用合法边界值Max:sequence=Integer.MAX_VALUE; /**---------------------测试用例赋值开始---------------------**/ category.setName("用例11文档分类"); category.setSequence(Integer.MAX_VALUE); id++; /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"category" .andExpect(content().string(containsString("category"))) // 检查返回的数据节点 .andExpect(jsonPath("$.category.categoryId").value(id)) .andExpect(jsonPath("$.category.projectId").value(category.getProjectId())) .andExpect(jsonPath("$.category.name").value(category.getName())) .andExpect(jsonPath("$.category.sequence").value(category.getSequence())) .andExpect(jsonPath("$.category.creationTime").isNotEmpty()) .andExpect(jsonPath("$.category.creatorUserId").value(operator)) .andExpect(jsonPath("$.category.lastModificationTime").isEmpty()) .andExpect(jsonPath("$.category.lastModifierUserId").value(0)) .andExpect(jsonPath("$.category.isDeleted").value(false)) .andExpect(jsonPath("$.category.deletionTime").isEmpty()) .andExpect(jsonPath("$.category.deleterUserId").value(0)) .andReturn(); // 用例12:sequence采用合法边界值Max-:sequence=Integer.MAX_VALUE-1; /**---------------------测试用例赋值开始---------------------**/ category.setName("用例12文档分类"); category.setSequence(Integer.MAX_VALUE-1); id++; /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"category" .andExpect(content().string(containsString("category"))) // 检查返回的数据节点 .andExpect(jsonPath("$.category.categoryId").value(id)) .andExpect(jsonPath("$.category.projectId").value(category.getProjectId())) .andExpect(jsonPath("$.category.name").value(category.getName())) .andExpect(jsonPath("$.category.sequence").value(category.getSequence())) .andExpect(jsonPath("$.category.creationTime").isNotEmpty()) .andExpect(jsonPath("$.category.creatorUserId").value(operator)) .andExpect(jsonPath("$.category.lastModificationTime").isEmpty()) .andExpect(jsonPath("$.category.lastModifierUserId").value(0)) .andExpect(jsonPath("$.category.isDeleted").value(false)) .andExpect(jsonPath("$.category.deletionTime").isEmpty()) .andExpect(jsonPath("$.category.deleterUserId").value(0)) .andReturn();
经测试,均通过。
现在针对排序字段的非法等价类空值写测试代码:
由于是非法等价类,期望操作失败,所以id不必加一;为防止name重名,所以name仍然赋值;sequence是int型数据,无法通过“category.setSequence()”赋空值,所以直接通过.param("sequence","")给参数赋值:
// 用例13:sequence采用非法等价类:空值; /**---------------------测试用例赋值开始---------------------**/ category.setName("用例13文档分类"); /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence","")//int型数据空值参数直接在mock请求中传参 .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"formErrors" .andExpect(content().string(containsString("formErrors"))) // 检查返回的数据节点 .andExpect(content().string(containsString("\"code\" : \"NotNull\""))) .andReturn();
运行单元测试,发现返回了“errorMessage”异常,而不是formErrors:

从领域类category的sequence字段注解着手修改代码,修改前代码:
/** * 排序 */ //@Range(min=value,max=value, groups={CheckCreate.class, CheckModify.class}) @Column(nullable = false, name = "sequence") private Integer sequence;
修改后代码:
/** * 排序 */ @NotNull(groups={CheckCreate.class, CheckModify.class}) @Min(value = 1, groups={CheckCreate.class, CheckModify.class}) @Column(nullable = false, name = "sequence") private Integer sequence;
再次运行测试,通过。剩下的测试用例代码直接贴代码,测试通过:
// 用例14:sequence采用非法边界值Min-:sequence=0; /**---------------------测试用例赋值开始---------------------**/ category.setName("用例14文档分类"); category.setSequence(0); /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"formErrors" .andExpect(content().string(containsString("formErrors"))) // 检查返回的数据节点 .andExpect(content().string(containsString("\"code\" : \"Min\""))) .andReturn(); // 用例15:sequence采用非法边界值:sequence=-1; /**---------------------测试用例赋值开始---------------------**/ category.setName("用例15文档分类"); category.setSequence(-1); /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"formErrors" .andExpect(content().string(containsString("formErrors"))) // 检查返回的数据节点 .andExpect(content().string(containsString("\"code\" : \"Min\""))) .andReturn(); // 用例16:sequence采用非法边界值Max+:sequence=Integer.MAX_VALUE+1; /**---------------------测试用例赋值开始---------------------**/ category.setName("用例16文档分类"); category.setSequence(Integer.MAX_VALUE+1); /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence",category.getSequence().toString()) .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"formErrors" .andExpect(content().string(containsString("formErrors"))) // 检查返回的数据节点 .andExpect(content().string(containsString("\"code\" : \"Min\""))) .andReturn(); // 用例17:sequence采用非法等价类:abc(字符); /**---------------------测试用例赋值开始---------------------**/ category.setName("用例17文档分类"); /**---------------------测试用例赋值结束---------------------**/ this.mockMvc.perform( MockMvcRequestBuilders.post("/category/create") .param("projectId",category.getProjectId().toString()) .param("name",category.getName()) .param("sequence","abc") .param("operator",operator.toString()) ) // 打印结果 .andDo(print()) // 检查状态码为200 .andExpect(status().isOk()) // 检查内容有"formErrors" .andExpect(content().string(containsString("formErrors"))) // 检查返回的数据节点 .andExpect(content().string(containsString("\"code\" : \"typeMismatch\""))) .andReturn();
通过以上17个测试用例,完成了“添加文档分类”所需的单元测试代码,如果后续仍发现未被覆盖到的情况,可以在此基础上继续增加测试用例。