12.7 在测试间共享代码
在ScalaTest里,有两种方式可以在测试间共享代码。假设要为java.util.ArrayList
写多个测试,我们并不希望在每个方法中都创建一个实例,而是希望在一个公共的方法里创建——这可以保证代码的DRY⑩。下面逐一来看这两种方式,第一种跟JUnit的方式很像,第二种利用了闭包的优势。
⑩参见《程序员修炼之道》中的“不要重复你自己”。
12.7.1 用BeforeAndAfter共享代码
ScalaTest提供了一个trait:BeforeAndAfter,可以把它混入到测试套件中,为套件提供beforeEach()
和afterEach()
方法。这两个方法跟JUnit的setUp()
、tearDown()
很像,跟肉夹馍一样,把每个测试方法夹在中间——beforeEach()
会在每个测试运行之前自动运行,afterEach()
则会在测试之后运行。BeforeAndAfter还提供了beforeAll()
和afterAll()
方法,它们都只会执行一次,前者在套件中任何测试都还没有被运行之前执行,后者则是所有测试运行完毕后执行。下面来看一下beforeEach()
和afterEach()
的应用:
UnitTestingWithScala/ShareCodeImperative.scala
class ShareCodeImperative extends org.scalatest.Suite
with org.scalatest.BeforeAndAfter {
var list : java.util.ArrayList[Integer] = _
override def beforeEach() { list = new java.util.ArrayList[Integer] }
override def afterEach() { list = null }
def testListEmptyOnCreate() {
expect(0, "Expected size to be 0") { list.size() }
}
def testGetOnEmptyList() {
intercept[IndexOutOfBoundsException] { list.get(0) }
}
}
(new ShareCodeImperative).execute()
ShareCodeImperative
混入了BeforeAndAfter,改写了beforeEach()
和afterEach()
方法。在beforeEach()方法里,我们实例化出java.util.ArrayList
的一个实例,将其存在ShareCodeImperative
的list
字段里。现在,每个测试在执行之前都会拥有一个全新创建的ArrayList
实例。在测试完成之后,afterEach()
方法会将引用置为null
——这个操作实际上是多余的,不过,总的来说,如果要做任何有意义的清理工作,请放在这里。
12.7.2 用闭包共享代码
在上面的例子中,我们不得不在测试套件中创建一个字段list
,然后在beforeEach()
的每一次调用中不停地给它赋值。这是命令式的风格,我们要因此承担风险——类里面的某些字段会在测试之间传来传去。单元测试的准则之一是测试必须要保证相互独立。仔细编写beforeEach()
和afterEach()
保证独立性固然好,但运用函数式风格——即闭包,完全可以避免使用字段。下面就是个例子:
UnitTestingWithScala/ShareCodeFunctional.scala
class ShareCodeFunctional extends org.scalatest.Suite {
def withList(testFunction : (java.util.ArrayList[Integer]) => Unit) {
val list = new java.util.ArrayList[Integer]
try {
testFunction(list)
}
finally {
// perform any necessary cleanup here after return
}
}
def testListEmptyOnCreate() {
withList { list => expect(0, "Expected size to be 0") { list.size() } }
}
def testGetOnEmptyList() {
withList {
list => intercept[IndexOutOfBoundsException] { list.get(0) }
}
}
}
(new ShareCodeFunctional).execute()
ShareCodeFunctional
继承了Suite
——这个类我们已经相当熟悉了。withList()
会接收一个闭包做参数,在方法定义的括号中,对闭包的签名有详细描述:这个闭包会接收ArrayList
,返回Unit
(即Scala中的void
类型),这个闭包的参数名叫做testFunction
。
在withList()
方法中,我们创建了一个ArrayList
的实例,把它赋值给一个局部常量,叫做list
——上面BeforeAndAfter的例子中,它声明为var
。然后再用list
作为参数,调用testFunction
这个闭包。测试方法返回之后,可以做一些必要的清理工作。这是Execute Around Method模式的又一个例子(参见6.7节,Execute Around Method模式)。
在每个测试方法中,我们都调用了withList()
,给它一个闭包,让闭包使用withList()
创建出来的list
,执行真正的测试。像withList()
这样的初始化方法,我们还可以创建很多很多,然后把它们用于其他测试。这样我们可以在不同的初始化及清理方式间做出选择。这让初始化和清理工作变得更加清晰,更易理解。同时,我们也会更容易掌握每个测试中发生的一切问题。