原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (一)什么是单元测试

2020 年 05 月 07 日 阅读数: 13
原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (一)什么是单元测试

If builders built buildings the way programmers wrote programs, then the first woodpecker that came along would destroy civilization.(如果建筑业者用程序员写程序的方式盖楼造桥,那么第一只飞来驻足的啄木鸟就足以毁灭人类文明)

—Weinberg's second law(温伯格第二定律)

序言

当前我国的软件行业普遍缺乏专业性

杰拉尔德·温伯格(Gerald M. Weinberg)是软件开发行业的老前辈,有史以来最伟大的程序员之一。如果说Martin Fowler、Kent Beck、Robert C. Martin等人是我们行业的贤人,温伯格就是行业的哲人。他是杰出的程序员、专业作家和思想家,著有30多本书籍和数以百计的论文,我有幸读过其中的几本,包括《你的灯亮着吗?》、《咨询的奥秘》、《理解专业程序员》、《成为技术领导者》、《程序开发心理学》和《系统化思维导论》等。其中《你的灯亮着吗?》最为家喻户晓,而《系统化思维导论》最启迪智慧(个人观点,不喜勿喷)。

温伯格上面这段话引起我心中强烈的共鸣。笔者在软件开发行业中摸爬滚打,至今也有二十来年了。这么多年以来,最强烈的感觉就是:我们行业中绝大多数的开发者、组织和产品,都严重缺乏专业性。既缺乏专业能力,更缺乏专业意识。我们的产品内部质量堪忧,而且自动化测试覆盖远远不足。与会计、建筑、医学等行业相比,软件开发的专业性现状还差的远。温伯格在《理解专业程序员》一书中说过,在欧洲,一个侍者可能要经过10年,甚至20年的训练,才能获准在一个一流饭馆服务。而我们的程序员只要有5年的工作经验,就敢号称行业专家了。

专业性从编写单元测试开始

要成为专业程序员,请从编写单元测试开始。通过单元测试,证明你写的每一个类的每一个有意义的公开方法在各种正常和异常的条件下都遵守它的API契约,符合你的设计意图,从软件最底层的实现代码开始证明你写的程序的正确性和可靠性。毕竟,如果你都不能证明底层代码的正确性和可靠性,又怎敢觍颜声称自己是“专业程序员”?

TDD:先写好单元测试,再写实现代码通过测试

是否采用测试驱动开发(简称TDD),是专业程序员与业余程序员的分水岭。

TDD是多个敏捷方法论力推的最佳实践之一。如果只能保留一个敏捷最佳实践,我必然是保留TDD。如果可以保留两个,那就再加上持续集成。

采用与不采用TDD,代码质量和设计质量有天壤之别。

不采用TDD的,也有写的好的代码(但很罕见)。采用TDD的,基本都是好代码。长期坚持TDD,程序员的的代码和设计会越来越好。你会感觉写代码越来越像写诗,而不是像搬砖;你自己成了诗人,而不再是悲催的码农。你不再需要加班加点寻找和改正Bug;你不再需要担心在一个地方修改代码会导致在意想不到的地方冒出新的几个Bug,即使有,你也可以及时发现、准确定位Bug并加以改正;你晚上会睡得比较安稳,不用担心领导或客户半夜的夺命连环call;你对自我形象感觉满意,因为在同事、领导和客户面前书里了专业、可靠的形象。

单元测试工具集:JUnit、AssertJ和Mockito

JUnit、AssertJ和Mockito是单元测试三剑客。JUnit用于编写和执行测试,AssertJ用于编写断言,而Mockito用于编写测试替身,以隔离被测类的协作者,使得测试范围真正局限于代码“单元”,而不至于变成集成测试。

本教程将讲述如何利用这三大工具编写单元测试和进行测试驱动开发。我力图达到以下的目标:

  • 遵循80/20原则,重点剖析三大工具中最重要、最常用的功能。

  • 系统。不是散漫地介绍一个个知识点,而是以“知识树”的方式去讲述。

  • 简单。不讲废话。直接以代码方式呈现测试范例。我深信一千克纯金比十吨金矿石更有价值。

  • 直接。所有的东西尽可能以列清单一二三四的方式指导学员去实现。

现在,让我们一起迈出成为专业程序员的第一步,从单元测试和TDD开始。

什么是单元测试

单元测试用于测试一个独立的工作单元的执行结果是否符合预期。

一个工作单元就是一项任务,不直接依赖于其他任何任务的完成。在面向对象的编程语言例如Java中,一个工作单元通常是(但不总是)一个类中的一个非private的方法。因此,单元测试是最细粒度的测试。相比之下,集成测试和验收测试检查的是各个组件如何交互,是更粗粒度的测试。

单元测试通常关注的是一个方法是否遵循了它的API契约中的条款。通常而言,在单元测试中,我们会依照下面的步骤进行测试:

  1. 创建被测对象;

  2. 将测对象的设置到一个初始状态,包括注入被测对象的所依赖的协作对象的实例;

  3. 使用指定的参数集调用被测对象的方法,执行测试;

  4. 断言方法执行的结果符合预期。

执行结果通常有以下三种形式:

  • 对于有返回值的方法,断言方法的返回值符合预期

  • 对于没有返回值的方法,断言它产生的副作用(例如:修改了对象的内部状态,调用了协作对象)符合预期。

  • 抛出了我们预期的异常。例如对于ATM机应用,当测试中的取款金额超出了账户的可用余额时,断言取款方法调用会抛出BalanceInsufficientException异常。或者当账户被冻结时,断言调用取款方法会抛出AccountLockedException异常。

单元测试用于检验被测类中的一个工作单元(通常是一个非private的方法)在各种正常和异常条件下被调用时的响应(返回值、副作用、抛出异常)都符合设计者的预期。

例如,当我们设计银行账户类Account的取款方法withdraw(double amount)时,我们的设计意图包括下面几个要点:

  • 当withdraw()方法被调用时,如果Account的状态正常且当前可用余额不小于取款金额Amount,则方法应当正常返回。当前余额更新为原余额-取款金额(产生了副作用——修改了Account对象的内部状态)。

  • 如果Account的状态正常且取款金额大于账户的当前可用余额,方法应该调用失败并抛出BalanceInsufficientException异常。Account的当前可用余额不变。

  • 如果账户被冻结,无论余额是否足够,调用withdraw()方法都应该失败并抛出AccountLockedException异常。Account的当前可用余额不变。

以上就是要设计的Account类的withdraw()方法的API契约。我们应该针对上面几种情况编写一组单元测试,以保证在以上3种状况下调用withdraw()方法的响应都符合设计意图。如果上面的单元测试都通过了,我们的系统就可以保证:

  • 账户被冻结时候取款不会成功。

  • 账户未被冻结且余额足够时可以正常取款。

  • 余额不足时取款失败。

  • 如果取款成功,账户的余额必须相应减少。

  • 如果取款失败,不管是由于余额不足还是账户被冻结,账户的当前余额都不变。

这样的系统就是足够安全的。客户不会由于不完善的代码导致金钱和名誉损失。

专业程序员必须保证他写的每一行代码都是正确的、可靠的。单元测试就是保证代码正确性和可靠性的最重要的一步。

用户头像

编程道与术

关注

领域驱动设计(DDD)的早期践行者和布道者 2020.04.28 加入

杨宇Yangyu于2020年创立编程道与术,致力于研究领域分析与建模、测试驱动开发、架构设计、自动化构建和持续集成、敏捷开发方法论、微服务、云计算等顶尖技术领域。 了解更多公众号:编程道与术

评论

发布
暂无评论
原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (一)什么是单元测试