Как разработать тест базы данных Specs2 с взаимозависимыми тестами?

Есть ли предпочтительный способ разработки теста Specs2 с большим количеством тестов, которые зависят от результатов предыдущих тесты?

Ниже вы найдете мой текущий набор тестов. Мне не нравятся var между тестовыми фрагментами. Однако они «необходимы», поскольку некоторые тесты генерируют идентификационные номера, которые повторно используют последующие тесты.

  1. Должен ли я вместо этого хранить идентификационные номера в контексте Specs2 или создавать отдельный объект, который содержит все изменяемые состояния? И размещать в объекте спецификации только тестовые фрагменты? Или есть еще лучший подход?

  2. Если тест не пройден, я бы хотел отменить оставшийся тест на той же глубине. Могу ли я заставить тестовые фрагменты зависеть друг от друга? (Я знаю, что могу отменить оставшиеся совпадения в одном тестовом фрагменте (используя изменяемые тесты или через илиПропустить), но как насчет отмены целых фрагментов?)

.

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

}

person KajMagnus    schedule 01.11.2012    source источник
comment
BTW specs2 3.x был разработан именно для решения этой проблемы, когда вы можете создавать произвольные тесты, опираясь на результаты предыдущих тестов. См. здесь: etorreborre.github.io /specs2/guide/SPECS2-3.1.1/   -  person Eric    schedule 25.03.2015


Ответы (3)


В вашем вопросе есть две части: использование vars для хранения промежуточного состояния и остановка примеров при сбое.

1 - Использование переменных

Есть несколько альтернатив использованию vars при использовании изменяемой спецификации.

Вы можете использовать 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)

Другая возможность, которая потребует двух небольших улучшений, — использовать Дано/Когда/Тогда способ написания спецификаций, но с использованием "выброшенных" ожиданий внутри Given и When шагов. Как видно из Руководства пользователя, 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
}

где объект Given[T] может быть создан из String => MatchResult[T], потому что объект MatchResult[T] уже содержит значение типа T, которое станет "данным".

2 - Остановить выполнение после неудачного примера

Использование неявного контекста WhenFail Around, безусловно, лучший способ сделать то, что вы хотите (если вы не используете описания ожиданий, как показано выше в примере G/W/T).

Примечание к step(stepOnFail = true)

step(stepOnFail = true) работает, прерывая следующие примеры, если один пример в предыдущем блоке параллельных примеров дал сбой. Однако, когда вы используете sequential, этот предыдущий блок ограничен только одним примером. Отсюда то, что вы видите. На самом деле я думаю, что это ошибка и что все оставшиеся примеры не должны выполняться независимо от того, используете ли вы последовательный код или нет. Так что следите за исправлением, которое выйдет на этих выходных.

person Eric    schedule 02.11.2012
comment
Группировка ожиданий в пример с использованием ==> с 1.12.3-SNAPSHOT выглядит неплохо. Полученный код, я думаю, довольно легко читается. Кроме того, размещение всех lazy vals над тестовым кодом приводит к тому, что код становится немного легче читать, как мне кажется. — Я, вероятно, буду переписывать набор тестов понемногу в течение следующих нескольких месяцев (когда будет выпущена версия 1.12.3) и рассмотрю возможность использования ==>. (Я также разделю его на множество небольших наборов тестов и include их.) Спасибо. - person KajMagnus; 02.11.2012
comment
Спасибо за заметку о step(stepOnFail = true). - person KajMagnus; 02.11.2012
comment
Все изменения в 1.12.3-SNAPSHOT: сбои теперь могут быть выброшены из шагов Given/When/Then, заданный шаг может быть создан непосредственно из функции, принимающей строку полного описания, шаг (stopOnFail=true) ведет себя так, как ожидалось, с последовательная спецификация. - person Eric; 04.11.2012

(Относительно вопроса 1: я не знаю, есть ли лучшая альтернатива var внутри примеров. Возможно, мои примеры просто слишком длинные, и, возможно, мне следует разделить мои спецификации на множество более мелких спецификаций.)

Что касается вопроса 2, я нашел в этом письме от etorreborre. что остановить последующие тесты можно так:

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

"ex4" >> ok

(Ex4 будет пропущен в случае сбоя ex1, ex2 или ex3. (Однако это не работает должным образом в Specs2 ‹ 1.12.3, если вы используете последовательную спецификацию.))


Вот еще один способ: согласно этой электронной почте Specs2 Googl от etorreborre можно остановить последующие тесты в случае сбоя, например: («пример 2» будет пропущен, но «пример 3» и «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
  }
}

В этом письме от 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")))
person KajMagnus    schedule 02.11.2012

В документе спецификаций указано, что вы можете использовать .orSkip чтобы пропустить остальную часть примера в случае неудачи

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

Но лично я не пробовал

person om-nom-nom    schedule 01.11.2012
comment
На самом деле это только отменяет оставшиеся тесты в текущем блоке { ... } (который, я думаю, называется тестовым фрагментом). То, что я ищу, это что-то, что убивает все последующие фрагменты теста на той же глубине (я имею в виду глубину, с которой начинается текст Второй пример..., а не глубину внутри блока { ... }). Я обновлю свой вопрос, чтобы сделать это более ясным. - person KajMagnus; 01.11.2012