【问题标题】:Specs2: how to test a class with more than one injected dependency?Specs2:如何测试具有多个注入依赖项的类?
【发布时间】:2016-03-13 14:34:39
【问题描述】:

播放 2.4 应用,使用 dependency injection 服务类。

我发现,当正在测试的服务类具有多个注入依赖项时,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
    }
  }

}

如果我从ReportService 中删除UserServiceSupportService 依赖项,则测试有效。但显然,依赖项存在于生产代码中是有原因的。 问题是,我该如何进行这项测试?

编辑:当尝试在 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"
)

【问题讨论】:

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


    【解决方案1】:

    我的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 配合使用?


    编辑:最后我用了一个“Injector”测试助手,几乎和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]
      // ...
    }
    

    【讨论】:

    • “玩得不好” ....是的,这很令人惊讶和沮丧...我花了整个上午尝试从 Play 2.4 -> 2.5 更新一些代码,终于意识到了这一点。跨度>
    【解决方案2】:

    如果你真的需要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

    尝试通过使用任何可用的构造函数来创建给定类的实例,如果该构造函数有参数,则尝试递归地实例化第一个参数。

    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)
    

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

    【讨论】:

    • 谢谢!至于运行时 DI,值得注意的是 new default approach 在 Play 中将事物连接在一起(正如我们在 Futurice 后端流程中讨论的那样)。我对此并没有真正的强烈意见,但我确实更喜欢我选择的最新版本框架中的默认推荐方法。
    • 在类似的注释中,我更喜欢使用 Play 的 API(play.api.inject,如 Eric's answer),而不是在我自己的代码中添加显式的 com.google.inject 依赖项。但最好了解两者,并且在实践中可能没有区别。
    【解决方案3】:

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

    【讨论】:

    • 谢谢。您想添加一个真正适用于 Play 的解决方案吗? Guice is the Play default;我没有兴趣改变它。我知道你从 Specs2 的角度来看这个,但我正在寻找一个实用的解决方案。 :) (我对这个堆栈很陌生,除了使用@Inject 之外没有做过任何 DI。)
    • 我现在没有 Play 项目,但 according to the documentation 这应该可以。
    • 感谢您的帮助!当前代码无法编译:injector.getInstance 处的“无法解析符号 getInstance”。要使用最新的 Play 和与我相同的设置快速设置项目,请尝试克隆:github.com/jonikarppinen/play-specs2-testbench
    • 感谢测试项目。我已经用一些工作代码编辑了答案。
    • 答案终于ok了吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-10-20
    • 1970-01-01
    • 2016-01-02
    • 2020-12-20
    • 1970-01-01
    相关资源
    最近更新 更多