单元测试已是软件工程师必备的技能,但在我的经验中,有些人写的单元测试实际上却没测到重点,而且还容易因为重构而导致测试失败,可说是为了测试而测试。这样的测试不仅不会带来好处,反而还使专项更不稳健,因此遵循测试的Best Practice是很重要的。本文将介绍Java Unit Test最佳实践与案例,并提出许多人遭遇的盲点。

想像一下这样的场景:

新人小A的第一个任务是新增一个简单的功能,也许只需要十几行代码。但是当他完成后,自动测试系统却回报了大量的测试失败。

小A很难去理解这些测试的内容,以及为什么会失败,因为他这次写的小功能看起来和测试没有关系。本应很快完成的工作却花了小A好几天的时间。扼杀了他的工作效率,也挫伤了他的士气。

大家都知道单元测试有许多好处,前辈们也很负责的将产品加入了许多单元测试。但不幸的是,他们写的这些单元测试却产生了反效果:

测试结果不准确:这次的更改并没有加入真正的 bug,但是测试却失败了。

测试意义不清楚:小A很难确定哪出了问题、如何修复,以及这些测试最初应该做什么。

这种情况经常发生,那么如何避免呢?优秀的单元测试都有这几个特点:

准确: 有bug时就要尽可能抓出来;没有bug就不应该亮红灯。

维护成本低: 测试一旦写好后就不会再改,除非产品需求异动。

执行速度快: 能快速得到反馈,开发人员也才能快速的做出对应。

测试结果直观: 开发人员才能知道哪一行出了什么问题,并最小化问题的范围。

拥有这些特点,就可以算是优秀的单元测试了!这也是优秀的开发者们应努力追求的目标。但要写出优秀的单元测试并不容易,我将介绍几个手段来更靠近这个目标,但前提是要提升产品代码的可测试性

优秀单元测试的设计原则

情境式的结构

尽量以Arrange, Act, Assert 或 Given, When, Then 的 pattern去写单元测试。通过这样的pattern让测试案例比较能表达某情境下的一种行为或结果,会比较贴近使用者,毕竟单元测试就是在模拟使用者如何使用该产品代码。

一个测试案例只验证一个行为

如同好的快筛应只能筛一种病,一个测试案例也应只验证一种行为;如果不是这个测试案例要检验的行为,就不应该把它们写在一起,而是把它们拆分成多个测试案例。

测试案例之间无相依性

测试案例之间应该要各自独立,也不能有先后顺序的相依。如果测试案例相依,万一其中一个发生测试失败时,容易让其他测试案例一起失败,火烧连环船、一发不可收拾,无法快速厘清问题以及很难发现问题的根源。

测试案例的命名尽量清楚、口语化

命名是一件很高深的学问,对于母语非英语的我们更是难以做出清楚的命名。不过幸好Junit 5 之后就可以透过 @DisplayName 来命名中文的测试案例,而且它也会被输出到测试报告中,相当实用:

