Specs2: как протестировать класс с более чем одной внедренной зависимостью?

Приложение Play 2.4, использующее внедрение зависимостей для классов обслуживания.

Я обнаружил, что Specs2 задыхается, когда тестируемый класс обслуживания имеет более одной введенной зависимости. Ошибка "Не удается найти конструктор для класса..."

$ test-only services.ReportServiceSpec
[error] Can't find a constructor for class services.ReportService
[error] Error: Total 1, Failed 0, Errors 1, Passed 0
[error] Error during tests:
[error]         services.ReportServiceSpec
[error] (test:testOnly) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 2 s, completed Dec 8, 2015 5:24:34 PM

Рабочий код, сокращенный до минимума, чтобы воспроизвести эту проблему:

package services

import javax.inject.Inject

class ReportService @Inject()(userService: UserService, supportService: SupportService) {  
   // ...  
}

class UserService {  
   // ...  
}

class SupportService {  
   // ...  
}

Тестовый код:

package services

import javax.inject.Inject

import org.specs2.mutable.Specification

class ReportServiceSpec @Inject()(service: ReportService) extends Specification {

  "ReportService" should {
    "Work" in {
      1 mustEqual 1
    }
  }

}

Если я удалю либо UserService или же SupportService зависимость от ReportServiceТест работает. Но очевидно, что зависимости находятся в производственном коде по причине. Вопрос в том, как мне заставить этот тест работать?

Редактировать: при попытке запустить тест внутри IntelliJ IDEA происходит сбой той же вещи, но с разными сообщениями: "Тестовый фреймворк неожиданно завершил работу", "Это похоже на исключение specs2..."; увидеть полный вывод с помощью трассировки стека. Я открыл проблему Specs2, как указано в выходных данных, хотя я понятия не имею, если проблема в Play или Specs2 или где-то еще.

Мои библиотечные зависимости ниже. (Я попытался указать версию Specs2 явно, но это не помогло. Похоже, мне нужно specs2 % Test как есть, для тестовых классов Play, таких как WithApplication работать.)

resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases"
libraryDependencies ++= Seq(
  specs2 % Test,
  jdbc,
  evolutions,
  filters,
  "com.typesafe.play" %% "anorm" % "2.4.0",
  "org.postgresql" % "postgresql" % "9.4-1205-jdbc42"
)

3 ответа

Решение

Ограниченная поддержка внедрения зависимостей в specs2, в основном для сред выполнения или аргументов командной строки.

Ничто не мешает вам просто использовать lazy val и ваш любимый инъекционный каркас:

class MySpec extends Specification with Inject {
  lazy val reportService = inject[ReportService]

  ...
}

С Play и Guice у вас может быть помощник по тестированию, такой как этот:

import play.api.inject.guice.GuiceApplicationBuilder
import scala.reflect.ClassTag    

trait Inject {
  lazy val injector = (new GuiceApplicationBuilder).injector()

  def inject[T : ClassTag]: T = injector.instanceOf[T]
}

Если вам действительно нужно внедрение зависимостей во время выполнения, то лучше использовать загрузку Guice, я думаю:

package services

import org.specs2.mutable.Specification

import scala.reflect.ClassTag
import com.google.inject.Guice

// Something you'd like to share between your tests
// or maybe not
object Inject {
  lazy val injector = Guice.createInjector()

  def apply[T <: AnyRef](implicit m: ClassTag[T]): T = 
    injector.getInstance(m.runtimeClass).asInstanceOf[T]
}

class ReportServiceSpec  extends Specification {
  lazy val reportService: ReportService = Inject[ReportService]

  "ReportService" should {
    "Work" in {
      reportService.foo mustEqual 2
    }
  }
}

В качестве альтернативы вы можете реализовать Inject объект как

import scala.reflect.ClassTag
import play.api.inject.guice.GuiceApplicationBuilder  

object Inject {
  lazy val injector = (new GuiceApplicationBuilder).injector()
  def apply[T : ClassTag]: T = injector.instanceOf[T]
}

Это зависит от того, хотите ли вы использовать Guice напрямую или через оболочки игры.


Похоже, вам не повезло

