Проектирование сложного рабочего процесса с помощью спецификаций2

При разработке функциональных тестов мне нужно смоделировать рабочий процесс, в котором результат одного шага используется в качестве входных данных для следующих. Пример такой:

  1. search for a hotel\room with given criteria
    • check that request succeeded
    • проверить, что есть хоть какие-то результаты
  2. выбрать случайную комнату с шага 1.
  3. book the room from step 2.
    • check that request succeeded
  4. cancel the booking from step 3.
    • check that request succeeded

Ключевые моменты здесь:

  • мы не можем выполнить 3. без выполнения 1.
  • мы не можем выполнить 4. без выполнения 3.
  • если шаг терпит неудачу, мы должны прервать функцию

Каков подход к разработке спецификации для такого случая?


person scorpp    schedule 09.09.2013    source источник


Ответы (1)


Проще всего иметь изменяемый объект, представляющий процесс, и sequential спецификацию:

class HotelSpec extends mutable.Specification { sequential
  val hotel = new HotelProcess

  "get a room available on Monday" >> ifHotelOk {
    val rooms = request(MONDAY)
    hotel.selectedRooms = rooms
    rooms must not beEmpty
  }

  "book the room" >> ifHotelOk {
    val booking = bookRoom(hotel.selectedRooms.head)
    hotel.currentBooking = booking
    booking must beOk
  }

  def ifHotelOk(r: =>Any) = if (hotel.canContinueProcess) {
    try { r; hotel.continueProcess }
    catch { case t: Throwable => hotel.stopProcess; throw t }
  } else skipped("hotel process error in previous steps")
}

[ОБНОВИТЬ]

Вот еще один способ сделать это, где переменная лучше инкапсулирована:

import org.specs2._
import org.specs2.execute._
import org.specs2.specification.FixtureExample

class HotelSpec extends HotelProcessSpec {
  "get a room available on Monday" >> { hotel: HP =>
    val rooms = request(MONDAY)
    rooms must be empty

    // update the state of the process at the end of the example
    hotel.selectedRoomsAre(rooms)
  }

  // this example will only execute if the previous step was ok
  "book the room" >> { hotel: HP =>
    val booking = bookRoom(hotel.selectedRooms.head)
    booking.booked must beTrue
  }

  val MONDAY = "monday"
  def request(day: String): Seq[Room] = Seq(Room())
  def bookRoom(room: Room) = Booking()
}

/**
 * A specification trait encapsulating the process of booking hotel rooms
 */
trait HotelProcessSpec extends mutable.Specification with FixtureExample[HotelProcess] {
  sequential

  type HP = HotelProcess
  private var hotelProcess = HotelProcess()

  // if the hotelProcess is returned as the last statement of an Example
  // set the new value of the hotelProcess and return Success
  implicit def hotelProcessAsResult: AsResult[HotelProcess] = new AsResult[HotelProcess] {
    def asResult(hp: =>HotelProcess) =
      try { hotelProcess = hp; Success() }
      catch { case t: Throwable => hotelProcess = hotelProcess.stop; throw t }
  }

  /**
   * stop executing examples if one previous step failed
   */
  protected def fixture[R : AsResult](f: HotelProcess => R): Result = {
    if (hotelProcess.continue) {
      val result = AsResult(f(hotelProcess))
      if (!result.isSuccess) hotelProcess = hotelProcess.stop
      result
    }
    else                       skipped(" - SKIPPED: can't execute this step")
  }

}

case class HotelProcess(selectedRooms: Seq[Room] = Seq(), continue: Boolean = true) {
  def stop = copy(continue = false)
  def selectedRoomsAre(rooms: Seq[Room]) = copy(selectedRooms = rooms)
}
case class Room(number: Int = 0)
case class Booking(booked: Boolean = true)
person Eric    schedule 09.09.2013
comment
Я думаю, что должна быть возможность инкапсулировать это с помощью Fixture, чтобы избежать изменяемого состояния, я попробую это позже. - person Eric; 10.09.2013
comment
спасибо за отличный пример! мне потребовалось некоторое время, чтобы понять это, но в конце концов я был прав, думая о необходимости удержания изменяемого состояния. спасибо за отличную поддержку и очень интересную библиотеку! - person scorpp; 10.09.2013