Specs2:如何测试具有多个注入依赖项的类?

时间:2015-12-08 15:34:39

标签: scala playframework dependency-injection specs2 playframework-2.4

使用dependency injection播放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删除SupportServiceReportService依赖项,则测试有效。但显然,依赖关系在生产代码中是有原因的。 问题是,我该如何进行此测试?

编辑:当尝试在IntelliJ IDEA中运行测试时,同样的事情失败了,但是有不同的消息:“测试框架意外退出”,“这看起来像specs2异常......” ;见full output with stacktrace。我按照输出中的指示打开了Specs2 issue,但我不知道问题是在Play还是Specs2或其他地方。

我的库依赖项如下。 (我尝试指定Specs2版本explicitly,但这没有帮助。看起来我需要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 个答案:

答案 0 :(得分:7)

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

答案 1 :(得分:3)

如果你真的需要运行时依赖注入,那么最好使用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还是通过播放包装器。

看起来你好运ATM:The comment

  

尝试使用任何可用的构造函数创建给定类的实例,并尝试以递归方式实例化第一个参数(如果该构造函数有参数)。

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)

如果你不需要运行时依赖注入,那么最好使用蛋糕模式,并忘记全部反射。

答案 2 :(得分:2)

我的colleague提出了“低技术”的解决方法。在测试中,使用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的default test framework与Play的default DI mechanism效果不佳有什么好奇心呢?

编辑:最后,我选择了一个“注射器”测试助手,与Eric suggested几乎相同:

进样器:

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