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

  1. class ShareCodeImperative extends org.scalatest.Suite
  2. with org.scalatest.BeforeAndAfter {
  3. var list : java.util.ArrayList[Integer] = _
  4. override def beforeEach() { list = new java.util.ArrayList[Integer] }
  5. override def afterEach() { list = null }
  6. def testListEmptyOnCreate() {
  7. expect(0, "Expected size to be 0") { list.size() }
  8. }
  9. def testGetOnEmptyList() {
  10. intercept[IndexOutOfBoundsException] { list.get(0) }
  11. }
  12. }
  13. (new ShareCodeImperative).execute()

ShareCodeImperative混入了BeforeAndAfter,改写了beforeEach()afterEach()方法。在beforeEach()方法里,我们实例化出java.util.ArrayList的一个实例,将其存在ShareCodeImperativelist字段里。现在,每个测试在执行之前都会拥有一个全新创建的ArrayList实例。在测试完成之后,afterEach()方法会将引用置为null——这个操作实际上是多余的,不过,总的来说,如果要做任何有意义的清理工作,请放在这里。

12.7.2 用闭包共享代码

在上面的例子中,我们不得不在测试套件中创建一个字段list,然后在beforeEach()的每一次调用中不停地给它赋值。这是命令式的风格,我们要因此承担风险——类里面的某些字段会在测试之间传来传去。单元测试的准则之一是测试必须要保证相互独立。仔细编写beforeEach()afterEach()保证独立性固然好,但运用函数式风格——即闭包,完全可以避免使用字段。下面就是个例子:

UnitTestingWithScala/ShareCodeFunctional.scala

  1. class ShareCodeFunctional extends org.scalatest.Suite {
  2. def withList(testFunction : (java.util.ArrayList[Integer]) => Unit) {
  3. val list = new java.util.ArrayList[Integer]
  4. try {
  5. testFunction(list)
  6. }
  7. finally {
  8. // perform any necessary cleanup here after return
  9. }
  10. }
  11. def testListEmptyOnCreate() {
  12. withList { list => expect(0, "Expected size to be 0") { list.size() } }
  13. }
  14. def testGetOnEmptyList() {
  15. withList {
  16. list => intercept[IndexOutOfBoundsException] { list.get(0) }
  17. }
  18. }
  19. }
  20. (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()这样的初始化方法,我们还可以创建很多很多,然后把它们用于其他测试。这样我们可以在不同的初始化及清理方式间做出选择。这让初始化和清理工作变得更加清晰,更易理解。同时,我们也会更容易掌握每个测试中发生的一切问题。