首页 > 解决方案 > 使用 ScalaCheck / ScalaTest 子句时令人困惑的单元测试代码执行顺序

问题描述

在使用变量对类进行单元测试时,我面临以下令人困惑的行为。

为了一个简单的例子,假设我有以下类:

// Case classes are not an alternative in my use case.
final class C(var i: Int = 0) {
  def add(that: C): Unit = {
    i += that.i
  }

  override def toString: String = {
    s"C($i)"
  }
}

为此,我编写了以下琐碎且看似无害的单元测试:

import org.junit.runner.RunWith
import org.scalacheck.Gen
import org.scalatest.junit.JUnitRunner
import org.scalatest.prop.GeneratorDrivenPropertyChecks
import org.scalatest.{MustMatchers, WordSpec}

@RunWith(classOf[JUnitRunner])
class CUnitTest extends WordSpec with MustMatchers with GeneratorDrivenPropertyChecks {
  private val c: C = new C()

  forAll (Gen.choose(1, 100).map(new C(_))) { x =>
    s"Adding $x to $c" must {
      val expectedI = c.i + x.i

      c.add(x)

      s"result in its .i property becoming $expectedI" in {
        c.i mustBe expectedI
      }
    }
  }
}

除最后一个测试用例外,所有测试用例均失败:

在此处输入图像描述

例如,前三个测试用例失败,结果如下:

org.scalatest.exceptions.TestFailedException: 414 was not equal to 68
org.scalatest.exceptions.TestFailedException: 414 was not equal to 89
org.scalatest.exceptions.TestFailedException: 414 was not equal to 151

现在,围绕单元测试并移动子句c.add(x)内的部分:in

import org.junit.runner.RunWith
import org.scalacheck.Gen
import org.scalatest.junit.JUnitRunner
import org.scalatest.prop.GeneratorDrivenPropertyChecks
import org.scalatest.{MustMatchers, WordSpec}

@RunWith(classOf[JUnitRunner])
class CUnitTest extends WordSpec with MustMatchers with GeneratorDrivenPropertyChecks {
  private val c: C = new C()

  forAll (Gen.choose(1, 100).map(new C(_))) { x =>
    s"Adding $x to $c" must {
      val expectedI = c.i + x.i

      s"result in its .i property becoming $expectedI" in {
        c.add(x)

        c.i mustBe expectedI
      }
    }
  }
}

除第一个测试用例外,所有测试用例均失败:

在此处输入图像描述

例如,第二个和第三个测试用例失败并显示以下消息:

org.scalatest.exceptions.TestFailedException: 46 was not equal to 44
org.scalatest.exceptions.TestFailedException: 114 was not equal to 68

此外,c.i测试用例描述似乎并没有像我预期的那样增加。

显然,ScalaTest 子句中的执行顺序不是自上而下的。某些事情发生的时间早于或晚于它的编写顺序,或者可能根本不发生,具体取决于它在哪个子句中,但我无法理解它。

发生了什么事,为什么?此外,我怎样才能实现所需的行为(c.i增加,所有测试用例都通过)?

标签: scalavariablesscalatestscalacheckorder-of-execution

解决方案


考虑像这样重写测试

import org.scalacheck.Gen
import org.scalatest._
import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks

class HelloSpec extends WordSpec with MustMatchers with ScalaCheckDrivenPropertyChecks {
  private val c: C = new C()

  "class C" must {
    "add another class C" in {
      forAll (Gen.choose(1, 100).map(new C(_))) { x =>
        val expectedI = c.i + x.i
        c.add(x)
        c.i mustBe expectedI
      }
    }
  }
}

请注意这里forAll是如何在测试主体的“内部”,这意味着我们有一个测试,它使用由提供的多个输入forAll来测试系统C。当它像这样在“外面”时

forAll (Gen.choose(1, 100).map(new C(_))) { x =>
  s"Adding $x to $c" must {
    ...
    s"result in its .i property becoming $expectedI" in {
      ...
    }
  }
}

thenforAll被误用于生成多个测试,其中每个测试都有一个测试输入,但是其目的是为被测系统forAll生成多个输入,而不是多个测试。此外,CUnitTest后续测试中的结果设计取决于先前测试的状态,这是错误且难以维护的。理想情况下,测试将彼此隔离运行,其中所有需要的状态都作为测试夹具的一部分重新提供。

一些旁注:@RunWith(classOf[JUnitRunner])应该不是必需的,并且GeneratorDrivenPropertyChecks已被弃用。


推荐阅读