Specs2: how to test a class with more than one injected dependency?
Asked Answered
N

3

7

Play 2.4 app, using dependency injection for service classes.

I found that Specs2 chokes when a service class being tested has more than one injected dependency. It fails with "Can't find a constructor for 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

Production code, stripped to bare minimum to reproduce this problem:

package services

import javax.inject.Inject

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

class UserService {  
   // ...  
}

class SupportService {  
   // ...  
}

Test code:

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

}

If I remove either UserService or SupportService dependency from ReportService, the test works. But obviously the dependencies are in the production code for a reason. Question is, how do I make this test work?

Edit: When trying to run the test inside IntelliJ IDEA, the same thing fails, but with different messages: "Test framework quit unexpectedly", "This looks like a specs2 exception..."; see full output with stacktrace. I opened a Specs2 issue as instructed in the output, though I have no idea if the problem is in Play or Specs2 or somewhere else.

My library dependencies below. (I tried specifying Specs2 version explicitly, but that didn't help. Looks like I need specs2 % Test as is, for Play's test classes like WithApplication to work.)

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"
)
Nakasuji answered 8/12, 2015 at 15:34 Comment(0)
A
9

There is limited support for dependency injection in specs2, mostly for execution environments or command-line arguments.

There is nothing preventing you from just using a lazy val and your favourite injection framework:

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

  ...
}

With Play and Guice, you could have a test helper such as this:

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]
}
Antitrust answered 9/12, 2015 at 2:28 Comment(6)
Thanks. Do you want to add a solution that actually works with Play? Guice is the Play default; I have no interest in changing it. I know you look at this from Specs2 point of view, but I'm looking for a practical solution. :) (I'm quite new to this stack, and haven't done any DI beyond using @Inject.)Nakasuji
I don't have a Play project with me right now but according to the documentation this should work.Antitrust
Thanks for trying to help! Current code doesn't compile: "cannot resolve symbol getInstance" at injector.getInstance. To quickly set up a project with latest Play and same setup as I have, try cloning this: github.com/jonikarppinen/play-specs2-testbenchNakasuji
Thanks for the test project. I have edited the answer with some working code.Antitrust
Is the answer finally ok?Antitrust
Yes Eric the code you provided works fine. Thank you for sharingTega
E
3

If you really need runtime dependency injection, then it's better to use Guice loading, I guess:

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

Alternatively you can implement Inject object as

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

It depends whether you want to use Guice directly, or thru play wrappers.


Looks like you are out of luck ATM: The comment says

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)

i.e. Specs2 doesn't provide own DI out-of-the box,


Or you can reimplement the functionality yourself, if Guice isn't working for you.

App code:

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
}

Test code

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

And that expectedly fails with

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

If you don't need runtime dependency injection, then it's better to use cake pattern, and forget reflection all-together.

Egmont answered 9/12, 2015 at 1:9 Comment(2)
Thanks! As for runtime DI, worth noting that it is the new default approach to wiring things together in Play (as we discussed in Futurice backend flow). I don't really have strong opinions about this, but I do prefer the default, recommended approach in the latest version of the framework I've chosen.Nakasuji
On a similar note, I slightly prefer using Play's API (play.api.inject as in Eric's answer) to adding explicit com.google.inject dependency in my own code. But good to be aware of both, and there's probably no difference in practice.Nakasuji
N
2

My colleague suggested a "low-tech" workaround. In the test, instantiate service classes with new:

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

This also works:

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

Feel free to post more elegant solutions. I've yet to see a simple DI solution that works (with Guice, Play's default).

Does anyone else find it curious that Play's default test framework does not play well with Play's default DI mechanism?


Edit: In the end I went with an "Injector" test helper, almost the same as what Eric suggested:

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

Test:

class ReportServiceSpec extends Specification {
  val service = Injector.inject[ReportService]
  // ...
}
Nakasuji answered 9/12, 2015 at 10:53 Comment(1)
"does not play well" .... Yes, it's quite surprising and frustrating... I spent all morning trying to update some code from Play 2.4 -> 2.5 and finally realized this.Dibucaine

© 2022 - 2024 — McMap. All rights reserved.