Testing Services

§Testing Services

§Running tests

You can run tests from the sbt console.

  • To run all tests, run test.
  • To run only one test class, run testOnly followed by the name of the class i.e. testOnly my.namespace.MyTest.
  • To run only the tests that have failed, run testQuick.
  • To run tests continually, run a command with a tilde in front, i.e. ~testQuick.

§JUnit

The recommended test framework for Lagom is JUnit 4

import static org.junit.Assert.*;

import org.junit.Test;

public class SimpleTest {

  @Test
  public void testSum() {
    int a = 1 + 1;
    assertEquals(2, a);
  }

  @Test
  public void testString() {
    String str = "Hello world";
    assertFalse(str.isEmpty());
  }
}

§Dependency

To use this feature add the following in your project’s build.

<dependency>
    <groupId>com.lightbend.lagom</groupId>
    <artifactId>lagom-javadsl-testkit_${scala.binary.version}</artifactId>
    <version>${lagom.version}</version>
    <scope>test</scope>
</dependency>

In sbt:

libraryDependencies += lagomJavadslTestKit

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

.settings(lagomForkedTestSettings: _*)

§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.

import static com.lightbend.lagom.javadsl.testkit.ServiceTest.*;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class HelloServiceTest {

  @Test
  public void shouldSayHello() throws Exception {
    withServer(
        defaultSetup(),
        server -> {
          HelloService service = server.client(HelloService.class);

          String msg = service.sayHello().invoke("Alice").toCompletableFuture().get(5, SECONDS);
          assertEquals("Hello Alice", msg);
        });
  }
}

Dependencies to other services must be replaced by stub or mock implementations by overriding the bindings of the GuiceApplicationBuilder in the Setup. If we are writing a test for the HelloService and that has a dependency to a GreetingService we must create an implementation of the GreetingService that can be used for the test without running the real GreetingService. Something like this:

static class GreetingStub implements GreetingService {
  @Override
  public ServiceCall<String, String> greeting() {
    return req -> CompletableFuture.completedFuture("Hello");
  }
}

private final Setup setup =
    defaultSetup()
        .configureBuilder(b -> b.overrides(bind(GreetingService.class).to(GreetingStub.class)));

Note how the dependency is overridden when constructing the test Setup object, which then can be used as parameter to the withServer method instead of the defaultSetup() in the above HelloServiceTest.

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

private final Setup setup = defaultSetup().withCluster();

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:

private final Setup setup = defaultSetup().withCassandra();

To enable JDBC Persistence:

private final Setup setup = defaultSetup().withJdbc();

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.

There are two different styles that can be used when writing the tests. It is most convenient to use withServer as illustrated in the above HelloServiceTest. It automatically starts and stops the server before and after the given lambda.

When your tests have several test methods, and especially when using persistence, it is faster to only start the server once in a static method annotated with @BeforeClass and stop it in a method annotated with @AfterClass.

import static com.lightbend.lagom.javadsl.testkit.ServiceTest.*;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

public class AdvancedHelloServiceTest {

  private static TestServer server;

  @BeforeClass
  public static void setUp() {
    server = startServer(defaultSetup().withCluster(false));
  }

  @AfterClass
  public static void tearDown() {
    if (server != null) {
      server.stop();
      server = null;
    }
  }

  @Test
  public void shouldSayHello() throws Exception {
    HelloService service = server.client(HelloService.class);
    String msg = service.sayHello().invoke("Alice").toCompletableFuture().get(5, SECONDS);
    assertEquals("Hello Alice", msg);
  }

  @Test
  public void shouldSayHelloAgain() throws Exception {
    HelloService service = server.client(HelloService.class);
    String msg = service.sayHello().invoke("Bob").toCompletableFuture().get(5, SECONDS);
    assertEquals("Hello Bob", msg);
  }
}

§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.

@Test
public void testOverTlsWithCustomClient() {
  ServiceTest.withServer(
      ServiceTest.defaultSetup()
          .withCluster(false)
          .withSsl()
          .configureBuilder(builder -> builder.bindings(new TestTlsServiceModule())),
      server -> {
        SSLContext sslContext = server.clientSslContext().get();
        // Builds an instance of a WSClient with the provided SSLContext so
        // the fake SSLContext prepared by Lagom's testkit is used.
        WSClient wsClient = buildCustomWS(sslContext);
        // use `localhost` as authority
        String url = "https://localhost:" + server.portSsl().get() + "/api/sample";
        String response =
            wsClient
                .url(url)
                .get()
                .thenApply(WSResponse::getBody)
                .toCompletableFuture()
                .get(5, TimeUnit.SECONDS);

        assertEquals("sample response", 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 have streaming request and/or response parameters. For example an EchoService like this:

public interface EchoService extends Service {

  ServiceCall<Source<String, NotUsed>, Source<String, NotUsed>> echo();

  default Descriptor descriptor() {
    return named("echo").withCalls(namedCall("echo", this::echo));
  }
}

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

import static com.lightbend.lagom.javadsl.testkit.ServiceTest.*;
import static java.util.concurrent.TimeUnit.SECONDS;
import java.util.Arrays;
import org.junit.Test;
import akka.NotUsed;
import akka.stream.javadsl.Source;
import akka.stream.testkit.TestSubscriber.Probe;
import akka.stream.testkit.javadsl.TestSink;

public class EchoServiceTest {

  @Test
  public void shouldEchoStream() throws Exception {
    withServer(
        defaultSetup().withCluster(false),
        server -> {
          EchoService service = server.client(EchoService.class);

          // Use a source that never terminates (concat Source.maybe) so we
          // don't close the upstream, which would close the downstream
          Source<String, NotUsed> input =
              Source.from(Arrays.asList("msg1", "msg2", "msg3")).concat(Source.maybe());
          Source<String, NotUsed> output =
              service.echo().invoke(input).toCompletableFuture().get(5, SECONDS);
          Probe<String> probe =
              output.runWith(TestSink.probe(server.system()), server.materializer());
          probe.request(10);
          probe.expectNext("msg1");
          probe.expectNext("msg2");
          probe.expectNext("msg3");
          probe.cancel();
        });
  }
}

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

§How to test broker publishing and consuming

The section on Message Broker Testing is related to Testing Services but is specific to testing the production and consumption via Brokers.

§How to test PersistentEntity

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 documentation of Persistent Entity

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.