【问题标题】:How to cut a long ScalaTest spec to pieces如何将较长的 ScalaTest 规范切割成碎片
【发布时间】:2015-02-01 01:55:44
【问题描述】:

我正在测试一个 REST API,代码如下:

  1. 设置内容,使用 PUSH 调用填充数据库
  2. 测试 API a
  3. 测试 API b ...

代码目前在一个相当大的FlatSpec

class RestAPITest extends FlatSpec
  with Matchers
  with ScalatestRouteTest
  with SprayJsonSupport

我想删除“测试 API a/b/...”部分,以使代码更易于管理。尝试这样做似乎是一个禁忌:it 的类型是什么 - 如何传递它等等。

那么,处理这些事情的推荐方法是什么。

一旦基本设置成功,a/b/... 测试可以并行运行。

我目前在 a/b/... 测试中使用 assume,以便在初始化失败时取消它们。

我应该查看“固定装置”还是为了这个?之前尝试过BeforeAndAfterAll,但并没有真正让它为我工作。

感谢您的指点/意见。您如何保持测试套件的简短?

【问题讨论】:

  • 如果设置总是(大部分)相同,那么我会说使用BeforeAndAfterAll 可能是要走的路。什么不适合你?
  • @KuluLimpa 我已经有一段时间没有尝试过了。会再试一次。

标签: scala functional-testing scalatest spray


【解决方案1】:

我想说,在你想要做的场景中,混合使用 BeforeAndAfterBeforeAndAfterAll 是减少重复的最直观的方法之一:“设置”->“运行 test1”->“设置”-> “运行 test2”,“设置”(大部分)相同。

假设我们有一个讨厌的、难以测试的Database

object Database {
  private var content: List[Int] = Nil

  def add(value: Int) = content = value :: content

  def remove(): Unit = content = if (content.nonEmpty) content.tail else Nil

  def delete(): Unit = content = Nil

  def get: Option[Int] = content.headOption

  override def toString = content.toString()
}

它是一个单例(所以我们不能只为每个测试实例化一个新的Database)并且它是可变的(所以如果第一个测试改变了一些东西,它会影响第二个测试)。

显然,最好不要有这样的结构(例如,在本例中使用实现DatabaseList 会更好),但假设我们不能简单地改变这个结构.

编辑:请注意,在这种情况下,不可能(至少我想不出办法)并行地运行对同一个单例实例进行变异的测试。

为了仍然能够对其进行测试,我们需要在运行每个测试之前有一个干净的状态。假设我们想为每个测试使用相同的值填充数据库,我们可以让我们的基本测试套件类扩展BeforeAndAfter注意:存在两个特征:BeforeAndAfter,它定义了在每个测试用例执行之前和之后运行的beforeafter,以及@987654333 @,不同之处在于它定义了在每个测试套件之前和之后运行的方法。

class RestAPITest extends FlatSpec with ShouldMatchers with BeforeAndAfter {
  before {
    Database.delete()
    Database.add(4)
    Database.add(2)
  }
}

现在我们可以有一个测试套件ATest 扩展这个基类:

class ATest extends RestAPITest {
  "The database" should "not be empty" in {
    Database.get shouldBe defined
  }
  it should "contain at least two entries" in {
    Database.remove()

    Database.get shouldBe defined
  }
  it should "contain at most two entries" in {
    Database.remove()
    Database.remove()

    Database.get should not be defined
  }
}

在每个测试开始时,数据库包含两个值42。我们现在可以让其他测试套件扩展这个基类:

class BTest extends RestAPITest {
  "The contents of the database" should "add up to 6" in {
    getAll.sum shouldBe 6
  }

  "After adding seven, the contents of the database" should "add up to 13" in {
    Database.add(7)

    getAll.sum shouldBe 13
  }

  def getAll: List[Int] = {
    var result: List[Int] = Nil
    var next = Database.get
    while(next.isDefined){
      result = next.get :: result
      Database.remove()
      next = Database.get
    }
    result
  }
}

当然,我们也可以在常规方法中分解出通用功能,就像两个测试用例都使用的getAll 中所做的那样。

附录

引用问题:

您如何保持测试套件的简短?

在我看来,测试代码与生产代码没有太大区别。如果它们不属于您已经拥有的特定类,则使用方法将常用功能分解并放入单独的特征中。

但是,如果您的生产代码要求测试始终执行同一段代码,那么您的生产代码中可能存在太多依赖项。假设你有一个函数(在你的生产代码中)

def plus: Int = {
  val x = Database.get.get
  Database.remove()
  x + Database.get.get
}

那么你不能测试这个函数,除非你用你想要添加的两个值来填充你的数据库。在这种情况下,让你的测试更短、更易读的最好方法是重构你的生产代码。

"plus 3 2" should "be 5" in {
  Database.add(3)
  Database.add(2)

  plus shouldBe 5
}

可能变成

"plus 3 2" should "be 5" in {
  plus(3,2) shouldBe 5
}

在某些情况下,摆脱依赖并不容易。但是您可能希望测试场景中的对象依赖于特殊的测试环境。数据库就是一个很好的例子,文件系统或日志记录也是如此。这些事情的执行成本(I/O 访问)往往更高,并且可能具有您必须首先建立的进一步依赖关系。

在这些情况下,您的测试很可能会从使用模拟对象中获益。例如,您可能希望实现一个实现数据库接口的内存数据库。

