测试驱动开发实践3————从testList开始
【内容指引】
运行单元测试;
装配一条数据;
模拟更多数据测试列表;
测试无搜索列表;
测试标准查询;
测试高级查询。
一、运行单元测试
我们以文档分类(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
测试驱动开发实践3————从testList开始的更多相关文章
- 测试驱动开发实践2————从testList开始
内容指引 1.测试之前 2.改造dto层代码 3.改造dao层代码 4.改造service.impl层的list方法 5.通过testList驱动domain领域类的修改 一.测试之前 在" ...
- 测试驱动开发实践—从testList开始
[内容指引]运行单元测试:装配一条数据:模拟更多数据测试列表:测试无搜索列表:测试标准查询:测试高级查询. 一.运行单元测试 我们以文档分类(Category)这个领域类为例,示范如何通过编写测试用例 ...
- 测试驱动开发实践 - Test-Driven Development(转)
一.前言 不知道大家有没听过“测试先行的开发”这一说法,作为一种开发实践,在过去进行开发时,一般是先开发用户界面或者是类,然后再在此基础上编写测试. 但在TDD中,首先是进行测试用例的编写,然后再进行 ...
- 测试驱动开发实践 - Test-Driven Development
一.前言 不知道大家有没听过“测试先行的开发”这一说法,作为一种开发实践,在过去进行开发时,一般是先开发用户界面或者是类,然后再在此基础上编写测试. 但在TDD中,首先是进行测试用例的编写,然后再进行 ...
- 测试驱动开发实践4————testSave之新增文档分类
[内容指引] 1.确定"新增文档分类"的流程及所需的参数 2.根据业务规则设计测试用例 3.为测试用例赋值并驱动开发 一.确定"新增文档分类"的流程及所需的参数 ...
- 测试驱动开发实践3————testSave之新增用户
内容指引 1.确定新增用户的业务规则 2.根据业务规则设计测试用例 3.为测试用例赋值并驱动开发 一.确定新增用户的规则 1.注册用户允许通过"用户名+密码"."手机号+ ...
- 测试驱动开发实践5————testSave之修改文档分类
[内容指引] 1.确定"修改文档分类"的微服务接口及所需的参数 2.设计测试用例及测试用例合并 3.为测试用例赋值并驱动开发 上一篇我们通过17个测试用例完成了"新增文档 ...
- TDD(测试驱动开发)培训录
2014年我一直从事在敏捷实践咨询项目,这也是我颇有收获的一年,特别是咨询项目的每一点改变,不管是代码质量的提高,还是自组织团队的建设,都能让我们感到欣慰.涉及人的问题都是复杂问题,改变人,改变一个组 ...
- 测试驱动开发(TDD)的思考
极限编程 敏捷开发是一种思想,极限编程也是一种思想,它与敏捷开发某些目标是一致的.只是实现方式不同.测试驱动开发是极限编程的一部分. 1.极限编程这个思路的来源 Kent Beck先生最早在其极限编程 ...
随机推荐
- 通过Navicat连接MySQL数据库
步骤一.从Navicat官网下载Navicat11版本安装包安装 下载连接:http://www.formysql.com/xiazai_mysql.html 步骤二.下载补丁破解程序PatchNav ...
- mysql压缩包安装方式
从官网https://dev.mysql.com/downloads/mysql/上下载mysql-5.6.31-winx64.zip,将其解压,接下来的安装是通过命令来安装MySQL数据库的.(P. ...
- Postman使用小技巧
Postman使用小技巧 2017-09-13 目录: 1 自动生成流水号2 保存响应结果 1 自动生成流水号 返回 为了让接口具有幂等性,在设计时,往往有一个字段是唯一的(比如流水号,交易编号等), ...
- opencv 学习入门篇
unbuntu 安装:http://blog.csdn.net/cocoaqin/article/details/78163171 windows 安装:https://jingyan.baidu.c ...
- 关于eclipse新建项目问题
新建Dynamic Web Project项目的时候不能有中文名和空格. 不然会报错:The requested resource is not available. 里面的文件名也不能有中文名和空格 ...
- 关于 promise 吃到错误的理解
关于 promise 吃到错误的理解 下面的内容需要对浏览器原生支持的 promise 的基本用法有了解,如果你还不知道 promise 和 promise 的 catch 方法,你可能需要先在 这里 ...
- unity A*寻路 (一)导出NavMesh数据
使用unity的API NavMesh.CalculateTriangulation 可以获取NavMesh数据 首先 我们创建一个新的工程 保存一个test场景 然后在场景中添加一个Plane作 ...
- LNMP详解
目录 Nginx配置 1 PHP解析 1 Mysql操作 3 服务安装 3 连接测试 3 数据配置 3 Blogs建立 4 LNMP 环境 Mysql:1 ...
- SpringBoot工作机制
1:前言 回顾探索Spring框架 1.spring ioc IoC其实有两种方式,一种就是DI,而另一种是DL,即Dependency Lookup(依赖查找),前者是当前软件实体被动接受其依赖的其 ...
- [Bzoj 2547] [Ctsc2002] 玩具兵
2547: [Ctsc2002]玩具兵 Time Limit: 10 Sec Memory Limit: 128 MBSubmit: 317 Solved: 152[Submit][Status] ...