如何将长ScalaTest规范剪成碎片

时间:2014-12-03 13:21:20

标签: scala functional-testing scalatest spray

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

  1. 设置内容,使用PUSH调用填充数据库
  2. 测试API a
  3. 测试API b ...
  4. 代码目前处于一个相当庞大的FlatSpec

    class RestAPITest extends FlatSpec
      with Matchers
      with ScalatestRouteTest
      with SprayJsonSupport
    

    我想砍掉"测试API a / b /..."零件,使代码更易于管理。试图这样做似乎是一个禁忌:it的类型是什么 - 如何传递它等等。

    那么,推荐的方法是什么呢?

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

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

    我应该看看"灯具"或者这是为了什么?之前曾尝试BeforeAndAfterAll,但并没有真正让它为我工作。

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

4 个答案:

答案 0 :(得分:2)

我说在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)并且它是可变的(所以如果第一个测试改变某些东西,它会影响第二个测试)。

显然,没有这样的结构会更加可取(例如,使用在此示例中实现List的{​​{1}}会更好,但是假设我们不能只需改变这种结构。

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

为了仍能测试它,我们需要在运行每个测试之前保持干净状态。假设我们想要为每个测试使用相同的值填充数据库,我们可以让我们的基础测试套件类扩展Database注意:存在两个特征:BeforeAndAfter,用于定义在执行每个测试用例之前和之后运行的BeforeAndAfterbefore after,它的不同之处在于它定义了在每个测试套件之前和之后运行的方法。

BeforeAndAfterAll

现在我们可以让测试套件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 } } 4。我们现在可以让其他测试套件扩展这个基类:

2

当然,我们也可以将常规方法中的常用功能分解出来,就像在两个测试用例中使用的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
}

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

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

答案 1 :(得分:1)

我开始工作的方式如下。

我通过混合TestAFirst特征使测试B和C在它们之前执行A.这个特性也确保TestA只能执行一次。

有几种可能的变化。我选择禁止TestA注释自动启动DoNotDiscover。理想情况下,我希望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等放在不同的源文件中,达到我所瞄准的模块化。这只是一个片段。

答案 2 :(得分:0)

添加为新答案,因此差异很明显,无需删除上述讨论。如果我没有做任何拼写错误,这应该有效(我测试了它,并在我的项目中采用)。

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
    }
  }
}

答案 3 :(得分:-1)

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

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