xUnit之自动化测试的哲学
作者:ntop 发布时间:[ 2017/6/6 15:15:58 ] 推荐标签:软件测试工具 软件测试
本篇来自 Philosophy of Test Automation 章节,本文讨论的内容是非常有意思的,所谓 “哲学” 指的是在做单元测试的过程中遇到的各种不同的观点。这些观点往往源自一些问题,这也是我们写单元测试时会遇到的,看看别人的困惑和解答对我们很有帮助。
在开始陈述测试原则之前需要先看一下一些测试争执点:
· “Test after” versus “test first”
· Test-by-test versus test all-at-once
· “Outside-in” versus “inside-out”
· Behavior verification versus state verification
· “Fixture designed test-by-test” versus “big fixture design upfront”
注:“Outside-in” versus “inside-out” (applies independently to design and coding)
Test First or Last
是先写业务代码还是先写测试代码?在传统的软件开发中是先写业务代码的,单元测试和一些功能测试都是在软件开发完毕才做的事情,但是在敏捷开发中是先写测试的也是所谓的TDD。其实稍微有点经验的同学都知道,在一个已有的系统上添加单元测试是非常困难的,往往之前的代码并没有考虑到可测试性(“design for testability”),很多设计和依赖问题都会让测试无法进行。
而即使在写代码的时候心中有所考虑,在真实的情况下事后写测试依然困难,如果考量的不够全面遗漏了某些细节同样会造成接下来的测试难以进行。一个解决办法是TDD,事前写测试,这样自然可以让被实现的系统支持测试,可以测试(因为如果系统没有设计的可测试,那么便无法跑现有测试用例,所以这才是TDD的无奈...)。
而且在这个时候也会自然的考虑到依赖的问题,为测试代码想好容易实现的依赖。比如这时候自然不会在代码中直接调用 System.currentTimeMillis()而是通过接口来封装这个实现,为了让对象更容易比较,也会考虑实现自己的 equals() 方法。
Tests or Examples
在TDD中,我们写下的是测试还是示例?这个问题很有意思,往往我们被“TDD”这个名词熏陶久了反而会忽视这个问题:如何能够给一个不存在的系统写测试呢??细想之下这句话确实是有道理的,如果把Test看成是Example那么问题便容易理解了,所谓测试实际上是写一些Example(这些Example是可执行的),然后验证一下这些Example的功能实现输出是否满足需求。
所以有时我们应该认识到实际上 TDD 是 Example Driven DevelopmentD 即 EDD。从这个角度,便可以很好的理解TDD。现在已经出现了很多EDD框架,基于 Ruby 的 RSpec 或者 基于Java 的 JBehave。其实EDD和TDD的思想是一样的,但是通过这个名词的更改可以更好的体现出 “Executable Specification”。
Test-by-Test or Test All-at-Once
这个话题指的是“写一点测试再写一点代码”还是一次写完所有的测试?这是一个很实在的问题,TDD鼓励 “write a test” 然后 “write some code” 来通过测试。这个过程并不是说要在写业务代码之前写完所有的测试,而是交错的写测试和产品代码(控制在一个很细的粒度),开发过程是增量的。。
“Test a bit, code a bit, test a bit more”
另外一种方式是一次写完某个功能的所有测试,这种好处是可有让开发者 “think like a client” 或者 “think like a tester” 从而避免过早的沉溺到 “solution mode” 。TDD鼓励前者,因为增量式的开发更容易定位错误,在某次代码改动导致测试失败的时候,我们可以保证之前的代码是可用的。
考虑上面两种情况会达成一种妥协,我们可以在某个功能开发之前写完所有的测试,但是测试方法是没有实现的,之后再通过增量开发来实现。
Outside-In or Inside-Out
在软件设计中我们经常使用的一种方式是“从外到内”或者“从上到下”的方式来设计软件,这样可以让我们像终“用户”一样来思考软件的功能,枚举需要实现的接口。但是在开发过程中,往往又会采取“从内而外”、“自下而上”的方式来编码,采取这种方式的好处是,不需要为了底层未实现的依赖而烦恼,因为我们开始是从底层开始写的。如果有三个依次依赖的系统A、B、C,A依赖B,B依赖C,那么可以先实现C,再实现B,后实现A,这是典型的自下而上的编码方式。
与之相反的是自上而下的编码方式,先实现A再实现B后C,在这种方式中,每实现一个系统都需要考虑依赖问题,解决办法是使用 TestStubs 或者 Mock Objects 实现一个临时的依赖通过测试,等整个系统完善之后可以考虑再去除这些临时依赖。
State or Behavior Verification
是验证对象的状态还是验证对象的行为?支持验证状态的一派人认为对于SUT只要初始化对象的状态,执行用例,再验证对象的结束状态便足够了,这种方式通常需要SUT提供一些getter方法来获取状态。而对于支持验证对象行为的同学而言,不仅要验证状态还要确定某个被依赖的对象的某个方法是否被调用并返回期望的结果(注:有可能是没有返回结果的),而这类间接的返回结果通常需要借助于某些Mock框架来验证。
对于后者也叫BDD - Behavior Driven Development,这种测试方式通常会大量的使用 Mock Object 和 Test Spies。行为验证可以较好的隔离不同的单元,做到真正的单元测试,但是行为测试会迫使开发同学过多的考虑依赖的内部实现、过多的Mock依赖导致在代码重构时面临困难。
注:没有研究过单元测试的同学可能会难以理解这两个概念,可以参考这篇文章
Fixture Design Upfront or Test-by-Test
是提前写好大而全的Fixture还是一个测试一个Fixture呢?
在传统的软件开发中,会有 “Test Bed” 的概念,它有应用和数据库组成,数据库中会提前写入精心准备的数据来模拟各种可能出现的用例。xUnit的Fixture其实是类似的概念,有时候会给一个或多个TestCase定义一个标准的Fixture让几个TestCase共用它,每次执行TestCase的时候会重建Fixture(比如把Fixture的构建放在setup方法里,这个方法会在每次执行测试用例之前执行),这样可以保证在执行的时候得到的都是一个新的Fixture。
但是这种方式会带来一个问题,很难判断一个TestCase的执行时和这个大而全的Fixture中的那些配置有直接关联。比较敏捷的做法是为每一个测试定义一个小的Texture实现,这样会显得更直观一些。
注:真心不知道如何翻译 Test Fixture,从感官上来说应该指的是一些测试设置,这些设置为接下来的测试准备了环境。Fixture的本意是夹具/固定装置,比如在做手工活的时候需要一个装置来固定木头之类。
结论
不同的人有不同的见解,每种见解都有其相应的理由(想想近公司的架构师给我等草民做CodeReview,一切都以他自己的认识为标准,于是乎要为每个if-else加上注释),其实并没有高低上下之分。上面对各种争论点的描述,在一定程度上有点倾向于支持作者(xUnit Test Pattern一书的作者)的嫌疑,本着先僵化再固化的学习态度,我会遵循作者的测试哲学:
· Write the tests first!
· Tests are examples!
· I usually write tests one at a time, but sometimes I list all the tests I can think of as skeletons upfront.
· Outside-in development helps clarify which tests are needed for the next layer inward.
· I use primarily State Verification but will resort to Behavior Verification when needed to get good code coverage.
· I perform fixture design on a test-by-test basis.
相关推荐
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11