Testing Services

§Testing Services

§Running tests

Tests can be run from sbt, or from your IDE. Running tests from your IDE will be specific to your IDE, so we’ll focus here on how to run tests from sbt.

  • To run all tests, run test.
  • To run only one test class, run testOnly followed by the name of the class i.e. testOnly com.example.MyTest. Wildcards are supported, so you can say testOnly *.MyTest, for example.
  • To run only the tests that cover code that has changed, run testQuick.
  • To run tests continually, run a command with a tilde in front, i.e. ~testQuick.

§Testing libraries

You can use any testing framework with Lagom, popular ones include ScalaTest and Specs2. If you’re not sure which to use, or don’t have a preference, we use ScalaTest for testing Lagom itself, and we’ll document it here.

In addition to a test framework, Lagom also provides a helper library for testing common Lagom components, called the Lagom testkit.

To use your preferred test framework and the Lagom testkit, you’ll need to add them to your library dependencies, like so:

libraryDependencies ++= Seq(
  lagomScaladslTestKit,
  "org.scalatest" %% "scalatest" % "3.0.1" % Test
)

You may want to use ScalaTest in multiple places in your build, so often it’s a good idea to create a single val to hold and, which means you can just reference that val from each place that you need it, rather than having to retype the group id, artifact id and version each time. This can be done like this:

val scalaTest = "org.scalatest" %% "scalatest" % "3.0.1" % "test"

Then you can use it in your libraryDependencies by simply referring to it:

libraryDependencies ++= Seq(
  lagomScaladslTestKit,
  scalaTest
)

When using Cassandra the tests must be forked, which is enabled by adding the following in your project’s build:

lazy val `hello-impl` = (project in file("hello-impl"))
  .enablePlugins(LagomScala)
  .settings(lagomForkedTestSettings: _*)
  .settings(
    // ...
  )

§How to test one service

Lagom provides support for writing functional tests for one service in isolation. The service is running in a server and in the test you can interact with it using its service client, i.e. calls to the service API. These utilities are defined in ServiceTest.

Here’s what a simple test may look like:

import com.lightbend.lagom.scaladsl.server.LocalServiceLocator
import com.lightbend.lagom.scaladsl.testkit.ServiceTest
import org.scalatest.AsyncWordSpec
import org.scalatest.Matchers

class HelloServiceSpec extends AsyncWordSpec with Matchers {

  "The HelloService" should {
    "say hello" in ServiceTest.withServer(ServiceTest.defaultSetup) { ctx =>
      new HelloApplication(ctx) with LocalServiceLocator
    } { server =>
      val client = server.serviceClient.implement[HelloService]

      client.sayHello.invoke("Alice").map { response =>
        response should ===("Hello Alice!")
      }
    }
  }
}

There are a few points to note about this code:

  • The test is using ScalaTest’s asynchronous test support. The actual test itself returns a future, and ScalaTest ensures that that future is handled appropriately.
  • withServer takes three parameters. The first is a setup parameter, which can be used to configure how the environment should be setup, for example, it can be used to start Cassandra. The second is a constructor for a LagomApplication, which is where we construct the application, and the third is a block to run that takes the started server and runs the actual test.
  • When we construct the LagomApplication, we mix in LocalServiceLocator. This provides a local service locator which will resolve just the services that our application is running itself, and is how the service client we construct knows where to find our running service.
  • In the test callback, we implement a service client, which we can then use to talk to our service.

The spec above will start a server for each test, which is often handy because it guarantees a clean state between each test. Sometimes however starting a server for each test can be prohibitively expensive, especially when databases are involved. In these cases it may be better to share the server between all tests in a suite. To do this, startServer can be used instead, invoking stop in a after suite callback:

import com.lightbend.lagom.scaladsl.server.LocalServiceLocator
import com.lightbend.lagom.scaladsl.testkit.ServiceTest
import org.scalatest.AsyncWordSpec
import org.scalatest.Matchers
import org.scalatest.BeforeAndAfterAll

class HelloServiceSpec extends AsyncWordSpec with Matchers with BeforeAndAfterAll {

  lazy val server = ServiceTest.startServer(ServiceTest.defaultSetup) { ctx =>
    new HelloApplication(ctx) with LocalServiceLocator
  }
  lazy val client = server.serviceClient.implement[HelloService]

  "The HelloService" should {
    "say hello" in {
      client.sayHello.invoke("Alice").map { response =>
        response should ===("Hello Alice!")
      }
    }
  }

  protected override def beforeAll() = server

  protected override def afterAll() = server.stop()
}

Dependencies to other services must be replaced by stub or mock implementations by overriding them in your LagomApplication constructor callback. If we were writing a test for the HelloService and it had a dependency on a GreetingService we must create a stub implementation of the GreetingService that can be used for the test without running the real greeting service. It might look like this:

