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