【内容指引】
运行单元测试;
装配一条数据;
模拟更多数据测试列表;
测试无搜索列表;
测试标准查询;
测试高级查询。

一、运行单元测试

我们以文档分类(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开始的更多相关文章

  1. 测试驱动开发实践2————从testList开始

    内容指引 1.测试之前 2.改造dto层代码 3.改造dao层代码 4.改造service.impl层的list方法 5.通过testList驱动domain领域类的修改 一.测试之前 在" ...

  2. 测试驱动开发实践—从testList开始

    [内容指引]运行单元测试:装配一条数据:模拟更多数据测试列表:测试无搜索列表:测试标准查询:测试高级查询. 一.运行单元测试 我们以文档分类(Category)这个领域类为例,示范如何通过编写测试用例 ...

  3. 测试驱动开发实践 - Test-Driven Development(转)

    一.前言 不知道大家有没听过“测试先行的开发”这一说法,作为一种开发实践,在过去进行开发时,一般是先开发用户界面或者是类,然后再在此基础上编写测试. 但在TDD中,首先是进行测试用例的编写,然后再进行 ...

  4. 测试驱动开发实践 - Test-Driven Development

    一.前言 不知道大家有没听过“测试先行的开发”这一说法,作为一种开发实践,在过去进行开发时,一般是先开发用户界面或者是类,然后再在此基础上编写测试. 但在TDD中,首先是进行测试用例的编写,然后再进行 ...

  5. 测试驱动开发实践4————testSave之新增文档分类

    [内容指引] 1.确定"新增文档分类"的流程及所需的参数 2.根据业务规则设计测试用例 3.为测试用例赋值并驱动开发 一.确定"新增文档分类"的流程及所需的参数 ...

  6. 测试驱动开发实践3————testSave之新增用户

    内容指引 1.确定新增用户的业务规则 2.根据业务规则设计测试用例 3.为测试用例赋值并驱动开发 一.确定新增用户的规则 1.注册用户允许通过"用户名+密码"."手机号+ ...

  7. 测试驱动开发实践5————testSave之修改文档分类

    [内容指引] 1.确定"修改文档分类"的微服务接口及所需的参数 2.设计测试用例及测试用例合并 3.为测试用例赋值并驱动开发 上一篇我们通过17个测试用例完成了"新增文档 ...

  8. TDD(测试驱动开发)培训录

    2014年我一直从事在敏捷实践咨询项目,这也是我颇有收获的一年,特别是咨询项目的每一点改变,不管是代码质量的提高,还是自组织团队的建设,都能让我们感到欣慰.涉及人的问题都是复杂问题,改变人,改变一个组 ...

  9. 测试驱动开发(TDD)的思考

    极限编程 敏捷开发是一种思想,极限编程也是一种思想,它与敏捷开发某些目标是一致的.只是实现方式不同.测试驱动开发是极限编程的一部分. 1.极限编程这个思路的来源 Kent Beck先生最早在其极限编程 ...

随机推荐

  1. 重磅消息-Service Fabric 正式开源

    微软的Azure Service Fabric的官方博客在2017.3.24日发布了一篇博客 Service Fabric .NET SDK goes open source ,介绍了社区呼声最高的S ...

  2. 树莓派centos安装的基本配置

    萌新再发一帖,这篇文章呢主要是为大家在树莓派上安装centos以后提供一个问题的解决方案. 首先我呢觉得好奇就在某宝上花了两百来块钱买了一套树莓派,很多人喜欢在树莓派上安装Debian,我呢更青睐用R ...

  3. 用JavaScript写一个区块链

    几乎每个人都听说过像比特币和以太币这样的加密货币,但是只有极少数人懂得隐藏在它们背后的技术.在这篇博客中,我将会用JavaScript来创建一个简单的区块链来演示它们的内部究竟是如何工作的.我将会称之 ...

  4. (python走过的坑)OpenCV中错误opencv-3.3.1\modules\highgui\src\window.cpp:339: error: (-215) size.width>0 && size.height>0 in function cv::imshow

    第一次在python中使用OpenCV(cv2),运行时报错opencv-3.3.1\modules\highgui\src\window.cpp:339: error: (-215) size.wi ...

  5. 初步谈谈 C# 多线程、异步编程与并发服务器

    多线程与异步编程可以达到避免调用线程异步阻塞作用,但是两者还是有点不同. 多线程与异步编程的异同: 1.线程是cpu 调度资源和分配的基本单位,本质上是进程中的一段并发执行的代码. 2.线程编程的思维 ...

  6. javascript排序、功能代码总结[长期更新]

    //基本数组去重 ///输入:向数组中添加功能函数unique ///输出:返回一个没有重复元素的数组 Array.prototype.unique = function(){ result =[]; ...

  7. CentOS 远程桌面相关服务安装笔记

    # CentOS 7安装图形界面 sudo yum groupinstall "GNOME Desktop" "Graphical Administration Tool ...

  8. 【Unity与23种设计模式】原型模式(Prototype)

    GoF中定义: "使用原型对象来产生指定类的对象,所以产生对象时,是使用复制原型对象来完成." Unity中 开发者可以组装游戏对象 它可以包括复杂的组件 组装好了之后,就可以将其 ...

  9. 使用git指令下载github仓库代码(笔记)

    通过Git指令下载源码 Git概念说明 ​ 三种状态:修改状态.暂存状态和Git仓库 ​ 基本的Git工作流程: ​ 在工作目录中修改文件 ​ 暂存文件,将文件的快照放入暂存区域 ​ 提交更新,找到暂 ...

  10. 部署腾讯云(CentOS6.6版本,jdk1.7+tomcat8+mysql)

    这是从一个大神哪里学到的,用来留下来用以记录 http://blog.csdn.net/qingluoII/article/details/76053736 只是其中有一个地方,我在学习的时候觉得可以 ...