Как я могу внедрить зависимость от одноэлементного объекта?

Предположим, я тестирую метод, который зависит от (импортированного) одноэлементного экземпляра с именем WS (веб-служба), у которого есть метод url(url: String), который принимает URL-адрес и возвращает запрос.

def doRequest(url: String): Future[Response] = {
  val request = WS.url(url)
  for {
    response <- request.post(params)
  } yield {
    val res: JsResult[MyResult] = response.json.validate[MyResult]
    res.getOrElse(throw new NotSupportedException)
  }
}

Я хотел бы иметь возможность внедрить зависимость WS, чтобы мои модульные тесты не требовали фактического исходящего HTTP-запроса, а вместо этого могли зависеть от фиктивного экземпляра WS.

Это было проблемой для меня, потому что, хотя синглтон технически имеет тип (Class[WS.type]), свойства и методы WS теряются при привязке синглтона к val, который ожидает Класс[WS.тип]. Это означает, что я не могу просто использовать простой шаблон торта, например:

trait WSComponent {
  val ws: Class[_ <: WS.type]
}

object ApplicationContext extends WSComponent {
  val ws = WS
}

object TestContext extends WSComponent {
  val ws = mock[WS]
}

Если я сделаю это, а затем вызову методы WS в любом контексте, я получу ошибку компиляции, которая заключается в том, что Class[_ ‹: WS.type] не имеет метода с именем (например) url().

По похожей причине (по сути, одноэлементные объекты не имеют типов, хотя они и есть), я не могу предоставить неявный параметр, который принимает WS.type, потому что, опять же, я потеряю методы и свойства, которые были объявлены для одноэлементного объекта.

Какие существуют подходы для внедрения зависимостей в одноэлементные объекты? Мне нравится использовать шаблон торта для внедрения зависимостей, но он вносит в мой код довольно много шаблонов, поэтому идеальное решение не будет смешивать код со слишком большим количеством шаблонов.

Заранее благодарим вас за любые предложения.


person Ben Wilhelm    schedule 20.06.2013    source источник
comment
как у вас может быть val ws: Class[_ <: WS.type] и в подтипе val ws = WS? Вы имеете в виду val ws: WS.type?   -  person gourlaysama    schedule 20.06.2013


Ответы (2)


Объект Singleton имеет типы, и вы можете вызывать для них методы:

scala> val i: Int.type = Int
i: Int.type = object scala.Int

scala> i.box(42)
res0: Integer = 42

Я предполагаю, что ваша ошибка связана с

val ws: Class[_ <: WS.type]

реализуется с:

val ws = WS

Это не может скомпилироваться, и действительно, Class[...] не имеет метода url(). Вы можете просто ввести ws в WS.type:

trait WSComponent {
  val ws: WS.type
}

И измените насмешку на mock[WS.type].


Редактировать: другой способ ниже работает только в том случае, если у вас есть контроль над типом WS (очевидно, здесь это не так, поскольку он исходит из игры)

Если вы действительно хотите избежать одноэлементного типа, вы можете превратить WS в трейт с реализацией синглтона и ссылаться только на трейт в вашем пироге.

trait WS {
  def url(url: String): Request
}

object SingletonWS extends WS {
  def url(url: String) = ??? // actual implementation
}

И в твоем торте:

trait WSComponent {
  val ws: WS
}

object ApplicationContext extends WSComponent {
  val ws = SingletonWS
}

object TestContext extends WSComponent {
  val ws = mock[WS]
}
person gourlaysama    schedule 20.06.2013
comment
Я считаю, что для OP WS исходит из игровой библиотеки. Он определяется как объект и не смешивается с какими-либо чертами, представляющими операции, предоставляемые объектом. Этот вид делает тупиком для насмешек, и единственным решением, вероятно, является обертывание, затем делегирование, а затем насмешка над оберткой. - person cmbaxter; 20.06.2013
comment
cmbaxter правильно. WS исходит из игры и расширяет только Class. Тем не менее, gourlaysama прав в том, что если я использую только WS.type вместо Class[WS.type] (или Class[_ ‹: WS.type]), то все компилируется и методы есть. Спасибо, оба! - person Ben Wilhelm; 21.06.2013
comment
о, я не видел ссылку с play-framework. Тогда я думаю, что использование WS.type везде - единственный вариант. Я предполагаю, что mock[WS.type] тоже сработает. - person gourlaysama; 21.06.2013
comment
это скомпилируется, но не будет работать. mock от Mockito, и он не может издеваться над финалами. член типа объекта scala компилируется в окончательный класс. - person Chris DaMour; 20.02.2014

Вы можете попробовать определить трейт, который содержит вызовы из WS, которые вы используете, а затем простую импл-оболочку, делегирующую WS. Что-то вроде этого:

trait WSMethods{
  def url(str:String):Request
}

object WSWrapper extends WSMethods{
  def url(str:String) = WS.url(str)
}

Затем используйте его в трейте, который вы смешиваете с классами, которым он нужен:

trait WSClient{
  val ws:WSMethods
}

Как только вы это сделаете, это будет более издевательски. Это немного громоздко, но так оно и будет, когда объект не смешивается с чертой, определяющей его операции. Если бы люди из typesafe сделали это с WS, было бы легче издеваться. Кроме того, большинство фиктивных фреймворков (вероятно, все) будут блевать, если вы попробуете что-то вроде:

val m = mock[WS.type]
person cmbaxter    schedule 20.06.2013