10.1 参数化测试
现在,许多单元测试框支持随时可用的参数化测试(parameterized test)。使用Spock,覆盖10个不同的保险费因子的测试如下所示:
@Unroll("""A #gender driver of #age has a premium factor of #expectedPremiumFactor""") def "Verify premium factor"() { expect: new PremiumRuleEngine().getPremiumFactor(age, gender) == expectedPremiumFactor where: age | gender || expectedPremiumFactor 18 | Gender.MALE || 1.75 23 | Gender.MALE || 1.75 24 | Gender.MALE || 1.0 59 | Gender.MALE || 1.0 60 | Gender.MALE || 1.35 18 | Gender.FEMALE || 1.575 23 | Gender.FEMALE || 1.575 24 | Gender.FEMALE || 0.9 59 | Gender.FEMALE || 0.9 60 | Gender.FEMALE || 1.215 } |
该测试通过将表格扩展为10个单独的测试实例(这是通过@Unroll标注明确的)来实现。如代码所示,这些被注入到测试中的数据可能是基本数据类型和对象,也可能由任意Groovy结构体产生。JUnit等价类更冗长、更繁杂,这就是我为什么把它放到附录里的原因。
NUnit的实现也很优雅。属性TestCase的未命名参数被直接注入到它标注的方法中,而且将方法的返回值与ExpectedResult参数进行了比较。
[TestCase(18,Gender.MALE,ExpectedResult=1.75)] [TestCase(23,Gender.MALE,ExpectedResult=1.75)] [TestCase(24,Gender.MALE,ExpectedResult=1.0)] //... publicdoubleVerifyPremiumFactor(intage,Gendergender) { returnnewPremiumRuleEngine().GetPremiumFactor(age,gender); } |
第12章 测试替身
在第9章"依赖关系"中,我们学习了如何通过将协作者外显并四处传递,使依赖关系清晰化并打破这种依赖。程序元素(program element)--通常是对象--在某种程度上相互依赖是极其自然的,但是这些依赖关系必须是可控的。本章将会再次提到依赖关系,并对其进行更详细的探究。根据在测试中所扮演角色的不同,可以采用不同的方法对依赖关系进行控制。有时,应该忽略协作对象(collaborating objects),虽然这可能不像它听起来那么容易。有时,协作对象太重要了,以至于必须以最详细的审查来对其实施监视。
测试替身(test double) 常用来表示代替协作者的对象。不同的测试替身有不同的任务,从替代协作者并使其返回预定值,到监听每一个对它的调用。本章将介绍五种不同用途的测试替身:桩对象(stub)、伪对象(fake)、模拟对象(mock object)、探针(spy)和哑对象(dummy)。下一章将介绍怎样使用框架来实现一部分测试替身。
12.1 桩对象
当被测试的对象依赖于另一个对象时,最简单、最通用的测试方法如图12.1所示。
它引出了一种堪称经典的测试方法:
[TestMethod] public void CanonicalTest(){ var tested = new TestedObject(new Collaborator()); Assert.AreEqual(?, tested.ComputeSomething()); } |
图12.1 桩对象应用场景
注:1.测试代码调用被测对象。2.被测对象调用它的协作对象。3.协作者执行计算操作并返回一个数值。4.被测对象使用该返回值,并返回一个可以从该返回值推导出的结果。
就像在第9章中所描述的那样,简单地将实现了ICollaborator接口的协作者传入到被测试对象的构造函数中。然而,我们仍无法计算出AreEqual的第一个参数数值应该是多少(注意那个代替了合适参数的问号),原因在于ComputeSomething以如下方式实现:
public int ComputeSomething() { return 42 * collaborator.ComputeAndReturnValue(); } |
这是最简单情况,其中被测对象只是调用了返回一些数值的协作者,然后继续将该数值做某种细化,并返回给调用该对象的测试。根据前面几章的内容,我们知道由协作者提供的数值被称为间接输入。为了使本例简洁,仅将该数值乘以一个常数。
为了控制像这样的依赖关系,需要使用桩对象。使用桩对象背后的原始动机在于控制被测对象的间接输入,因为协作者被注入到了被测对象(tested object)的构造函数中,所以创建一个桩对象是很容易的,需要做的就是返回一个硬编码(hard-coded)的数值。
class CollaboratorStub : ICollaborator { public int ComputeAndReturnValue() { return 10; } } |
现在,我们可以使用桩对象代替真实对象,那么测试用例可被重写为如下的形式:
[TestMethod] public void CanonicalTestWithStub() { var tested = new TestedObject(new CollaboratorStub ()); Assert.AreEqual(420, tested.ComputeSomething()); } |
12.1.1 桩对象的灵活性
让一个桩对象返回单个数值是最简单,但也最不聪明的做法。其他测试迟早都需要返回一个不同的数值,这是一个十字路口,从这里开始,我们要么决定实现一个新的桩对象用于返回另一硬编码数值,要么对现有的桩对象进行扩展。
class ParameterizedStub : ICollaborator { private int value; public ParameterizedStub (int value) { this.value = value; } public int ComputeAndReturnValue() { return value; } } |
一旦踏上这个旅程,就会有无限的可能性。例如,如果测试需要抛出异常,一个小小的if就能转危为安。
class ParameterizedStub : ICollaborator { private int value; public ParameterizedStub (int value) { this.value = value; } public int ComputeAndReturnValue() { if (value < 10) { throw new InvalidOperationException(); } return value; } } |
然而,这样也会有危险。当我们实现愈加复杂的桩对象时,尽管我们感觉非常明智,但是仍会冒着业务逻辑镜像的风险。智能桩对象迟早将包括真实业务规则的简化版,而当原始规则发生改变时,桩对象就会弊大于利,它会使不熟悉业务规则变化的人感到很困惑,并使得规则维护和更新变得更加困难。但是,就这一点来说,仅仅因为一系列的测试用例需要不同的数值,就批量生产许多类似的桩对象也不会使测试集变得非常完美或者可维护。使用参数化的桩对象是不错的选择,但是应该避免其中包含条件分支或者其他形式的复杂逻辑。这条准则适用于单元测试中的桩对象。当用桩对象来替代较大组件或者系统时,通常很难避免桩对象中包含某种逻辑。
12.1.2 用桩对象来避免副作用
除了控制间接输入,桩对象还可能有其他目的。设想一下,将前面章节提到的简单测试场景做一个变化,这一次,协作者不返回任何内容,而是开始实现那些将测试变得不同于单元测试的功能,如写或读一个文件、建立网络连接或者更新数据库。图12.2总结了这一场景。
图12.2 用桩对象来避免副作用
注:1.测试代码调用被测对象。2.被测对象调用其协作者。3.协作者执行一个操作,该操作会引起一种或多种难以觉察的副作用。4.被测对象返回一个数值,该数值与测试相关,但与协作对象的交互无关。
假设副作用不是测试的焦点,我们只需要一种避免副作用的方法。为此,我们所需做的就是使用一个"空"的桩对象来代替充斥着副作用的代码。
版权声明:51Testing软件测试网获人民邮电出版社和作者授权连载本书部分章节。
任何个人或单位未获得明确的书面许可,不得对本文内容复制、转载或进行镜像,否则将追究法律责任。