使用 ScalaCheck / ScalaTest 子句时令人困惑的单元测试代码执行顺序

Perplexing unit test code execution order when using ScalaCheck / ScalaTest clauses

我在使用变量进行单元测试时遇到以下令人困惑的行为 class。

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

// 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 增加,所有测试用例都通过)?[​​=21=]

考虑像这样重写测试

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 在测试主体的 "inside" 上,这意味着我们有一个测试使用 forAll 提供的多个输入来测试系统 C。当它在 "outside" 上时像这样

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

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

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