测试驱动开发实践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个测试用例,完成了“添加文档分类”所需的单元测试代码,如果后续仍发现未被覆盖到的情况,可以在此基础上继续增加测试用例。
测试驱动开发实践4————testSave之新增文档分类的更多相关文章
- 测试驱动开发实践5————testSave之修改文档分类
[内容指引] 1.确定"修改文档分类"的微服务接口及所需的参数 2.设计测试用例及测试用例合并 3.为测试用例赋值并驱动开发 上一篇我们通过17个测试用例完成了"新增文档 ...
- 测试驱动开发实践3————testSave之新增用户
内容指引 1.确定新增用户的业务规则 2.根据业务规则设计测试用例 3.为测试用例赋值并驱动开发 一.确定新增用户的规则 1.注册用户允许通过"用户名+密码"."手机号+ ...
- 测试驱动开发实践3————从testList开始
[内容指引] 运行单元测试: 装配一条数据: 模拟更多数据测试列表: 测试无搜索列表: 测试标准查询: 测试高级查询. 一.运行单元测试 我们以文档分类(Category)这个领域类为例,示范如何通过 ...
- 测试驱动开发实践—从testList开始
[内容指引]运行单元测试:装配一条数据:模拟更多数据测试列表:测试无搜索列表:测试标准查询:测试高级查询. 一.运行单元测试 我们以文档分类(Category)这个领域类为例,示范如何通过编写测试用例 ...
- scikit learn 模块 调参 pipeline+girdsearch 数据举例:文档分类 (python代码)
scikit learn 模块 调参 pipeline+girdsearch 数据举例:文档分类数据集 fetch_20newsgroups #-*- coding: UTF-8 -*- import ...
- 学习笔记CB002:词干提取、词性标注、中文切词、文档分类
英文词干提取器,import nltk,porter = nltk.PorterStemmer(),porter.stem('lying') . 词性标注器,pos_tag处理词序列,根据句子动态判断 ...
- UWP-MSDN文档分类
原文:UWP-MSDN文档分类 UWP学习目录整理 0x00 可以忽略的废话 10月6号靠着半听半猜和文字直播的补充看完了微软的秋季新品发布会,信仰充值成功,对UWP的开发十分感兴趣,打算后面找时间学 ...
- 测试驱动开发实践2————从testList开始
内容指引 1.测试之前 2.改造dto层代码 3.改造dao层代码 4.改造service.impl层的list方法 5.通过testList驱动domain领域类的修改 一.测试之前 在" ...
- 随机采样和随机模拟:吉布斯采样Gibbs Sampling实现文档分类
http://blog.csdn.net/pipisorry/article/details/51525308 吉布斯采样的实现问题 本文主要说明如何通过吉布斯采样进行文档分类(聚类),当然更复杂的实 ...
随机推荐
- java中断
理解java中断 Java中断机制是一种协作机制,即通过中断并不能直接终止另一个线程,而需要被中断的线程自己处理中断.例如,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为tr ...
- vue.js设置、获取、删除cookie
项目需要前端获取后台返回的cookie,并以此作判断.我是在main.js入口文件下使用的 具体代码: new Vue({ el: '#app', router, template: '<App ...
- 新手立体四子棋AI教程(1)——基础扫盲
一.引言 最近身边好几个朋友开始玩立体四子棋,激起了我的好奇心.那么首先来说什么是[立体四子棋],规则又是如何呢? 上图即为立体四子棋,规则类似于五子棋四子连在一起,但是四子棋更加多样.丰富.不仅可以 ...
- Java基础知识回顾之二 ----- 修饰符和String
前言 在上一篇中,回顾了Java的基本数据类型 ,这篇就来回顾下Java中的一些修饰符以及String. 修饰符介绍 Java修饰符主要分为两类: 访问修饰符 非访问修饰符 其中访问修饰符主要包括 p ...
- 关于Oracle-SQL语句性能优化
Oracle-Sql语句性能优化 相信许多从事几年的开发人员都有过一些经验,相对于刚出来的毕业生而言,对于同种操作sql结果,他们的代码性能会更高一些.虽然本人还是个实习生,在这还是写写自己 ...
- java创建运行以及项目结构
一 创建java project 再src下添加class,选择一个class添加main方法作为程序的入口 二.项目结构: src下添加不同的包,命名方法为com.jikexueyuan.hello ...
- C++关联容器知识总结
C++的容器类型可以分为顺序容器和关联容器两大类.顺序容器的知识可以参看我上篇的随笔<C++顺序容器知识总结>.关联容器支持通过键值来高效的查找和读取元素,这是它和顺序容器最大的区别.两种 ...
- python函数知识点(详解匿名函数)
Python函数是组织好的.单一的.具有独立功能模块的代码块. 函数能提高应用的模块性,和代码的重复利用率.Python提供了许多内建函数,比如print().但你也可以自己创建函数,这被叫做用户自定 ...
- python开发装饰器的应用
python全栈开发-Day10 装饰器(闭合函数的应用场) 一. 装饰器 装饰器就是闭包函数的一种应用场景 什么是闭包函数?我们再来回忆一下: 闭包函数: 定义在函数内部的函数,并且该函数包含对 ...
- servlet本质
首先我们先要知道servlet是什么,这有两种解释.一是目前大多数人所说的,一个实现了servlet接口的类就可以叫作servlet.二,servlet只是一个接口.那么看起来这两点都和servlet ...