avatar

Andres Jaimes

Testing scala classes and controllers

By Andres Jaimes

- 7 minutes read - 1309 words

This article discusses different approaches for testing classes, services, and PlayFramework controllers using scalatest.

Setting up the project dependencies

First step is to add to build.sbt the next dependency:

"org.scalatest" %% "scalatest" % "3.2.2"

The following template unit test uses the WordSpec style which offers a natural way for writing tests. scalatest offers more styles, but I find this one more expressive.

PlaySpec (found in the PaylFramework) extends WordSpec and has a similar behavior.

package testing

import org.scalatest.BeforeAndAfterAll
import org.scalatest.wordspec.AnyWordSpec

class MyTemplateSpec extends AnyWordSpec with BeforeAndAfterAll {

  import MyTemplateSpec._

  override def beforeAll(): Unit = ???

  override def afterAll(): Unit = ???

  "An item to test" when {
    "a function is called or an event is triggered" should {
      "do something if a condition is met" in {
         assert(...)
      }

      "throw an exception if a condition is not met" in {
        intercept[Exception] {
          f()
        }
      }
    }
  }

}

object MyTemplateSpec {

}

Mockito

Add the MockitoSugar trait to the class:

import org.mockito.Mockito
import org.scalatest.mock.MockitoSugar

class MyClassSpec extends AnyWordSpec with BeforeAndAfterAll with MockitoSugar
// First, create the mock object
val mockCollaborator = mock[Collaborator]

// Create the class under test and pass the mock to it
classUnderTest = new ClassUnderTest
classUnderTest.addListener(mock)

// Use the class under test
classUnderTest.addDocument("Document", new Array[Byte](0))
classUnderTest.addDocument("Document", new Array[Byte](0))
classUnderTest.addDocument("Document", new Array[Byte](0))
classUnderTest.addDocument("Document", new Array[Byte](0))

// Then verify the class under test used the mock object as expected
verify(mockCollaborator).documentAdded("Document")
verify(mockCollaborator, times(3)).documentChanged("Document")

Here is another example that uses mockito to send a body request using the play framework.

import org.mockito.Mockito
import org.scalatest.mock.MockitoSugar
import play.api.test.Helpers._

...

"some test" in {
  val app: Application =
          new GuiceApplicationBuilder()
            .build()

  val controller = app.injector.instanceOf[MyController]
  val person = Person("Name", 30)
  val request = mock[Request[JsValue]]
  Mockito.when(request.body) thenReturn Json.toJson(person)

  val response = await(controller.create()(request))
  response.header.status mustBe INTERNAL_SERVER_ERROR
  response.body.contentType mustBe Some("application/json")
  (contentAsJson(Future.successful(response)) \ "message").get.as[String] must include("Some description")
}

...

case class Person(name: String, age: Int)

We are expecting the previous controller call to return the following response:

{
  "message": "Some description and probably something else"
}

Mockito.when can be to receive and return generic or specific parameters as well. However, it’s important not to mix both types of parameters or an exception will be thrown. Here’s an example using generic parameters, which takes an additional implicit generic parameter and returns a boolean:

Mockito.when(someService.function(any[String], any[Int], any[Option[String]])(any[String])) thenReturn Future.successful(true)

...

verify(someService, times(1)).function(any[String], any[Int], any[Option[String]])(any[String])

We don’t have to build a full injector to test a controller. We can instantiate a class and mock any required parameters. For example:

val appConfig = AppConfig(ConfigFactory.load)
val service1 = mock[SomeService1]
val service2 = mock[SomeService2]

...

"some test" in {
  val controller = new SomeController(appConfig, someService1, someService2)
  // we can now use it
}

Testing a service and database access

I like this method because it does not use mocks, but a real database. This class extends from PlaySpec.

import play.api.test.Helpers._
import play.api.Application
import play.api.inject.guice.GuiceApplicationBuilder

val app: Application =
    new GuiceApplicationBuilder()
      .build()

val appConfig = AppConfig(ConfigFactory.load)
// or val appConfig = app.injector.instanceOf[AppConfig]
val someService = app.injector.instanceOf[SomeService]

"insert a record data into the database" in {
  val person = Person(
    name = "Name",
    age = 30
  )
  try {
    val insert: Person = await(someService.insert(person))
    val result: Option[Person] = await(someService.findById(person.id))
    assert(result.isDefined && (result.get.name == person.name))
  } finally {
    await(someService.delete(person.id)) // clean up the db
  }
}

Application injection

We can use injection for creating and overriding/mocking classes. The following example uses overrides for mocking a service class using a dummy one we have created that returns expected values, and configure for overriding values in our application.conf file. We can have as many overrides and configure calls as needed.

import play.api.Application
import play.api.inject.bind
import play.api.inject.guice.GuiceApplicationBuilder

...

val app: Application = new GuiceApplicationBuilder()
  .overrides(bind[RealService].to[DummyService])
  .configure("config.entry" -> s"dummy-value")
  .build()

This way of overriding values is very useful for testing controllers.

Testing controllers

A simple example:

import play.api.test._

...

val app: Application = new GuiceApplicationBuilder().build()

"get a response that contains our code" in  {
  val code = "ABC-123"
  val result = route(app, FakeRequest(GET, s"/path/page?code=$code")).map(contentAsString(_))
  result.map(s => s must (include(code)))
}

We can replace contentAsString with contentAsJson for json testing.