lazy val server = ServiceTest.startServer(ServiceTest.defaultSetup) { ctx =>
  new HelloApplication(ctx) with LocalServiceLocator {

    override lazy val greetingService = new GreetingService {
      override def greeting = ServiceCall { _ =>
        Future.successful("Hello")
      }
    }
  }
}

The server is by default running with pubsub, cluster and persistence features disabled. You may want to enable clustering in the Setup:

lazy val server = ServiceTest.startServer(
  ServiceTest.defaultSetup.withCluster()
) { ctx =>
  new HelloApplication(ctx) with LocalServiceLocator
}

If your service needs persistence you will need to enable it explicitly. This can be done by enabling Cassandra or JDBC, depending on which kind of persistence is used by your service. In any case, Lagom persistence requires clustering, so when enabling one or another, cluster will also be enabled automatically.

You can’t enable both (Cassandra and JDBC) at the same time for testing, which could be a problem if you are mixing persistence for write and read side. If you are using Cassandra for write-side and JDBC for read-side, just enable Cassandra.

To enable Cassandra Persistence:

lazy val server = ServiceTest.startServer(
  ServiceTest.defaultSetup.withCassandra()
) { ctx =>
  new HelloApplication(ctx) with LocalServiceLocator
}

To enable JDBC Persistence:

lazy val server = ServiceTest.startServer(
  ServiceTest.defaultSetup.withJdbc()
) { ctx =>
  new HelloApplication(ctx) with LocalServiceLocator
}

There’s no way to explicitly enable or disable pubsub. When cluster is enabled (either explicitly or transitively via enabling Cassandra or JDBC), pubsub will be available.

§How to use TLS on tests

To open an SSL port on the TestServer used in your tests, you may enable SSL support using withSsl:

Setup.defaultSetup.withSsl()

Enabling SSL will automatically open a new random port and provide an javax.net.ssl.SSLContext on the TestServer. Lagom doesn’t provide any client factory that allows sending requests to the HTTPS port at the moment. You should create an HTTP client using Play-WS, Akka-HTTP or Akka-gRPC. Then, use the httpsPort and the sslContext provided by the testServer instance to send the request. Note that the SSLContext provided is built by Lagom’s testkit to trust the testServer certificates. Finally, because the server certificate is issued for CN=localhost you will have to make sure that’s the authority on the requests you generate, otherwise the server may decline and fail the request. At the moment it is not possible to setup the test server with different SSL Certificates.

"complete a WS call over HTTPS" in {
  val setup = defaultSetup.withSsl()
  ServiceTest.withServer(setup)(new TestTlsApplication(_)) { server =>
    implicit val ctx = server.application.executionContext
    // To explicitly use HTTPS on a test you must create a client of your
    // own and make sure it uses the provided SSLContext
    val wsClient = buildCustomWS(server.clientSslContext.get)
    // use `localhost` as authority
    val url = s"https://localhost:${server.playServer.httpsPort.get}/api/sample"
    val response =
      wsClient
        .url(url)
        .get()
        .map {
          _.body[String]
        }
    whenReady(response, timeout) { r =>
      r should be("sample response")
    }
  }
}

§How to test several services

Lagom will provide support for writing integration tests that involve several interacting services. This feature is not yet implemented.

§How to test streamed request/response

Let’s say we have a service that has streaming request and/or response parameters. For example an EchoService like this:

trait EchoService extends Service {
  def echo: ServiceCall[Source[String, NotUsed], Source[String, NotUsed]]

  override def descriptor = {
    import Service._
    named("echo").withCalls(
      call(echo)
    )
  }
}

When writing tests for that the Akka Streams TestKit is very useful. We use the Streams TestKit together with the Lagom ServiceTest utilities:

"The EchoService" should {
  "echo" in {
    // Use a source that never terminates (concat Source.maybe) so we
    // don't close the upstream, which would close the downstream
    val input = Source(List("msg1", "msg2", "msg3")).concat(Source.maybe)
    client.echo.invoke(input).map { output =>
      val probe = output.runWith(TestSink.probe(server.actorSystem))
      probe.request(10)
      probe.expectNext("msg1")
      probe.expectNext("msg2")
      probe.expectNext("msg3")
      probe.cancel
      succeed
    }
  }
}

Read more about it in the documentation of the Akka Streams TestKit.

§How to test a persistent entity

Persistent Entities can be used in the service tests described above. In addition to that you should write unit tests using the PersistentEntityTestDriver, which will run the PersistentEntity without using a database.

This is described in the Persistent Entity documentation.

Found an error in this documentation? The source code for this page can be found here. Please feel free to edit and contribute a pull request.