跳转至

05 TDD中的测试(1):状态验证为什么是主要的使用方式?

你好,我是徐昊。从这节课开始我们来正式学习测试驱动开发(Test-Driven Development,TDD)。

通过前面四节课的演示,相信你对测试驱动开发已经有了一定的感性认识。测试作为整个流程的驱动力,无论是在开发还是重构过程中都起到了重要作用。我想你对这一点一定有了比较深刻的印象。那么从今天开始,我们就来学习如何编写测试。

测试的基本结构

无论使用什么样的测试框架,每个测试都由四个依次执行的阶段组成:初始化(SetUp)、执行测试(Exercise)、验证结果(Verify)和复原(Teardown)。如下图所示:

需要说明一下的是,测试上下文(Test Context)在很多文献中被称作测试夹具(Test Fixture)。

夹具是个隐喻,是木工或者其他制造过程中,用以固定待加工工件的器具(上图中,棕色部分看起来是不是“夹”住了待测系统)。当然,这种拿一个生僻概念来隐喻另一个生僻概念的操作,也是很迷了。我要不是因为做吉他学习了木工,也不明白为什么要叫Fixture。抛开这个隐喻,直接称作测试上下文其实就简单易懂多了。

再多说一句,如果使用Fit系自动化测试工具(Fit、Fitness,甚至concordion、selenium),Fixture则表示驱动待测系统的交互接口,也叫Driver。这也是为啥,Selenium后来改叫WebDriver的原因。

言归正传,这四个阶段的主要作用是:

  • 初始化。主要是设置测试上下文,从而使待测系统(System Under Test)处于可测试的状态。例如,对于需要操作数据库的后台系统,测试上下文包含了已经灌注测试数据的测试用数据库,并将其与待测系统连接。
  • 执行测试。就是按照测试脚本的描述与待测系统互动。例如,按照功能描述,通过API对系统进行相应的操作。
  • 验证结果。就是验证待测系统是否处于我们期待的状态中。例如,经过测试,数据库中的业务数据是否发生了期待中的改变。
  • 复原。就是将测试上下文、待测系统复原回测试之前的状态,或者消除测试对于待测系统的副作用。例如,删除测试数据中的数据(通常是通过事务回滚)。

接下来,看一看前四节课演示的测试中,这四步都是如何实现的:

需要说明的是,在参数解析这个例子中,待测系统是命令行参数解析的Java类库(Java Library),并不涉及数据库、消息中间件、三方服务等进程外组件,也没有进程内三方组件依赖(比如OSGi Runtime、Servlet容器),因而测试上下文相对简单。而对于存在进程外组件或进程内三方组件依赖的场景,测试上下文的设置将直接影响编写测试的难度,以及维护测试的成本

在测试的四个步骤中,验证结果是最核心的一步,也是最核心的技术。验证结果有两种方式:状态验证(State Verification)和行为验证(Behavior Verification)。

状态验证

状态验证是指在与待测系统交互后,通过比对测试上下文与待测系统的状态变化,判断待测系统是否满足需求的验证方式。在前四节课的演示中,全部测试都采用了状态验证的方式:

状态验证是一种黑盒验证,它将测试上下文与待测系统当作一个整体。当待测系统不存在内部状态,而通过作用于依赖组件(Depended On Component)达成功能的时候,我们会从依赖组件中获取状态,以验证待测系统。如下图所示:

让我们回忆一下开篇词中“测试应用”的例子。其中StudentRepository自身不具有内部状态,而是通过EntityManager操纵数据库来完成功能。如果以TDD的方式来构造对于StudentRepository的测试,就会是这样的:

状态验证的一个难点是复原测试上下文,消除因执行测试造成的状态累积。如果待测系统是类库,一般问题不大,重新构造新的对象实例就行了。

而对于测试环境中存在进程外组件的情况,问题就要复杂一些了。在这种情况下,增量状态验证(Delta Verification)是一种有效的手段:

当然,在这个例子里,在每一个测试复原时,都使用了“drop-and-create”来清除数据库中的数据,从而消除状态累积。但如果因为种种原因(比如测试数据量很大),使得每次清除测试数据都变得不现实时,就可以使用增量状态验证来降低状态累积的影响。

小结

今天我们介绍了测试的基本结构,这个结构也叫四阶段测试(Four-Phase Test)。四阶段测试作为测试的基本模型,存在于各种测试框架中。而对于四阶段不同的组织方式,也构成了不同的测试编写风格,比如RSpec、ScalaTest等框架提供的BDD风格。不过无论是哪种风格,都不影响我们按照四阶段来理解测试的执行。

在四阶段中,验证结果是最核心的一步。它主要有状态验证和行为验证两种方式,其中状态验证需要大量使用断言方法(Assertation Method)来判断状态。同样,每个框架都提供了大量的断言方法,这里请参看具体框架的文档。

虽然状态验证是TDD中最重要的验证方法,但在某些情况下,我们仍然需要使用另一种验证方式——行为验证。这个我们将在下节课进行详细讲解。

思考题

为什么TDD中主要使用状态验证来验证测试的结果?

编辑来信

TDD是一项技能,唯有动手实操、反复练习,才能有所小成。为了帮助你更快地进步,徐昊老师特发起了“TDD专栏首发·代码评点”活动。