@Test@DisplayName("我的测试")void my_test() { // ... }

测试案例不具备逻辑运算

如果在测试案例中加入逻辑运算,例如if, else, 加减乘除等等运算,甚至回圈,会让测试变得更复杂。试问:万一测试失败,到底是产品程序出问题?还是测试本身的问题?这是一个在测试中加入逻辑的不良示范:

@Testpublic void shouldNavigateToAlbumsPage() {String baseUrl = "http://photos.google.com/";Navigator nav = new Navigator(baseUrl);nav.goToAlbumPage();    assertThat(nav.getCurrentUrl()).isEqualTo(baseUrl + "/albums"); // 这个结果会多一个 slash }

这个例子用字串 + 运算把bug也埋进来了。测试案例就是应该清楚直观,尽量将预期结果直接hard code清楚地写出来,而不要用运算的。

进行相依验证时,只验证有side-effect的行为

实际操作上我们常用Mockito.verify来验证待测物件 (SUT) 与相依物件 (DOC) 的互动。例如有一个grant user程序的单元测试如下:

@Test public void grant_user_permission() {   // ... arrange, act ...verify(mockPermissionDatabase, times(1)).addPermission(FAKE_USER, USER_ACCESS);verify(mockPermissionDatabase, times(1)).getPermission(FAKE_USER);}

其实getPermission是不用verify的。我们应该要想清楚这个测试在验证什么?什么才是我们最关心的?通常就是这个 method有没有side-effect,有side-effect的行为我们才需要verify。这是个取舍(trade-off),如果程序每个路径都去 verify,确实最有可能会抓到bug、单元测试保护力最高,但这样却更容易导致让测试变得太敏感,就像刚刚小A的例子一样,就连重构也可能导致测试失败,阻碍了开发人员做重构的意愿。因此比较好的做法是只verify有side-effect 的行为。

验证时,不过度指定 (over specification)

如果我们验证的时候过度指定,就会让测试程序变得很敏感,一点风吹草动就坏了,也容易出现不准确的问题。例如有一个说早安的程序,测试如下:

@Test public void display_greeting_render_userName() {when(mockUserService.getUserName()).thenReturn("Fake User");userGreeter.displayGreeting(); verify(userPrompt, times(1)).setText("Fake User", "Good morning!", "Version 2.1");verify(userPrompt, times(1)).setIcon(IMAGE_SUNSHINE);}

这个测试案例所关心的事情应该是userName,如同它的本身命名一样。但是它verify太多不相干的东西,如果setText其它两个参数一但改变,就会亮起红灯。比较好的做法是先想清楚到底要测什么?将我们要测的行为想好,一个测试案例就只关注一件事:

@Test public void displayGreeting_renderUserName() {    when(mockUserService.getUserName()).thenReturn("Fake User");userGreeter.displayGreeting(); // 只验证我们真正在意的第1件事: userNameverify(userPrompter).setText(eq("Fake User"), any(), any());}@Test public void displayGreeting_timeIsMorning_useMorningSettings() {setTimeOfDay(TIME_MORNING);userGreeter.displayGreeting(); // 只验证我们真正在意的第2件事: iconverify(userPrompt).setIcon(IMAGE_SUNSHINE);}

这样做的好处是:

如果是第一个测试失败,我就可以明确知道,程序没有把名字写对。

如果是第二个测试失败,我就可以明确知道,程序可能把图案设错。

不过度依赖mocking framework

过度依赖mocking framework是很多人会犯的错,因为大家都想把单元测试写出来,所以穷尽了各式各样的mock技巧,但如果mock越多,越会让测试结果与事实结果越背离。例如有一个刷卡交易的程序,验证信用卡是否有被扣款的单元测试如下:

@Test public void credit_card_is_charged() {paymentProcessor =new PaymentProcessor(mockCreditCardServer, mockTransactionProcessor);    when(mockCreditCardServer.isServerAvailable()).thenReturn(true);when(mockTransactionProcessor.beginTransaction()).thenReturn(transaction);when(mockCreditCardServer.initTransaction(transaction)).thenReturn(true);when(mockCreditCardServer.pay(transaction, creditCard, 500)).thenReturn(false);when(mockTransactionProcessor.endTransaction()).thenReturn(true);paymentProcessor.processPayment(creditCard, Money.dollars(500));verify(mockCreditCardServer, times(1)).pay(transaction, creditCard, 500);}

这个例子过度依赖mocking framework了。测试不仅变得不直观又复杂,也暴露了待测物件的processPayment许多细节,此外,太多假的行为 (stubbing) 导致我们根本不知道 transaction有没有真正成功,即测试结果可能不准确。这样看起来好像有在测,但测试实际给予我们的信心却很低,讲白了就是太假了,像极了为了测试而测试。因此这个测试案例可能无法测出真实的问题。

解决办法之一就是该将它改成整合测试,不要mock,将这段程序放到真实的测试环境中做测试。虽然整合测试的成本较高,但结果也会比较贴近真实、有价值,尤其是这个案例与钱有关,所以需要更严谨、多方面的测试。

尽可能验证回传值

如同前面几个例子,用verify作为单元测试的验证有很多需要注意的细节,过多的verify也可能暴露待测物件过多的实操细节,增加了重构让测试失败的风险,进而降低测试案例的维护性,所以尽量测那些有回传值的行为,例如能用assertEquals, assertTrue等方式验证。毕竟有回传值,一翻两瞪眼,是非分明。

结语

信任,是单元测试的基石。如果单元测试写得不好,结果不准、时好时坏,久而久之大家都不会相信这个测试,甚至最后把它砍了,造成白忙一场、浪费大家时间。要写出优秀的单元测试并不容易,本文提供了一些做法给大家参考,希望能让大家将单元测试写得更好。

最后: 如果你平时有很多问题想要解决,你的测试职业规划也需要一点光亮,你也想跟着大家一起分享探讨,我给你推荐一个 「软件测试学习交流群:746506216」 你缺的知识这里有,你少的技能这里有,你要的大牛也在这里……


资源分享【这份资料必须领取~】

下方这份完整的软件测试视频学习教程已经上传CSDN官方认证的二维码,朋友们如果需要可以自行免费领取 【保证100%免费】

如何写出优秀的单元测试相关推荐

  1. 为什么好的程序员会写出糟糕的单元测试?

    恭喜你!在写了无数行代码之后你终于可以买一套海景别墅了.你雇了世界著名的摩天大楼建筑师 Peter Keating,他向你保证他设计的海景别墅是最好的. 几个月后你终于迎来了剪彩的时刻.新房子是一栋人 ...

  2. 外贸人如何写出优秀的开发信?附详细思路

    如何写出优秀的开发信? 最近做出口生意的客户都在抱怨,开发信的回复率越来越低,其实原因有很多,有时候并非自己的能力实在很欠缺.原因总结如图: 第一:市场不景气 这个就是就属于客观因素了,这也许跟整体的 ...

  3. 如何才能写出优秀作文?猿辅导:生活的观察与感受非常重要

    猿辅导发现,无论是小学还是中学,作文都应该是不少家长跟孩子心里的"老大难".二三年级的孩子,平时能快速做完作业,一到写作文就跟挤牙膏一样,拖拖拉拉,半个小时憋不出一句话.五六年级的 ...

  4. 黑马程序员教你如何写出优秀的前端工程师简历

    对于一名想找工作的前端开发工程师而言,简历直接关系到面试概率甚至薪资水平,其重要性已不用多说.在HR快速筛选简历的情况下,你的简历要脱颖而出,就得在短时间内将自己的亮点展示给招聘方.具体怎么做?黑马程 ...

  5. 软文营销常用的方式有哪些?如何写出优秀的软文

    如今硬广的效果越来越弱,软文营销逐渐吃香,相信不少人都对软文营销手法感兴趣,今天就给大家讲讲软文营销常用的方式有哪些. 一.故事式 顾名思义,就是通过讲述一个完整的故事,把产品信息隐蔽的贯穿在故事里, ...

  6. 如何写出优秀的技术文档?

    大家好,我是小枣君. 鲜枣课堂自从2017年5月开始正式创立,迄今已有3年多的时间.这一期间,我们的内容一直都坚持以技术类科普文章为主,输出了大约400多篇原创.其中绝大部分,都是我写的. 我的想法比 ...

  7. 设计模式学习之简单聊聊如何写出优秀的代码

    前言: 到底什么样的代码是优秀的代码?这个恐怕是仁者见仁.智者见智的问题.一个程序员随着能力的提升和视野的开拓不同的阶段看待这个问题会有不一样的答案.不过常见的一些评判的标准,像可维护性,可扩展性.可 ...

  8. 北大陈平原教授:写出优秀的学术论文,“小题大做”是关键

    文章 | 陈平原(北京大学中文系教授) 来源 | 中华读书报,管理学季刊整理 中华读书报:您在<中国小说叙事模式的转变>中特别提到"小题大做",这对写论文有什么直接的好 ...

  9. 怎样写出优秀的的研究计划 (Research Proposal) ?

    一.研究计划的架构 要写好一个研究计划就要先明确研究计划中应该包括什么. 一句话讲清楚什么是研究计划,研究计划书描述了你将调查什么.为什么你的研究问题很重要,以及你将如何进行研究这个问题. 不同领域的 ...

最新文章

  1. 【swjtu】数据结构实验2_中缀表达式的求值算法
  2. MAC OSX10.9.2上搭建Apache,php
  3. 读书笔记:做人不要太老实读后感
  4. java new 删除吗,java泛型对象初始化-java泛型对象会实例化吗T t=new T()
  5. 【设计模式】单一职责原则
  6. 网格合并之后物体的位置改变了_基于网格映射对自动驾驶环境信息表示方法
  7. 我这几年呆的这几个公司
  8. [转载]Hadoop 2.X 日志文件和MapReduce的log文件研究心得
  9. 白话java_白话Java
  10. python游戏编程实战教程_关于游戏编程的详细介绍
  11. matlab小波包分析,小波分析及小波包分析
  12. 2018会考计算机知识点,2018高中地理会考知识点总结:地理信息技术
  13. 基于业务流程管理框架的企业敏捷性研究
  14. 蛤蟆 Oracle,19.蛤蟆的Oracle杂记——数据字典dba_views
  15. OpenCV-分水岭算法
  16. CS231n assignment1 KNN部分用到的函数
  17. NEON intrinsics 函数模式介绍
  18. 企业数字化转型:聊聊数据思维!
  19. jOOQ-将两个表的连接提取到相应的POJO中
  20. Baxter学习笔记1-机器人软硬件配置篇

热门文章

  1. 36、有效的数独 | 算法(leetode,附思维导图 + 全部解法)300题
  2. 简单的自动化测试模型(python+selenium)
  3. CPU/GPU/GPGPU简介
  4. 计算机发展的几个重要事件,15件在计算机发展史中具有里程碑意义的事件
  5. 玩转Pandas函数
  6. (刘二大人)PyTorch深度学习实践-卷积网络(Advance)
  7. 最全阿里面试题:已拿offer,阿里P8岗位完整阿里技术面试题目,这些面试题你能答出多少
  8. 懂23种语言 2019年上市 宝马的AI助理有哪些不同!
  9. ubuntu11.10 华为无线上网卡e303s
  10. JavaScript进阶(二)