Adding Server.withRouter to our tests

A more complex example that creates a mock server that responds to internal requests from our endpoint.

import play.api.Application
import play.api.inject.guice.GuiceApplicationBuilder
import play.api.mvc.{Action, Results}
import play.api.test._
import play.core.server.Server

...

"verify we are calling three endpoints service" in  {
  Server.withRouter() {
    // define our fake remote endpoints
    case play.api.routing.sird.POST(accessTokenEndpoint) => Action {
      Results.Ok.sendResource("auth-token.json")
    }
    case play.api.routing.sird.GET(userEndpoint) => Action {
      Results.Ok.sendResource("user-info.json")
    }
    case play.api.routing.sird.DELETE(ordersEndpoint) => Action {
      Results.Ok("")
    }
  } { implicit port =>
    WsTestClient.withClient { client =>
      // configure our app to use our fake remote endpoints
      val app: Application = new GuiceApplicationBuilder()
        .configure("some.tokensEndpoint" -> s"http://localhost:${port.value.toString()}/path/tokens")
        .configure("some.usersEndpoint" -> s"http://localhost:${port.value.toString()}/path/users")
        .configure("some.ordersEndpoint" -> s"http://localhost:${port.value.toString()}/path/orders")
        .build()
      val controller = app.injector.instanceOf[MyController]

      // the following function internally calls the three remote endpoints
      val response = controller.someEndpoint()(FakeRequest(POST, s"/start")
                             .withFormUrlEncodedBody("code" -> "abc-123", "user" -> "user-123"))
      status(response) mustBe OK
      contentAsString(response) must (include("The process is complete."))
    }
  }
}

Reusing an application object with tests that use Server.withRouter

Creating an application instance is expensive and if not reused, it can significatively slow down the testing process. If we want to reuse the application object across multiple tests, we have to:

  • specify a port, and don’t let Server.withRouter assign a random one.
  • pass the port number during our application instance creation.
  • move Server.withRouter to a function that receives the port number and our test as parameters.

See the folowing example:

@@ -0,0 +1,92 @@
import org.scalatestplus.play.PlaySpec
import play.api.{Application, Mode}
import play.api.inject.guice.GuiceApplicationBuilder
import play.api.mvc.{Action, Results}
import play.api.test.Helpers._
import play.api.test.WsTestClient
import play.core.server.{Server, ServerConfig}

class MyClassSpec extends PlaySpec {

  import MyClassSpec._
  
  "MyClass" when {

    val port = 54321 // specify a port and use it in every place
    val app: Application = new GuiceApplicationBuilder()
      .configure("myConfig.someEndpoint" -> s"http://localhost:$port/v2/some-path")
      .build()
    val myClass: MyClass = app.injector.instanceOf[MyClass]
      
    "receives a response from someEndpoint" should {

      "return a valid item if some condition is met" in 
        withServer(port) { _ =>
          val result = await(myClass.someFunction("someParam1"))
          assert(someCondition === true)
        }
      
      "return an empty item if some condition is not met" in
        withServer(port) { _ =>
          val result = await(myClass.someFunction("someParam2"))
          assert(someCondition === true)
        }
      
    }
    
  }
  
}

object MyClassSpec {
  
  def withServer(port: Int)(block: Unit => Unit): Unit = {
    import play.api.routing.sird._
    Server.withRouter(ServerConfig(port = Some(port), mode = Mode.Test)) {
      case GET(p"/v2/some-path/$someParam") => Action {
        someParam match {
          case "someParam1" => 
            Results.Ok.sendResource("valid-response.json")
          case "someParam2" =>
            Results.NotFound.sendResource("empty-response.json")
        }
      }
    } { implicit port => 
      WsTestClient.withClient { client =>
        block(())
      }
    }
  }

}

This will call multiple times Server.withRouter, resulting in multiple instances, but the process is still very fast. What we really wanted to avoid was creating multiple application instances which slows down the process considerably.

Common errors

When testing Action.async endpoints there are times when we have seen an error similar to the following:

Type mismatch. Required: Future[Result] Found: Iteratee[Array[Byte], Result]

As explained by tjdett, the problem happens because play.api.mvc.Action[A] has two apply methods:

// What you're hoping for
def apply(request: Request[A]): Future[Result]

// What actually gets called
def apply(rh: RequestHeader): Iteratee[Array[Byte], Result]

Request[A] extends from RequestHeader, and the A in this case makes all the difference.

When you use ActionBuilder with a BodyParser[A], you create an Action[A]. As a result, you’ll need a Request[A] to test. parse.json returns a BodyParser[JsValue], so you need a Request[JsValue].

"should be valid" in {
  val controller = new TestController()
  val body: JsValue = ??? // Change this once your test compiles

  // Could do these lines together, but this shows type signatures
  val request: Request[JsValue] = FakeRequest().withBody(body)
  val result: Future[Result] = controller.index().apply(request)

  /* assertions */
}

A longer example can look like this:

"my test" in {
  service.someFunction(any) returns Future.successful(())

  val request: Request[JsValue] = FakeRequest()
    .withSession(session:_*)
    .withBody(Json.toJson(SomeCaseClass(prop1, prop2)))
  val result = controller.someEndpoint()(request)

  status(result) shouldBe OK
  contentAsString(result) should include ("Some text")

  verify(service, times(1)).someFunction(any)
}

Another source of error can be using withJsonBody instead of withBody.

References