[内容指引]
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 个测试用例, 完成了 "添加文档分类" 所需的单元测试代码, 如果后续仍发现未被覆盖到的情况, 可以在此基础上继续增加测试用例.
来源: https://www.cnblogs.com/cloud-dev/p/ce-shi-qu-dong-kai-fa-shi-jian4testSave-zhi-xin-ze.html