Testing Play 2.4 controllers and their routes using Scala & Specs2

8/6/2015

A common requirement when testing controller methods is to test the things done by the router, such as parsing the request and verifying arguments.

For example:

Usually, we care only about one controller at a time. The trouble with the Routes class generated by the Play routes compiler is that it has dependencies on all controllers referenced by the routes, which, in the usual case, is all of them.

What we'd generally like to do is to spin up an application with just one real controller instance, which we provide, and run tests against it, like this:

class MyControllerSpec extends PlaySpecification with Mockito with Results {
  trait WithMocks extends Scope {
     val mockService = mock[MyService]
     val underTest = new MyController(mockService)
  }

  "MyController.index()" should {
    "return the result of its service in JSON format" in new WithMockControllers with WithMocks {
       mockService.mockedMethod returns ...
       val request = FakeRequest(GET, "/mycontroller/index")

       val response = route(request).get

       status(response) must be_==(OK)
       contentAsJson(response) must be_==( ... )
       //etc.
    }
  }

This is perfectly possible, it just needs some magic in the form of the WithMockControllers class, which pulls up the application using the generated Routes class with mocked controllers, apart from the controller under test, which is provided by the abstract underTest method and implemented in the WithMocks trait which we mix in.

This helper class allows unit testing controllers without the need to pull up actual instances of all controllers with all their dependencies, and just concentrate on the one controller you'd like to test.

import controllers.Assets
import org.specs2.execute.{AsResult, Result}
import org.specs2.mutable.Around
import org.specs2.specification.Scope
import play.api.ApplicationLoader.Context
import play.api._
import play.api.http.DefaultHttpErrorHandler
import play.api.inject.{BindingKey, Injector, SimpleInjector}
import play.api.routing.Router
import play.api.test.{Helpers, WithApplicationLoader}
import router.Routes

import scala.reflect.ClassTag
import scala.reflect.runtime.universe._

/**
 * This allows to unit test a controller with an actual router, while mocking all
 * dependencies on all the other controllers which are irrelevant to the test.
 */
abstract class WithMockControllers extends Around with Scope {
  private lazy val withLoader = new WithApplicationLoader(
    new MockApplicationLoader(underTest, otherComponents)) {}
  implicit lazy val app = withLoader.app

  /**
   * The concrete controller instance under test.
   */
  def underTest: Any

  /**
   * Can be overridden to provide any additional components to the injector that should 
   * be used instead of mocks.
   */
  def otherComponents: Map[Class[_], _] = Map()

  def around[T: AsResult](t: => T): Result = {
    Helpers.running(app)(AsResult.effectively(t))
  }
}

class MockApplicationLoader(controllerUnderTest: Any, otherComponents: Map[Class[_], _]) 
extends ApplicationLoader {
  override def load(context: Context): Application = {
    Logger.configure(context.environment)
    val components = new BuiltInComponentsFromContext(context) with MockComponents {
      override def realComponents = otherComponents + (controllerUnderTest.getClass -> controllerUnderTest)
    }
    components.application
  }
}

trait MockComponents extends BuiltInComponents {
  def realComponents: Map[Class[_], _]

  lazy val assets: Assets = injector.instanceOf[Assets]

  override lazy val injector: Injector = new SimpleInjector(new MockInjector, realComponents) + crypto + httpConfiguration

  lazy val router: Router = {
    val m = runtimeMirror(getClass.getClassLoader)
    val constructorWithoutPrefix = typeOf[Routes].decl(termNames.CONSTRUCTOR).alternatives(1).asMethod
    val paramsList = constructorWithoutPrefix.paramLists.head
    val routesParamTypes = paramsList.map(_.typeSignature).map(m.runtimeClass).tail
    val routesArgs = routesParamTypes.map(injector.instanceOf(_))

    val constructorWithPrefix = typeOf[Routes].decl(termNames.CONSTRUCTOR).alternatives.head.asMethod
    val cm = m.reflectClass(typeOf[Routes].typeSymbol.asClass)
    val constructorFunction = cm.reflectConstructor(constructorWithPrefix)
    val routes = constructorFunction(DefaultHttpErrorHandler :: routesArgs ::: List("/contextOfApplication"): _*)
    routes.asInstanceOf[Router]
  }

}

class MockInjector extends Injector {
  def instanceOf[T](implicit ct: ClassTag[T]) = org.mockito.Mockito.mock(implicitly[ClassTag[T]].runtimeClass).asInstanceOf[T]
  def instanceOf[T](clazz: Class[T]) = org.mockito.Mockito.mock(clazz)
  def instanceOf[T](key: BindingKey[T]) = instanceOf(key.clazz)
}

We use a custom application loader, which in turn overrides the creation of the router with our own logic. This logic uses reflection to find the constructor of the generated Routes class, and instantiates it using a mock instance for every dependency, except the actual controller under test and the HttpErrorHandler. It's a bit messy and relies on reflection, but it looks clean when used, and it works.

Comments