Попробуйте создать экземпляр данного класса, используя любой доступный конструктор, и попытайтесь рекурсивно создать первый параметр, если для этого конструктора есть параметр.

val constructors = klass.getDeclaredConstructors.toList.filter(_.getParameterTypes.size <= 1).sortBy(_.getParameterTypes.size)

т.е. Specs2 не предоставляет собственный DI из коробки,


Или вы можете переопределить функциональность самостоятельно, если Guice не работает для вас.

Код приложения:

package services

import javax.inject.Inject

class ReportService @Inject()(userService: UserService, supportService: SupportService) {
  val foo: Int = userService.foo + supportService.foo
}

class UserService  {  
   val foo: Int = 1
}
class SupportService {  
    val foo: Int = 41
}

Тестовый код

package services

import org.specs2.mutable.Specification

import scala.reflect.ClassTag
import java.lang.reflect.Constructor

class Trick {
  val m: ClassTag[ReportService] = implicitly
  val classLoader: ClassLoader = m.runtimeClass.getClassLoader

  val trick: ReportService = Trick.createInstance[ReportService](m.runtimeClass, classLoader)
}

object Trick {
  def createInstance[T <: AnyRef](klass: Class[_], loader: ClassLoader)(implicit m: ClassTag[T]): T = {
    val constructors = klass.getDeclaredConstructors.toList.sortBy(_.getParameterTypes.size)
    val constructor = constructors.head

    createInstanceForConstructor(klass, constructor, loader)
  }

  private def createInstanceForConstructor[T <: AnyRef : ClassTag]
    (c: Class[_], constructor: Constructor[_], loader: ClassLoader): T = {
    constructor.setAccessible(true)

    // This can be implemented generically, but I don't remember how to deal with variadic functions
    // generically. IIRC even more reflection.
    if (constructor.getParameterTypes.isEmpty)
      constructor.newInstance().asInstanceOf[T]

    else if (constructor.getParameterTypes.size == 1) {
      // not implemented
      null.asInstanceOf[T]
    } else if (constructor.getParameterTypes.size == 2) {
      val types = constructor.getParameterTypes.toSeq
      val param1 = createInstance(types(0), loader)
      val param2 = createInstance(types(1), loader)
      constructor.newInstance(param1, param2).asInstanceOf[T]
    } else {
      // not implemented
      null.asInstanceOf[T]
    }
  }
}

// NB: no need to @Inject here. The specs2 framework does it for us.
// It sees spec with parameter, and loads it for us.
class ReportServiceSpec (trick: Trick) extends Specification {
  "ReportService" should {
    "Work" in {
      trick.trick.foo mustEqual 2
    }
  }
}

И это, как ожидается, не с

[info] ReportService should
[error]   x Work
[error]    '42' is not equal to '2' (FooSpec.scala:46)

Если вам не требуется внедрение зависимостей во время выполнения, лучше использовать шаблон тортов и полностью забыть о рефлексии.

Мой коллега предложил "низкотехнологичный" обходной путь. В тесте создайте экземпляры классов обслуживания с помощью new:

class ReportServiceSpec extends Specification {
  val service = new ReportService(new UserService, new SupportService)
  // ...
}

Это также работает:

class ReportServiceSpec @Inject()(userService: UserService) extends Specification {
  val service = new ReportService(userService, new SupportService) 
  // ...    
}

Не стесняйтесь размещать более элегантные решения. Я еще не видел простое решение DI, которое работает (с Guice, по умолчанию Play).

Кто-нибудь еще находит любопытным, что тестовая среда по умолчанию в Play не очень хорошо работает с механизмом DI по умолчанию в Play?


Изменить: В конце концов я пошел с помощником по тестированию "Инжектор", почти так же, как то, что предложил Эрик:

Инжектор:

package testhelpers

import play.api.inject.guice.GuiceApplicationBuilder    
import scala.reflect.ClassTag

/**
 * Provides dependency injection for test classes.
 */
object Injector {
  lazy val injector = (new GuiceApplicationBuilder).injector()

  def inject[T: ClassTag]: T = injector.instanceOf[T]
}

Тестовое задание:

class ReportServiceSpec extends Specification {
  val service = Injector.inject[ReportService]
  // ...
}
Другие вопросы по тегам