§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 saytestOnly *.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.matchers.should.Matchers
import org.scalatest.wordspec.AsyncWordSpec
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 inLocalServiceLocator
. 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.matchers.should.Matchers
import org.scalatest.wordspec.AsyncWordSpec
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 actorSystem = server.application.actorSystem
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.