[内容指引]
运行单元测试;
装配一条数据;
模拟更多数据测试列表;
测试无搜索列表;
测试标准查询;
测试高级查询.
一, 运行单元测试
我们以文档分类 (Category) 这个领域类为例, 示范如何通过编写测试用例来驱动代码开发. 首先我们可以打开 Category 的单元测试初始化代码 CategoryControllerTest.java:
上面是通过 macOS 操作系统下 "IntelliJ IDEA" 打开项目的界面, 首先我们在 "CategoryControllerTest" 上点鼠标右键, 选择 Run 'CategoryControllerTest'来运行 Category 的单元测试类:
我们看到该单元测试类中的四个测试方法均未通过测试. 第一个运行的测试方法是 testList, 所以, 我们将首先从 testList 这个方法开始.
二, 装配一条数据
从现在开始, 我们在 Category 的单元测试类代码 CategoryControllerTest 中, 从第一行代码开始, 从上往下, 根据 "//TODO" 的提示, 逐步完成测试用例的编写. 第一个 "//TODO" 的任务提示出现在 @Before 注解的 setUp()方法中. 该方法将会在后续每个测试方法 (testList,testSave,testView,testDelete) 运行前均会运行一次:
- // 使用 JUnit 的 @Before 注解可在测试开始前进行一些初始化的工作
- @Before
- public void setUp() throws JsonProcessingException {
- /**--------------------- 测试用例赋值开始 ---------------------**$/TODO 参考实际业务中新增数据所提供的参数, 基于 "最少字段和数据正确的原则", 将下面的 null 值换为测试参数
- c1 = new Category();
- c1.setProjectId(null);
- c1.setName(null);
- c1.setSequence(null);
- c1.setCreatorUserId(1);
- categoryRepository.save(c1);
- /**--------------------- 测试用例赋值结束 ---------------------**$/ 获取 mockMvc 对象实例
- mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
- }
- 在 setUp()方法中, 我们向数据库添加一条数据. 默认为领域类 Category 中的所有字段赋值(如果含有审计字段, 仅为创建者 creatorUserId 赋值). 代码如下:
- // 使用 JUnit 的 @Before 注解可在测试开始前进行一些初始化的工作
- @Before
- public void setUp() throws JsonProcessingException {
- /**--------------------- 测试用例赋值开始 ---------------------**/
- c1 = new Category();
- c1.setProjectId(1L);
- c1.setName("文档分类一");
- c1.setSequence(1);
- c1.setCreatorUserId(1);
- categoryRepository.save(c1);
- /**--------------------- 测试用例赋值结束 ---------------------**$/ 获取 mockMvc 对象实例
- mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
- }
- 最佳实践
- 在这里建议不要给所有字段赋值, 而是本着最少字段和数据正确的原则.
- 所谓最少字段赋值原则, 是指, 最终客户端添加数据的表单页面有几个字段是必须赋值, 那么就给这几个字段赋值. 以添加文档分类为例, form 表单上需要给分类名称 (name) 和排序 (sequence) 赋值, 但是提交表单时可能从 Session 或隐藏控件中提供该分类所属的项目 (projectId) 和操作人(operator). 所以这四个字段就是最少字段. 如果表单中有一些字段是非必填字段, 那么就不用赋值.
- 所谓数据正确原则, 是因为我们假设通过新增数据方法而插入数据库的数据都是合法数据, 对于不合法数据的校验是新增数据方法的职责, 而不是列表查询方法的职责. 我们这里是通过 JPA 接口直接保存到数据库的, 并未采用服务实现层的 save 方法. 在文档分类这个业务中, 正确的数据的基本要求: 项目 ID 应该是大于 0 的 Long 型数据; 文档名称不能为空, 不能超过十位长度; 排序应为大于 0 的整数, 不能是字符, 操作者 ID 应为大于 0 的数据.
- 现在再次运行测试, 看 testList 的 setUp()方法是否报错:
- 异常分析
- 现在出错的代码是第 108 行, 处于 testList()方法体内, 而 setUp()方法运行在 testList()方法之前. 这也意味着 setUp()方法没问题了.
- 如果我们给 setUp()中输入了合理的值, 但是该方法仍然出错该怎么做?
- 以我的经验, 就到该领域类 "Cagegory.java" 中调整下各字段的默认值:
- 最佳实践
- 一般领域类中的字段, 对于非必填值的字段的处理方法:
- 日期型: 允许 null 值即可;
- 布尔型: 输入一个默认值, true 或 false, 根据字段含义确定;
- 数值型: 输入一个默认值, 整数型的输入 0, 非整数型的输入 0.0, 但如果业务规则有特殊定义的, 输入特定默认数值;
- 字符型: 输入空字符串为默认值, 因为如果存入的是 null 值, 无法被上面 JPA 接口中标准查询和高级查询方法查出来.
- 三, 模拟更多数据测试列表
- 将代码定位到 testList 方法:
- 在上图中给出了添加第二条数据的代码模版, 可复制该段代码多份, 依次改为 c3,c4... 以向数据库插入多条数据, 充分测试无查询列表, 标准查询和高级查询.
- 最佳实践
- 前面基于最少字段和数据正确的原则模拟实际业务中创建数据的参数构建了一条数据, 一般而言, 我们还需要模拟出 "经过修改过的数据"(给更多字段赋值), 对于启用删除审计的领域类, 还应该模拟出非物理删除的数据.
- 模拟数据前代码:
- //TODO 建议借鉴下面的测试用例赋值模版构造更多数据以充分测试 "无搜索列表","标准查询" 和 "高级查询" 的表现
- // 提示: 构建 "新增数据" 提示: 根据新增数据时客户端实际能提供的参数, 依据 "最少字段和数据正确的原则" 构建
- // 提示: 构建 "修改过的数据" 提示: 根据修改数据时客户端实际能提供的参数构建
- // 提示: 可以构建 "非物理删除的数据"
- /**--------------------- 测试用例赋值开始 ---------------------**$/TODO 将下面的 null 值换为测试参数
- Category c2 = new Category();
- c2.setProjectId(null);
- c2.setName(null);
- c2.setSequence(null);
- c2.setCreatorUserId(2);
- // 提示: 构造 "修改过的数据" 时需要给 "最近修改时间" 和 "最近修改者" 赋值
- //c2.setLastModificationTime(new Date());
- //c2.setLastModifierUserId(1);
- // 提示: 构造 "非物理删除的数据" 时需要给 "已删除","删除时间" 和 "删除者" 赋值
- //c2.setIsDeleted(true);
- //c2.setDeletionTime(new Date());
- //c2.setDeleterUserId(1);
- categoryRepository.save(c2);
- /**--------------------- 测试用例赋值结束 ---------------------**/
模拟数据后代码:
- // 添加分类: 用例 2(分类名称与装配数据中的分类名称有部分关键字相同)
- /**--------------------- 测试用例赋值开始 ---------------------**/
- Category c2 = new Category();
- c2.setProjectId(1L);
- c2.setName("文档分类二");
- c2.setSequence(2);
- c2.setCreatorUserId(2);
- categoryRepository.save(c2);
- /**--------------------- 测试用例赋值结束 ---------------------**$/ 添加分类: 用例 3(分类名称与用例 1 和用例 2 完全不同)
- /**--------------------- 测试用例赋值开始 ---------------------**/
- Category c3 = new Category();
- c3.setProjectId(1L);
- c3.setName("项目资料归档");
- c3.setSequence(3);
- c3.setCreatorUserId(2);
- categoryRepository.save(c3);
- /**--------------------- 测试用例赋值结束 ---------------------**$/ 添加分类: 用例 4(名称与用例 1 一样, 但是所属项目不同)
- /**--------------------- 测试用例赋值开始 ---------------------**/
- Category c4 = new Category();
- c4.setProjectId(2L);
- c4.setName("文档分类一");
- c4.setSequence(1);
- c4.setCreatorUserId(2);
- categoryRepository.save(c4);
- /**--------------------- 测试用例赋值结束 ---------------------**$/ 修改分类: 用例 5
- /**--------------------- 测试用例赋值开始 ---------------------**/
- Category c5 = new Category();
- c5.setProjectId(1L);
- c5.setName("被修改过的文档分类");
- c5.setSequence(4);
- c5.setCreatorUserId(2);
- c5.setLastModificationTime(new Date());
- c5.setLastModifierUserId(1);
- categoryRepository.save(c5);
- /**--------------------- 测试用例赋值结束 ---------------------**$/ 删除分类: 用例 6
- /**--------------------- 测试用例赋值开始 ---------------------**/
- Category c6 = new Category();
- c6.setProjectId(1L);
- c6.setName("被删除过的文档分类");
- c6.setSequence(5);
- c6.setCreatorUserId(2);
- c6.setLastModificationTime(new Date());
- c6.setLastModifierUserId(1);
- c6.setIsDeleted(true);
- c6.setDeletionTime(new Date());
- c6.setDeleterUserId(1);
- categoryRepository.save(c6);
- /**--------------------- 测试用例赋值结束 ---------------------**/
现在再次运行 "UserControllerTest" 单元测试:
我们看到现在异常定位到 177 行代码, 在 "测试无搜索列表" 中, 说明上面构造五条测试数据的代码已能通过.
四, 测试无搜索列表
此时, 我们在 setUp()方法中构造了一条数据, 在 testList 中构造了三条新增数据, 一条修改过的数据和一条非物理删除的数据, 共六条数据, 加载项目 ID 为 1 的文档分类时, 排除用例 6(已非物理删除)和用例 4(不属于该项目的文档分类), 我们加载列表时应返回 4 条数据, 所以我们期望返回的数据数量应为 4.
修改前代码:
- /**
- * 测试无搜索列表
- *$**--------------------- 测试用例赋值开始 ---------------------**$/TODO 将下面的 null 值换为测试参数
- Pageable pageable=new PageRequest(0,10, Sort.Direction.DESC,"categoryId");
- // 期望获得的结果数量(默认有两个测试用例, 所以值应为 "2L", 如果新增了更多测试用例, 请相应设定这个值)
- expectResultCount = null;
- /**--------------------- 测试用例赋值结束 ---------------------**$/ 直接通过 dao 层接口方法获得期望的数据
- Page<Category> pagedata = categoryRepository.findByProjectIdAndIsDeletedFalse(c1.getCategoryId(), pageable);
- expectData = JsonPath.read(Obj2Json(pagedata),"$").toString();
- MvcResult mvcResult = mockMvc
- .perform(
- MockMvcRequestBuilders.get("/category/list/projectId=1")
- .accept(MediaType.APPLICATION_JSON)
- )
- // 打印结果
- .andDo(print())
- // 检查状态码为 200
- .andExpect(status().isOk())
- // 检查返回的数据节点
- .andExpect(jsonPath("$.pagedata.totalElements").value(expectResultCount))
- .andExpect(jsonPath("$.dto.keyword").isEmpty())
- .andExpect(jsonPath("$.dto.projectId").value(1))
- .andReturn();
- // 提取返回结果中的列表数据及翻页信息
- responseData = JsonPath.read(mvcResult.getResponse().getContentAsString(),"$.pagedata").toString();
- System.out.println("============= 无搜索列表期望结果:" + expectData);
- System.out.println("============= 无搜索列表实际返回:" + responseData);
- Assert.assertEquals("错误, 无搜索列表返回数据与期望结果有差异",expectData,responseData);
- 赋值说明
- 1>. 领域中含有 "sequence" 字段时, 通常是要根据该字段升序陈列数据;
- 2>. 将 "expectResultCount" 赋值为 4(因为是 Long 型, 所以输入为 "4L");
- 3>. 另外, 大部分情况下请求无搜索类别的网址是不需要带参数的, 但本例请求的列表需要根据项目 (projectId) 筛选数据, 仅显示某项目的文档分类, 所以实际业务中请求列表的网址应该是 "/category/list/projectId=XX";
- 修改后代码如下:
- 再次运行 "CategoryControllerTest" 单元测试:
- 异常分析
- 现在异常指向无搜索列表的最后一句断言, 期望返回的 json 数据和实际返回的 json 数据不一致, 在控制台看看打印的两个数据, 发现只是数据排序不一致导致的.
- 现在可以打开控制器层代码, 将控制器中列表的方法代码改一下. 修改前:
- /**
- * 文档分类
- * GET: /category/list
- * @param pageable
- * @param dto
- * @return
- */
- @GetMapping("/list") public Map < String,
- Object > list(@PageableDefault(sort = {
- "categoryId"
- },
- direction = Sort.Direction.DESC) Pageable pageable, CategoryDTO dto) {
- Map < String,
- Object > map = Maps.newHashMap();
- Page < Category > pagedata = categoryService.getPageData(dto, pageable);
- map.put("dto", dto);
- map.put("pagedata", pagedata);
- return map;
- }
稍作调整:
再次执行测试:
异常代码定位到 "测试标准查询" 部分了, 说明 "无查询列表" 部分的代码测试已通过了.
五, 测试标准查询
标准查询时搜索框会将关键字 (keyword) 作为参数传到 Rest 控制器接口.
当前 "测试标准查询" 的代码如下:
- /**
- * 测试标准查询
- *$**--------------------- 测试用例赋值开始 ---------------------**$/TODO 将下面的 null 值换为测试参数
- dto = new CategoryDTO();
- dto.setKeyword(null);
- dto.setProjectId(c1.getProjectId());
- pageable=new PageRequest(0,10, Sort.Direction.DESC,"categoryId");
- // 期望获得的结果数量
- expectResultCount = null;
- /**--------------------- 测试用例赋值结束 ---------------------**/
- String keyword = dto.getKeyword().trim();
- // 直接通过 dao 层接口方法获得期望的数据
- pagedata = categoryRepository.findByNameContainingAllIgnoringCaseAndProjectIdAndIsDeletedFalse(keyword, dto.getProjectId(), pageable);
- expectData = JsonPath.read(Obj2Json(pagedata),"$").toString();
- mvcResult = mockMvc
- .perform(
- MockMvcRequestBuilders.get("/category/list")
- .param("keyword",dto.getKeyword())
- .accept(MediaType.APPLICATION_JSON)
- )
- // 打印结果
- .andDo(print())
- // 检查状态码为 200
- .andExpect(status().isOk())
- // 检查返回的数据节点
- .andExpect(jsonPath("$.pagedata.totalElements").value(expectResultCount))
- .andExpect(jsonPath("$.dto.keyword").value(dto.getKeyword()))
- .andExpect(jsonPath("$.dto.name").isEmpty())
- .andReturn();
- // 提取返回结果中的列表数据及翻页信息
- responseData = JsonPath.read(mvcResult.getResponse().getContentAsString(),"$.pagedata").toString();
- System.out.println("============= 标准查询期望结果:" + expectData);
- System.out.println("============= 标准查询实际返回:" + responseData);
- Assert.assertEquals("错误, 标准查询返回数据与期望结果有差异",expectData,responseData);
第一个标准查询的测试目标
projectId 为 1, 关键字为 "文档分类", 这个关键字在用例 1, 用例 2, 用例 4, 用例 5, 用例 6 均有出现, 用例 3 不含该, 排除用例 4(projectId 值为 2), 排除用例 6(已删除), 所以应返回 3 条结果:
第二个标准查询的测试目标
现在我们将上面的代码复制一份, 改为标准查询的第二种测试. projectId 为 1, 关键字为空, 所有用例都应满足, 排除用例 4(projectId 值为 2), 排除用例 6(已删除), 所以应返回 4 条结果:
第三个标准查询的测试目标
projectId 设为 1, 关键字为 "资料归档", 应全部不满足:
第四个标准查询的测试目标
projectId 设为 2, 关键字为 "资料归档", 应仅用例 3 满足, 所以返回 1 条数据:
现在运行单元测试, 各.... 位.... 观.... 众!!!...
发现 testList 方法已变绿, 说明 list 方法已通过测试!
如果测试工程师提供更多有价值的测试用例, 可以继续添加测试代码.
六, 测试高级查询
本例 "文档分类" 没有高级查询接口, 故无法演示. 但是本项目的领域类文档 (Document) 有高级查询接口, 有兴趣的同学可以下载 Github 源码参考.
Github 代码获取: https://github.com/MacManon/top_cloudev_doc
来源: https://www.cnblogs.com/cloud-dev/p/ce-shi-qu-dong-kai-fa-shi-jian3congtestList-kai-sh.html