【讨论】:

  • 您对 BeforeAndAfter 等的解释非常棒 - 谢谢。但是,我的需求有所不同,因为一旦填充步骤通过(这本身就是一个测试),数据库内容就是纯只读的。提到从 RestAPITest 派生,我尝试了一种方法,其中测试类从一个公共基础派生,并且通过正常的闩锁机制阻止其他类执行。然而 :) 因为他们是姐妹(我第一次尝试从人口测试派生,但那种没有用),他们不知道应该先执行哪个测试,从而导致僵局。将尝试修复它。
  • @akauppi 啊,在这种情况下,您需要在执行所有依赖测试之前执行的代码 - 并且只执行一次。也许这个问题提供了一个解决方案:stackoverflow.com/questions/15423337/…
【解决方案2】:

我的工作方式如下。

我通过混合TestAFirst 特征使测试 B 和 C 在它们之前执行 A。该特征还确保TestA 只会执行一次。

有几种可能的变化。我选择通过 DoNotDiscover 注释来禁止自动启动 TestA 本身。理想情况下,我想让TestA 看起来尽可能像一个正常的测试,将所有依赖关系处理推到TestAFirst

import java.util.concurrent.atomic.{AtomicBoolean}
import org.scalatest.{DoNotDiscover, FlatSpec}

/*
* Mix this trait into any specs that need 'TestA' to have been run first.
*/
trait TestAFirst extends FlatSpec {
  import TestAFirst._

  if (!doneTestA.getAndSet(true)) {
    // tbd. Can we detect here if 'execute' failed? Would be a better place to set 'testASuccess' than within the
    //      'TestA' itself (= limit all dependency things to 'TestAFirst').
    //
    (new TestA).execute
  }
}

object TestAFirst {
  val doneTestA= new AtomicBoolean
  @volatile var testASuccess= false   // remains 'false' if 'TestA' failed, causing B and C to cancel
}

/*
* 'TestA' is a test *almost* like any other.
*/
@DoNotDiscover
class TestA extends FlatSpec {
  import TestAFirst._

  behavior of "Root class"; {
    it should "run prior to any of the B,C classes" in {

      assert(true)    // ... A tests

      testASuccess = true
    }
  }
}

class TestB extends TestAFirst {
  import TestAFirst._

  behavior of "class B"; {
    it should "run after A has been run" in {
      assume(testASuccess)

      assert(true)    // ... B tests
    }
  }
}

class TestC extends TestAFirst {
  import TestAFirst._

  behavior of "class C"; {
    it should "run after A has been run" in {
      assume(testASuccess)

      assert(true)    // ... C tests
    }
  }
}

仍然欢迎更好的解决方案,但由于这有效,我想将其发布。 SO 中还有其他线程(Doing something before or after all Scalatest testsorg.scalatest: Global setup (like beforeAllSuites?))处理类似问题,但没有明确的答案。

当然,这里的想法是将TestBTestC 等放在不同的源文件中,从而实现我所追求的模块化。这只是一个sn-p。

【讨论】:

  • 感谢您添加解决方案以供将来参考。在阅读您的解决方案时,我想到了类似的事情。如果您有一个单例object Setup 来执行所有设置代码作为其实例化的一部分怎么办?然后,您可以创建一个简单地访问此单例对象并将其与您的测试混合的特征。然后,单例属性将保证设置只执行一次,并且在对象访问中混合应该保证它在测试运行之前执行。这样,您就不必将 TestA 设为特殊。
  • 这是很棒的建议,@KuluLimpa,我想我会尝试的。它使用了一些 Scala“魔法”(s.a. 依赖于惰性对象初始化),但它确实有意义。这些东西已经很好地定义了,所以我只会向其他人添加 cmets 以了解发生了什么。明天会在这里提到它是怎么回事。
【解决方案3】:

添加为新答案,因此差异很明显,不需要删除上面的讨论。如果我没有做任何错别字,这应该可以工作(我确实测试过,并在我的项目中采用)。

import org.scalatest._

/*
* Mix this trait into any specs that need 'TestA' to have been run first.
*/
trait TestAFirst {

  // Reading a 'TestA' object's field causes it to be instantiated and 'TestA' to be executed (but just once).
  //
  val testASuccess = TestA.success
}

/*
* 'TestA' gets instantiated via the companion object explicitly (thus @DoNotDiscover)
* and creates a success value field. Otherwise, it's a test just like any other.
*/
@DoNotDiscover
class TestA private extends FlatSpec {
  private var success = false   // read once, by the companion object

  behavior of "Root class"; {
    it should "run prior to any of the B,C classes" in {

      assert(true)    // ... A tests

      success = true
    }
  }
}

object TestA {
  val success = {
    val o= new TestA
    o.execute
    o.success   // getting a value from the executed test ('.execute()' itself doesn't provide a status)
  }
}

class TestB extends FlatSpec with TestAFirst {

  behavior of "class B"; {
    it should "run after A has been run" in {
      assume(testASuccess)

      assert(true)    // ... B tests
    }
  }
}

class TestC extends FlatSpec with TestAFirst {

  behavior of "class C"; {
    it should "run after A has been run" in {
      assume(testASuccess)

      assert(true)    // ... C tests
    }
  }
}

【讨论】:

    【解决方案4】:

    你使用Spray框架?你可以试试spray.testkit.Specs2RouteTest

       class RestAPISpec extends Specification with Specs2RouteTest {
         "RestAPITest" should {
            "Test A" in {
              ... some code
            }
            "Test B" in {
              ... some code
            }
         }
       }
    

    【讨论】:

    • ScalatestRouteTest 做同样的事情,使用 ScalaTest (这对我来说更熟悉)。 Spec2是否会更好地解决这个问题?您的示例没有显示从主体中剪掉一些“一些代码”——这就是我想要做的。
    猜你喜欢
    • 2011-06-20
    • 2011-09-26
    • 1970-01-01
    • 2020-02-18
    • 2012-02-13
    • 1970-01-01
    • 1970-01-01
    • 2019-01-09
    • 1970-01-01
    相关资源
    最近更新 更多