大纲
Testing 的第一个切入点:单元测试。
本篇文章将针对单元测试进行简介,主要内容包含了5W:
- Why
- What
- Where
- Who
- When
而How 的部分,属于实现部分,将于下一篇文章介绍工具与简单的范例。
最后会提到测试用例所代表的意义与其重要性。
前言
单元测试,是开发人员最该写的测试程序,却也是最容易被忽略的测试。
大家常碰到的测试相关问题是:
- 往往一堆人写测试程序时,自以为是在写单元测试,却压根就不是单元测试,而是集成测试。
- 生产代码是我写的,如果测试程序也是我写,那有什么意义?所以应该给QA/QE 来写才能测出盲点。
- 我程序都写完了,跑起来也都对,这时写测试程序一点意义都没有。
- 测试程序要跑好久。
- 没有测试环境,要怎么写测试。
看完这几篇单元测试的相关文章后,希望大家可以获得一些想法,解决这些问题。
Why
先举几个在开发上常见的问题:
- 怎么让UI, Service, Data Access 平行开发?
- 要到真实环境方能测试程序无误
- 页面发生错误,到底是谁错了?
- 交付的程序,到底测过哪些东西了?
- 我改了这支程序,会不会害别的程序挂掉?
这些问题,可以有哪些Unit Test 相关的方式来解决:
- Unit Test 中使用stub/mock object,达到关注点分离
- Unit Test 使用stub/mock object 来模拟外部回传的数据
- 把input 值当做test case,跑一次Unit Test
- 交付的程序,包括Unit Test 程序
- 改完程序就跑一次Unit Test 吧
总而言之,没有被测试涵盖到的程序,即使它可能是对的,也没人敢拍胸脯保证。而有了测试用例来辅助说明与保护,至少可以拍胸脯保证,在这样的测试用例下,这个对象的设定,肯定如同预期般执行。
而单元测试可以提供回归测试的保护,在每一次异动完程式,可以单键执行就知道是否破坏了原本对对象行为的预期。
单元测试可以透过一些辅助设计,来达到与外部环境、服务、相依隔绝,而仅测试该物件本身的逻辑,以及与外部的互动是否符合预期。
造成问题的测试案例,往往是最珍贵的,因为最具代表性,也最具价值。因为它提供了我们修正bug的方向以及指标。而针对发生问题的测试案例,来执行单元测试,马上就可以知道是否是该对象的内部问题。
最后,单元测试由于具备与外界服务、相依隔绝的特性,所以可以帮助撰写实际的对象时,具有可测试性、低耦合性,彼此之间只相依于抽象或接口。进而通过IoC 的设计,让我们可以做到关注点分离,让开发各个对象的developer,可以透过接口来沟通,不相依于彼此实现,就能平行开发。
What
Unit Test 的定义与基本准则,如下图所示:
- 一个测试案例只测一种方法
- 最小的测试单位
- 不与外部(包括项目、数据库、网络、服务、对象、类型)直接相依
- 不具备逻辑
- 测试案例之间相依性为零
Unit Test的特性,一个字:FIRST。如下图所示:
- Fast:快速。
- Independent:独立。
- Repeatable:可重复。
- Self-Validating:可反应验证结果。单元测试不论成功或失败,都应该要从测试的reporting 直接了解其意义或失败原因。
- Timely:及时。单元测试应该恰好在使其通过的production code 之前撰写。
即:优良的单元测试具有以下的特点:简称为 A-TRIP。
- 自动性(Automatic)
- 完备性(Thorough)
- 可重复性(Repeatable)
- 独立性(Independent)
- 专业性(Professional)
Where
单元测试的覆盖范围,以定义来说,单元测试是最小的测试单位,在面向对象中,就是测试一个方法。而方法一定会在某个对象上(即使是静态方法,也是在类型对象上)。
所以,单元测试通常就只关注在测试的目标对象上,而不管目标对象以外的东西,例如:目标对象所相依的实体对象、相依服务、相依资源、相依环境等等...
单元测试,简单的说,就是用来模拟外部如何使用这个目标对象,或是如何与这个目标对象互动。所以我们所撰写的单元测试程序,就是模拟与目标对象互动的程序。测试案例,就是该互动下的情境。接着验证物件的行为是否符合我们预期。
因此,单元测试程式,既然是模拟外部如何使用目标物件,所以也只会针对目标对象对外开放的方法。
而基本上,单元测试透过哪些方式去验证对象的行为符合预期呢?简单来说,有三种:
- 验证目标对象的回传值,如下图所示:
- 验证目标对象的状态改变,如下图所示:
- 验证目标对象与外部相依接口的互动方式,如下图所示:
Who
单元测试该由谁来撰写,就如同前言所说,最应该撰写的是developer,而非QA/QE。
就如Where段落所说,单元测试简单的说,是我们在设计对象的时候,预期外部该如何使用这个对象,进而衍生出对象该提供什么样的功能、具备什么样的行为。正因为对象的设计人、使用人,都是developer,所以单元测试的程式,当然由developer来设计,最为妥当。尤其由用的人来写,最为精准。
归纳几个基本要点:
- 想要达到什么需求,就是测试案例。而对象的设计,只是为了满足需求,需求即测试案例。即生产代码只为了满足测试程序上的测试案例。
- 设计对象的人员,才能知道对象该怎么给外面使用。
- 由外部使用对象的角度来设计测试案例。
When
撰写单元测试的时机点,简单??分成三个:
- 外部需要使用对象,并对其执行结果有所预期时( developing )
- feature的异动时( modifying )
- 出现非预期执行结果时( bug fixing )
想清楚,外部的需求是什么,才能设计出符合需求的对象。
当需求异动时,自然需要针对新的需求,来设计新的测试程序,因为这样才能驱使目标物件行为的改变。
当出现非预期的执行结果时,通常代表目标物件有着非预期的行为发生,有可能是当初测试案例不足,所以要增加我们的「预期」。
也有可能是当初预期的结果就错了,那其实就可以当作是第二点,需求的异动。(当然对使用端来说,还是属于bug,但对对象设计来说,测试案例方向就错了)
Test Cases的意义
大家买过3C产品或电器吧,基本上拿到一个东西,我们都会先看使用说明书。
大家肯定也写过一堆「系统分析书」、「代码规格书」、「SA/SD 文件」等等...但这些文件,跟最后线上的代码,究竟有多少是相同的呢?文件越详细,代表后面修改的effort 越大。
因为软件设计,本来就是个需求频繁变动的过程,往往大家只想「冻结需求」,却很常因为「冻结需求」搞到作出来的系统难用,因为不符合使用者需求。
我们期望的是,每一次的需求异动,都是软??件进化的动力,每一次的异动,都是品质的累积,以及更符合使用者的需求。
而文件呢?只有一开始分析、设计爽的,因为代码写下去,跟文件搭不搭的起来,只有三个人知道,一个已经离职了,一个是我,另一个我不能说。
鲜少会有文件跟着代码一直进行更新的。
但文件却又是辅助了解与说明很重要的东西,那怎么办?很简单,会一直活着的,就只有代码。要验证代码是否符合我们预期,最简单的方式,就是用代码验证它的行为,一翻两瞪眼,现在的物件究竟满足了那些功能,哪些情境下可以跑出预期结果,测试案例一目了然。
所以,测试案例的意义与价值是什么?
- 可自动执行、马上执行、快速执行的对象使用说明书,不会有过期或漏了更新的问题。
- 不管什么情况发生,不管在什么环境底下,都能确保其执行结果如同预期。
代码即文件,高兴什么时候产生文件,就什么时候产生,保证即时、可运作、童叟无欺。测试案例上面有的,肯定work,而测试案例上面没有的,不一定会错,但不打包票。
小结
一句话总结:「Working software is based on working test cases」。
Working software 是TDD 的整个骨架,也是user 最需要的东西。
备注:这个系列是我毕业后时隔一年重新开始进入开发行业后对大拿们的博文摘要整理进行学习对自我的各个欠缺的方面进行充电记录博客的过程,非原创,特此感谢91,小朱等前辈