Specs2:如何测试具有多个注入依赖项的 class?
Specs2: how to test a class with more than one injected dependency?
播放 2.4 应用程序,使用 dependency injection 服务 classes.
我发现当正在测试的服务 class 具有多个注入的依赖项时,Specs2 会阻塞。它失败并显示“找不到 class 的构造函数 ...”
$ 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
}
}
}
如果我从 ReportService
中删除 UserService
或 SupportService
依赖项,则测试有效。但显然,依赖项在生产代码中是有原因的。 问题是,我该如何进行这个测试?
编辑:尝试运行 IntelliJ IDEA 中的测试时,同样的事情失败了,但消息不同:"Test framework quit unexpectedly"、"This looks like a specs2 exception...";按照输出中的说明查看 full output with stacktrace. I opened a Specs2 issue,但我不知道问题出在 Play 或 Specs2 还是其他地方。
下面是我的库依赖项。 (我尝试指定 Specs2 版本 explicitly,但这没有帮助。看起来我需要 specs2 % Test
原样,以便 Play 的测试 classes 像 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"
)
如果真的需要runtime依赖注入,那还是用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 还是通过 play wrappers。
看起来你运气不好 ATM:The comment 说
Try to create an instance of a given class by using whatever constructor is available and trying to instantiate the first parameter recursively if there is a parameter for that constructor.
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)
如果不需要运行时依赖注入,那还是用cake pattern比较好,反射全忘了
specs2 中对依赖注入的支持有限,主要是针对执行环境或命令行参数。
没有什么可以阻止您使用 lazy val
和您最喜欢的注入框架:
class MySpec extends Specification with Inject {
lazy val reportService = inject[ReportService]
...
}
使用 Play and 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]
}
我的 colleague 建议 "low-tech" 解决方法。在测试中,用 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)
// ...
}
欢迎 post 更优雅的解决方案。我还没有看到简单 DI 解决方案有效(使用 Guice,Play 的默认设置)。
还有其他人对 Play 的 default test framework does not play well with Play's default DI mechanism 感到好奇吗?
编辑:最后我用了一个"Injector"测试助手,和:
几乎一样
喷油器:
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]
// ...
}
播放 2.4 应用程序,使用 dependency injection 服务 classes.
我发现当正在测试的服务 class 具有多个注入的依赖项时,Specs2 会阻塞。它失败并显示“找不到 class 的构造函数 ...”
$ 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
}
}
}
如果我从 ReportService
中删除 UserService
或 SupportService
依赖项,则测试有效。但显然,依赖项在生产代码中是有原因的。 问题是,我该如何进行这个测试?
编辑:尝试运行 IntelliJ IDEA 中的测试时,同样的事情失败了,但消息不同:"Test framework quit unexpectedly"、"This looks like a specs2 exception...";按照输出中的说明查看 full output with stacktrace. I opened a Specs2 issue,但我不知道问题出在 Play 或 Specs2 还是其他地方。
下面是我的库依赖项。 (我尝试指定 Specs2 版本 explicitly,但这没有帮助。看起来我需要 specs2 % Test
原样,以便 Play 的测试 classes 像 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"
)
如果真的需要runtime依赖注入,那还是用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 还是通过 play wrappers。
看起来你运气不好 ATM:The comment 说
Try to create an instance of a given class by using whatever constructor is available and trying to instantiate the first parameter recursively if there is a parameter for that constructor.
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)
如果不需要运行时依赖注入,那还是用cake pattern比较好,反射全忘了
specs2 中对依赖注入的支持有限,主要是针对执行环境或命令行参数。
没有什么可以阻止您使用 lazy val
和您最喜欢的注入框架:
class MySpec extends Specification with Inject {
lazy val reportService = inject[ReportService]
...
}
使用 Play and 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]
}
我的 colleague 建议 "low-tech" 解决方法。在测试中,用 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)
// ...
}
欢迎 post 更优雅的解决方案。我还没有看到简单 DI 解决方案有效(使用 Guice,Play 的默认设置)。
还有其他人对 Play 的 default test framework does not play well with Play's default DI mechanism 感到好奇吗?
编辑:最后我用了一个"Injector"测试助手,和
喷油器:
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]
// ...
}