Насмешка над композицией slick.dbio.DBIO в spec2

Использование Scala, Play Framework, Slick 3, Specs2.

У меня есть слой репозиторий и слой службы. Репозитории довольно тупые, и я использую specs2, чтобы убедиться, что сервисный уровень выполняет свою работу.

Мои репозитории возвращали фьючерсы, например:

def findById(id: Long): Future[Option[Foo]] =
  db.run(fooQuery(id))

Затем он будет использоваться в сервисе:

def foonicate(id: Long): Future[Foo] = {
  fooRepository.findById(id).flatMap { optFoo =>
    val foo: Foo = optFoo match {
      case Some(foo) => [business logic returning Foo]
      case None => [business logic returning Foo]
    }

    fooRepository.save(foo)
  }
}

Услуги было легко определить. В спецификации сервиса над FooRepository будет издеваться так:

fooRepository.findById(3).returns(Future(Foo(3)))

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

Имея это в виду, я изменил свои репозитории, чтобы они возвращали slick.dbio.DBIO, и добавил вспомогательный метод для транзакционного выполнения запроса:

def findById(id: Long): DBIO[Option[Foo]] =
  fooQuery(id)

def run[T](query: DBIO[T]): Future[T] =
  db.run(query.transactionally)

Сервисы составляют DBIO и, наконец, вызывают репозиторий для выполнения запроса:

def foonicate(id: Long): Future[Foo] = {
  val query = fooRepository.findById(id).flatMap { optFoo =>
    val foo: Foo = optFoo match {
      case Some(foo) => [business logic finally returning Foo]
      case None => [business logic finally returning Foo]
    }

    fooRepository.save(foo)
  }

  fooRepository.run(query)
}

Кажется, это работает, но теперь я могу указать это только так:

val findFooDbio = DBIO.successful(None))
val saveFooDbio = DBIO.successful(Foo(3))

fooRepository.findById(3) returns findFooDbio
fooRepository.save(Foo(3)) returns saveFooDbio
fooRepository.run(any[DBIO[Foo]]) returns Future(Foo(3))

Этот any в макете run отстой! Теперь я не проверяю фактическую логику, а вместо этого принимаю любые DBIO[Foo]! Я пытался использовать следующее:

fooRepository.run(findFooDbio.flatMap(_ => saveFooDbio)) returns Future(Foo(3))

Но это прерывается с помощью java.lang.NullPointerException: null, который является способом specs2 сказать "извините, приятель, метод с этим параметром не найден". Я пробовал разные варианты, но ни один из них не работал.

Я подозреваю, что это может быть потому, что функции нельзя сравнивать:

scala> val a: Int => String = x => "hi"
a: Int => String = <function1>
scala> val b: Int => String = x => "hi"
b: Int => String = <function1>
scala> a == b
res1: Boolean = false

Есть идеи, как определить состав DBIO без обмана?


person tasuki    schedule 02.06.2017    source источник


Ответы (1)


У меня была похожая идея, и я также исследовал ее, чтобы узнать, смогу ли я:

  • создать ту же композицию DBIO
  • используйте его с матчером в насмешке

Однако я обнаружил, что на практике это практически невозможно:

  • как вы заметили, нельзя сравнивать функции
  • кроме того, когда вы исследуете внутренности DBIO, это в основном структура, подобная Free-монаде, - она ​​имеет реализацию для простых значений и непосредственно сгенерированных запросов (тогда, если вы извлекаете операторы, вы можете сравнить некоторую часть запроса), но есть также сопоставления, в которых хранятся функции
  • даже если вам как-то удалось переиспользовать функции, чтобы у них было ссылочное равенство, реализации DBIO не заботятся о переопределении equals, так что они все равно были бы другим зверем

Зная это, я отказался от первоначальной идеи. Что я могу порекомендовать взамен:

  • имитировать любой ввод database.run - это более подвержено ошибкам, так как не будет уведомлять вас, если тестовые ожидания начнут отличаться от возвращаемых результатов, но это лучше, чем ничего
  • замените DBIO некоторой промежуточной структурой, которую, как вы знаете, можно безопасно сравнивать. Например. Cats' Бесплатная монада Реализация использует case-классы, поэтому, если вам удается гарантировать, что функции каким-то образом сопоставимы (например, не создавая их ad hoc, а вместо этого используя vals и objects), вы можете сравнивать в промежуточном представлении и имитировать весь интерпретировать -> запустить процесс
  • заменить модульные тесты имитированной базой данных на интеграционные тесты с реальной базой данных
  • попробуйте Typed Tagless Final Interpreter шаблон для работы с базами данных - и в основном вводить в тесты другую монаду, чем в производство (например, prod -> service возвращая DBIO, production -> service возвращая фьючерсы, которые вы хотите)

На самом деле, вы можете попробовать многое другое с реализациями Free, TTFI и swapping. Суть в том, что вы не можете надежно сравнивать DBIO, поэтому создавайте свой код таким образом, чтобы вы могли тестировать, не делая этого. Это неприятный ответ, особенно если вы просто хотели собрать тест и двигаться дальше, но, насколько я знаю, другого пути нет.

person Mateusz Kubuszok    schedule 21.02.2018
comment
Отличный ответ, стоит подождать :) - person tasuki; 19.04.2018