我正在测试REST API,代码如下:
代码目前处于一个相当庞大的FlatSpec
:
class RestAPITest extends FlatSpec
with Matchers
with ScalatestRouteTest
with SprayJsonSupport
我想砍掉"测试API a / b /..."零件,使代码更易于管理。试图这样做似乎是一个禁忌:it
的类型是什么 - 如何传递它等等。
那么,推荐的方法是什么呢?
一旦基本设置成功,a / b / ...测试可以并行运行。
我目前在a / b / ...测试中使用assume
,以便在初始化失败时取消它们。
我应该看看"灯具"或者这是为了什么?之前曾尝试BeforeAndAfterAll
,但并没有真正让它为我工作。
感谢指点/意见。你如何保持测试套件的简短?
答案 0 :(得分:2)
我说在BeforeAndAfter
或BeforeAndAfterAll
中混音是减少你想做的场景中重复的最直观方法之一:"设置" - > "运行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
,用于定义在执行每个测试用例之前和之后运行的BeforeAndAfter
和before
和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 tests和org.scalatest: Global setup (like beforeAllSuites?))中还有其他线程处理类似的问题,但没有明确的答案。
当然,这里的想法是将TestB
,TestC
等放在不同的源文件中,达到我所瞄准的模块化。这只是一个片段。
答案 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
}
}
}