在第一个实战项目结束后,我们会根据你提交的学习反馈,手动选出其中几位进行代码评点与解疑答惑。而评点的详细内容我们也将制成加餐,展示在专栏里,供其他同学学习与参考。

划重点!如果学完第1-10讲再写反馈,将会大大提高你入选的机会!另,此次收集时间截至4月3日零点。所以非常希望你能跟上我们的更新进度,多动手实操,并记录学习体会。

最后,希望我们都能好好学习,更上层楼!

精选留言(13)
  • humor 👍(2) 💬(2)

    如果SUT使用了外部系统,并且外部系统比较慢,会不会拖慢测试执行的速度,进一步拖慢编码的速度呢

    2022-04-10

  • jimson 👍(0) 💬(1)

    为什么 TDD 中主要使用状态验证来验证测试的结果? 因为我们使用的是OOP, 操作的大多数是数据的改变。

    2022-04-22

  • humor 👍(0) 💬(1)

    因为在TDD刚开始的时候还没有代码实现呢,只能验证最终的结果(状态)

    2022-04-10

  • asusual 👍(5) 💬(0)

    为什么 TDD 中主要使用状态验证来验证测试的结果? TDD的主要核心在于tasking, tasking后的任务列表一定是一个具体且可验证的的行为。 e.g 用户可以使用 用户名: some-user 用户密码:some-password 成功登录系统 输入确定,输出必然确定 我们只需要关心测试输入与输出(黑盒测试),也就是说不需要关心如何实现 一旦我们切换到行为验证(相当于白盒测试),关注点从验证输入和输出结果转换到验证具体实现行为 假设我们需要切换到另外一种实现行为,单元测试也需要重新修改, 有点像“实现驱动测试”,最终导致变更成本提高

    2022-04-03

  • 汗香 👍(3) 💬(0)

    为什么 TDD 中主要使用状态验证来验证测试的结果? 如asuaual所说,状态验证更像黑盒,不用关心软件的内部结构,内部结构的改变对外部验证来说是透明的,更利于重构

    2022-04-08

  • 常文龙 👍(1) 💬(1)

    在看第一遍的时候比较难理解为什么是状态验证是“黑盒验证”,毕竟示例里都把数据库的数据挖出来看了,还不叫白盒? 但仔细品味,把数据库数据挖出来看,是为了看“没有直接输出的输出”,而不是看内部状态,从这个角度看,的确是黑盒验证。

    2022-09-23

  • aoe 👍(1) 💬(0)

    来自学习群的分享 有哪些状态验证技术推荐吗? - 做各种fake mountainbike,mock,in men do, test container - 目的就是尽量少做行为验证 - **所谓测试策略 就是在保证有效性的同时 尽可能降低测试成本** - 维持测试有效性 有个最小成本 - 小过这个 测试就无效了 或者不足以支撑长期演化(重构) - 所以你没办法一直缩短测试时间 - 这也是我们讲的 始终使用状态验证 防止测试失效 - 在状态验证里 通过stub 偷换fake 降低成本

    2022-03-27

  • jackstraw 👍(0) 💬(0)

    哎呀,这么好的保障课程,咋这么少的人留言呢

    2024-09-12

  • 士心23 👍(0) 💬(0)

    带着数据库跑测试岂不是很慢,对于repository层没想到好的测试方法

    2024-04-28

  • 努力努力再努力 👍(0) 💬(0)

    问题:为什么 TDD 中主要使用状态验证来验证测试的结果? 回答:我觉得是,任何事情的执行,注重的都是最终的结果,状态验证也是如此,忽略测试执行(exercise)的过程,专注于最终测试的结果是否与设想的一致。

    2022-09-14

  • Geek 👍(0) 💬(0)

    请问老师,查询数据库算是状态验证么,我理解也可以说是一个行为。因为数据库也是一种实现,以后可能重构成别的库或者别的表。这样这个case还是会失效

    2022-08-30

  • escray 👍(0) 💬(1)

    终于追到了第五讲,之前用 RSpec 写过 Ruby 的单元测试,但是没有感觉的状态验证和行为验证的具体差别,估计还是代码写的太少的缘故。 这一讲其实是前面几讲中相对轻松的一篇,前面课程的视频里面有不少代码示例,我努力完成了一部分,主要是因为自己对于 Java 实在是不够熟练,练习 TDD 的过程中顺便学习了一个 Java 编程。 同样把提供代码,希望能有帮助 https://github.com/escray/TDDCourse/tree/ch05 有点好奇的是,Java 的这些新特性在日常开发中使用的多么?似乎很多项目还是建立在 Java 1.8 的基础上的,虽然现在已经是 Java 18 了。

    2022-04-02

  • aoe 👍(0) 💬(0)

    推荐一个比 Selenium 更好用的 Selenide(我没用过,但看书上介绍很厉害) 1. 基于 Selenium 的项目 2. 提供了优良的测试编写语法,提高了测试的可读性 3. 它将 WebDriver 和配置隐藏,同时提供了极大的定制空间 4. PhantomJS 是一款无界面浏览器(headless browser),不使用任何 UI 就能工作 摘自《Java 测试驱动开发》2.9 用户界面测试;7.4.3 Selenium 和 Selenide 笔记链接:http://wyyl1.com/post/18/02/

    2022-03-26