Testing scala classes and controllers
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
.