【问题标题】:How design a Specs2 database test, with interdependent tests?如何设计具有相互依赖的测试的 Specs2 数据库测试?
【发布时间】:2012-11-01 10:12:56
【问题描述】:

是否有一些首选的方法来设计 Specs2 测试,其中有很多测试取决于以前的测试结果?

在下面,您会找到我当前的测试套件。我不喜欢测试片段之间的vars。但它们是“需要的”,因为某些测试会生成 ID 号,后续测试会重复使用。

  1. 我应该将 ID 号存储在 Specs2 上下文中,还是创建一个单独的对象来保存所有可变状态?并且只在规范对象中放置测试片段?还是有更好的方法?

  2. 如果测试失败,我想取消相同深度的剩余测试。我可以让测试片段相互依赖吗? (我知道我可以在单个测试片段中取消剩余的匹配器(通过使用可变测试,或通过 orSkip),但是取消整个片段呢?)

.

object DatabaseSpec extends Specification {
  sequential

  "The Data Access Object" should {

    var someId = "" // These var:s feels error prone, is there a better way?

    "save an object" >> {
      someId = database.save(something)
      someId must_!= ""

      // I'd like to cancel the remaining tests, below, at this "depth",
      // if this test fragmen fails. Can I do that?
      // (That is, cancel "load one object", "list all objects", etc, below.)
    }

    "load one object" >> {
      anObject = database.load(someId)
      anObject.id must_== someId
    }

    "list all objects" >> {
      objs = database.listAll()
      objs.find(_.id == someId) must beSome
    }

    var anotherId = ""
    ...more tests that create another object, and
    ...use both `someId` and `anotherId`...

    var aThirdId = ""
    ...tests that use `someId`, `anotherId` and `aThirdId...
  }


  "The Data Access Object can also" >> {
    ...more tests...
  }

}

【问题讨论】:

标签: scala integration-testing specs2


【解决方案1】:

您的问题分为两部分:使用 vars 存储中间状态,以及在失败时停止示例。

1 - 使用变量

在使用可变规范时,还有一些使用 var 的替代方法。

您可以使用lazy vals 表示您的流程步骤:

object DatabaseSpec extends mutable.Specification { 
  sequential

  "The Data Access Object" should {

    lazy val id1    = database.save(Entity(1))
    lazy val loaded = database.load(id1)
    lazy val list   = database.list

    "save an object"   >> { id1 === 1 }
    "load one object"  >> { loaded.id === id1 }
    "list all objects" >> { list === Seq(Entity(id1)) }
  }

  object database {
    def save(e: Entity) = e.id
    def load(id: Int) = Entity(id)
    def list = Seq(Entity(1))
  }
  case class Entity(id: Int)
}

由于这些值是惰性的,它们只会在示例执行时被调用。

如果您准备好更改当前规范的结构,您还可以使用最新的 1.12.3-SNAPSHOT 并将所有这些小期望归为一个示例:

"The Data Access Object provides a save/load/list api to the database" >> {

  lazy val id1    = database.save(Entity(1))
  lazy val loaded = database.load(id1)
  lazy val list   = database.list

  "an object can be saved"  ==> { id1 === 1 }
  "an object can be loaded" ==> { loaded.id === id1 }
  "the list of all objects can be retrieved" ==> {
    list === Seq(Entity(id1))
  }
}

如果这些期望中的任何一个失败,那么其余的将不会执行,您将收到如下失败消息:

x The Data Access Object provides a save/load/list api to the database
  an object can not be saved because '1' is not equal to '2' (DatabaseSpec.scala:16)

另一种可能需要 2 个小的改进,即使用 Given/When/Then 编写规范的方式,但在 GivenWhen 步骤中使用“抛出”期望。正如您在用户指南中看到的,Given/When/Then 步骤从字符串中提取数据并将键入的信息传递给下一个Given/When/Then

import org.specs2._
import specification._
import matcher.ThrownExpectations

class DatabaseSpec extends Specification with ThrownExpectations { def is = 
  "The Data Access Object should"^
    "save an object"             ^ save^
    "load one object"            ^ load^
    "list all objects"           ^ list^
  end

  val save: Given[Int] = groupAs(".*") and { (s: String) =>
    database.save(Entity(1)) === 1
    1
  }

  val load: When[Int, Int] =  groupAs(".*") and { (id: Int) => (s: String) =>
    val e = database.load(id)
    e.id === 1
    e.id
  }

  val list: Then[Int] =  groupAs(".*") then { (id: Int) => (s: String) =>
    val es = database.list
    es must have size(1)
    es.head.id === id
  }
}

我要做的改进是:

  • 捕获失败异常以将其报告为失败而不是错误
  • 当没有从字符串描述中提取的内容时,不再需要使用groupAs(".*") and

在这种情况下,写应该就足够了:

val save: Given[Int] = groupAs(".*") and { (s: String) =>
  database.save(Entity(1)) === 1
  1
}

另一种可能性是允许直接写:

val save: Given[Int] = groupAs(".*") and { (s: String) =>
  database.save(Entity(1)) === 1
}

可以从String => MatchResult[T] 创建Given[T] 对象,因为MatchResult[T] 对象已经包含T 类型的值,这将成为“Given”。

2 - 在示例失败后停止执行

使用隐含的 WhenFail Around 上下文当然是做你想做的事情的最佳方式(除非你使用上面 G/W/T 示例中显示的期望描述)。

注意step(stepOnFail = true)

step(stepOnFail = true) 的工作原理是,如果 上一个并发示例块中的一个示例失败,则中断以下示例。但是,当您使用sequential 时,前一个块仅限于一个示例。因此,您所看到的。实际上,我认为这是一个错误,并且无论您是否使用顺序,都不应该执行所有剩余的示例。因此,请继续关注本周末即将推出的修复。

【讨论】:

  • 将期望分组到一个示例中,使用 ==> 和 1.12.3-SNAPSHOT,看起来不错。我认为生成的代码相当容易阅读。此外,将所有 lazy vals 放在测试代码上方会导致我认为代码更易于阅读。 — 在接下来的几个月(当 1.12.3 发布时),我可能会一次重写一点测试套件,并考虑使用==>。 (我还将它分成许多较小的测试套件和include 它们。)谢谢
  • 感谢step(stepOnFail = true) 的留言。
  • 所有更改都在 1.12.3-SNAPSHOT 中:现在可以从 Given/When/Then 步骤中引发故障,可以直接从采用完整描述字符串的函数创建 Given 步骤,步骤 ( stopOnFail=true) 的行为符合顺序规范的预期。
【解决方案2】:

(关于问题 1:我不知道示例中的 vars 是否有更好的替代方法。也许我的示例太长了,也许我应该将我的 Spec:s 拆分为许多较小的规格。 )

关于问题 2,我发现 in this email by etorreborre 可以这样停止后续测试:

"ex1" >> ok
"ex2" >> ok
"ex3" >> ko
 step(stopOnFail=true)

"ex4" >> ok

(如果 ex1、ex2 或 ex3 失败,将跳过 Ex4。(但是,如果您使用的是顺序规范,这在 Specs2


这是另一种方式:根据this Specs2 Googl groups email by etorreborre,可以让后续测试在失败时停止,如下所示: (“example2”将被跳过,但“example3”和“4”会运行)

class TestSpec extends SuperSpecification {

    sequential

    "system1" >> {
      implicit val stop = WhenFail()
      "example1" >> ko
      "example2" >> ok
    }
    "system2" >> {
      implicit val stop = WhenFail()
      "example3" >> ok
      "example4" >> ok
    }
}

case class WhenFail() extends Around {
  private var mustStop = false

  def around[R <% Result](r: =>R) = {
    if (mustStop)          Skipped("one example failed")
    else if (!r.isSuccess) { mustStop = true; r }
    else                   r
  }
}

this email by etorreborre 中有一个方法可以在示例失败时取消后续的规范,前提是您已包含规范列表:

sequential ^ stopOnFail ^
"These are the selenium specifications"         ^
  include(childSpec1, childSpec2, childSpec3)

您需要在build.sbt 中编辑测试选项,这样子规范在被包含后就不会再次独立执行。来自电子邮件:

 testOptions := Seq(Tests.Filter(s =>
  Seq("Spec", "Selenium").exists(s.endsWith(_)) &&
    ! s.endsWith("ChildSpec")))

【讨论】:

    【解决方案3】:

    Specs doc 表示您可以使用 .orSkip 来在失败的情况下跳过示例的其余部分

    "The second example will be skipped" >> {
        1 === 2
       (1 === 3).orSkip
    }
    

    但我没有亲自尝试过

    【讨论】:

    • 实际上这只会取消当前{ ... } 块中的剩余测试(我认为这称为“测试片段”)。我正在寻找的是在同一深度杀死所有后续测试片段的东西(我的意思是,文本“第二个示例......”开始的“深度”,而不是 @987654324 中的“深度” @ 堵塞)。我会更新我的问题以使这一点更清楚。
    猜你喜欢
    • 2012-03-28
    • 2016-03-13
    • 1970-01-01
    • 1970-01-01
    • 2020-09-17
    • 2015-07-16
    • 1970-01-01
    • 2011-04-09
    • 2020-05-15
    相关资源
    最近更新 更多