使用 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
已弃用。
我在使用变量进行单元测试时遇到以下令人困惑的行为 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
已弃用。