《单元测试的艺术》学习笔记

引言

单元测试一直以来都是开发过程极容易被忽视的一环,我觉得测试是代码高质量、可维护的保证,对于自身开发而已、又或是他人接手代码而言,优秀的单元测试总能带来便捷;尽管单元测试会导致前期开发需要花费更多的时间来进行测试单元的编写,但我相信,对于长久而言,好处总归是更多的。

工作之余,抽空看了《单元测试的艺术(第2版)Roy Osherove 著》,便记录下了一些好的点,用于之后复盘学习。原书中还有大量C#在单元测试领域的工具使用指南,由于本人并非使用C#进行日常开发,所以这部分我也就简单略过了。


单元测试基础

要谈论单元测试,首先还是要谈谈集成测试

  • 集成测试是对一个工作单元进行的测试,这个测试对被测试的工作单元没有完全的控制 ,并使用该单元的一个或多个真实依赖物,例如时间、网络、数据库、线程或随机数产生器。

  • 单元测试是自动化的代码,调用被测试的工作单元,之后对这个单元的单个最终结果的某些假设进行检验,只要产品代码不发生改变,单元测试的结果是稳定的。

一个挺形象的例子,可以说明两者的不同以及单元测试的一些要素。假设有一个灌溉系统,你对这个系统如何灌溉庭院的树进行了配置:一天浇多少次,每次浇多少水。以下是测试灌溉系统是否正确工作的两种方式

  • 基于状态的集成测试

    让灌溉系统运行12小时,这期间系统应该给树浇了很多次水,这个时间段结束后,检查浇灌的树的情况,土壤是否足够湿润等等。这个测试只要能执行,你就知道灌溉系统是否正常工作。很明显我们也知道这种方法需要花费的时间很长,运行测试设计灌溉系统周边的整个环境。

  • 基于交互的单元测试

    在每个水管末端,安装一个设备,记录什么时间多少水流过。在每天结束的时候检查这个设备记录的次数是否正确,每次的水量是否正确,不用费心检查树的情况。而且最重要的是,其实也不需要树就可以检查这个系统是否正常工作。而且如果想更快完成测试,还可以进一步修改灌溉单元上的系统时钟(相当于一个存根),让系统以为灌溉时间已到,这样也就无需等待就能知道系统是否正常工作。

当然,在这种情况下,单元测试让测试变得更加简单。但有时,进行单元测试很困难或无法达到测试效果时,集成测试是最好的选择。

单元测试的优点有:

  • 避免“偶然引入缺陷”

    在应用程序的生命周期中,代码经常会发生变化,而如果在修改代码之后,你不能(或者是不愿意)对之前所有的功能进行测试,那有可能破坏了某个功能而不知情,这种情况称之为"偶然引入缺陷"。

    在维护旧系统的代码,或是接手他人代码进行更新时,如果不是重新对整个系统的代码架构进行复盘,很难保证说自己的改动一定不会影响到别的代码块功能,也就相当于自己都无法确定修改后的系统是否依旧稳定。而这时候优秀的单元测试相当于是每个代码块的安全网,能保证每一个代码块的功能不被破坏。

  • 测试驱动开发

    TDD的概念就不展开了。

    TDD所带来的一个增量特性就是,通过小步骤的积累得到高质量的最终结果。

    TDD能帮助创建高质量代码和测试,能够促进自身更好的设计代码,前提是需要编写优秀的单元测试

    另一方面,也能帮助你确保测试代码的代码覆盖率尽可能接近100%

一个优秀的单元测试应该做到

  1. 一段自动化的代码,它调用了另一个方法,然后检验关于此方法或类的逻辑行为的某些假设
  2. 用一个自动化测试框架编写
  3. 容易编写
  4. 运行快速
  5. 能被其他任何人重复执行

简而言之,需要做到可读、可维护并且可靠。


做一个单元测试

在真实世界中,应该测试任何包含逻辑代码的方法,不管它看起来多么简单。

测试方法的命名可以三部分:

  1. UnitOfWorkName 被测试的方法或一组方法
  2. Scenario 测试进行的假设条件
  3. ExpectedBehavior 预期的行为

例如对 IsValidLogFileName 方法进行测试时,场景是给方法传入一个有效的文件名,预期行为是方法返回一个 true。我们可以把这个测试方法命名为 IsValidFileName_BadExtension_ReturnFalse() 。

一个单元测试通常包含三个行为:

  1. 准备(Arrange)对象,创建对象,进行必要的设置
  2. 操作(Act)对象
  3. 断言(Assert)某件事情是预期的

使用存根破除依赖

在实际的开发中,要测试的对象依赖于另一个你无法控制(或暂时还未实现)的对象,这个对象可能是web服务、系统时间、线程调度等等。重点是你无法控制这个依赖的对象向你的代码返回什么,此时需要使用存根来破除依赖。

一个例子,把人类送入太空的任务,如何确保宇航员已经做好进入太空的准备,并按需求操作航天飞机控制台的装置呢?

如果是完全的集成测试,就需要将航空飞机真的位于太空,这显然是不行的,无法保证宇航员的操作安全

所以,NASA就建造了全套仿真系统,模拟航天飞机控制台的环境,消除了必须在外太空这个外部依赖。

  • 定义

    存根是系统中存在的依赖项的可控制的替代品

    通过使用存根,可以在测试代码时无需直接处理这个依赖项


使用模拟对象进行交互测试

每一个测试的工作单元可能有三种最终结果:

  1. 返回值
  2. 改变系统状态
  3. 调用第三方对象

对于前两个,我们可以对方法的返回值进行断言,使用存根改变系统状态来实现测试

而对于调用到第三方对象,第三方对象可能不返回任何结果,且该对象不受你的控制,也不存在外部api让你可以检验被测试对象内部是否发生了变化,那如何测试你的对象和第三方对象进行了正确的交互呢?

这个时候就需要用到模拟对象

  • 定义

    模拟对象是系统中的伪对象,它可以验证被测试对象是否按预期的方式调用了这个伪对象,因此导致这个单元测试通过或者失败。

    存根也是一种伪对象,模拟对象与存根的区别很小,最根本的区别就是存根不会导致测试失败,而模拟对象会。

还是刚刚那个宇航员进入太空的例子

  • 存根

    建造全套仿真系统,模拟航天飞机控制台

    这是为了破除宇航员和航天飞机对太空的依赖

  • 模拟对象

    控制台有一个按钮B,需要在先按下按钮A才能按下;此时宇航员直接拿下按钮B,直接的结果是测试失败。这就是一种交互测试。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值