Quarkus Integration Testing with Testcontainers and LocalStack

With containers, the ability to perform integration testing in a local environment is now table stakes for programmers. We are no longer stuck writing mocks or settling for hacks such as in memory databases. We can use the real databases and middleware to test against, or at least we can use proper facsimiles of the real thing which will provide us the confidence we need to release code that works.

Local Cloud Native Services

I recently found Localstack. I am working on a project that intended to use S3 as an object store and luckily, the exact tutorial for utilizing the Quarkus S3 client also had references to the Localstack container. Localstack is a container that mimics the functionality of AWS’s native services. When starting the container, you provide it with a list of services you want exposed and the container gives you locally served equivalents. The tutorial references the docker way of starting the LocalStack in the background prior to running any of the tests.

docker run -it --publish 4566:4566 -e SERVICES=s3 -e START_WEB=0 localstack/localstack:0.12.8

There is also the docker-compose way of doing things.

version: '3.8'
services:
  s3:
    image: localstack/localstack:0.12.8
    ports:
      - "4566:4566"
    environment:
      START_WEB: 0
      SERVICES: s3

This is great and all but I want to utilize this in my integration tests as ephemeral resources which will not have any conflicts, such as port conflicts, during the running of tests and have them torn down afterwards.

Integration Testing with Quarkus

Luckily, Quarkus provides a way of doing this. By utilizing the QuarkusTestResourceLifecycleManager, the developer has the ability to spin up resources at the beginning of the test run and will subsequently clean up the resources after the run completes. Testcontainers makes this super easy and also has a LocalStack module ready for use.

To do this with LocalStack, it would look like this. First the maven dependencies, then the manager class.

<dependencyManagement>
    <dependencies>
...
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>testcontainers-bom</artifactId>
            <version>1.15.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
...
<dependencies>
...
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>testcontainers</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.testcontainers</groupId>
        <artifactId>localstack</artifactId>
        <scope>test</scope>
    </dependency>
...
</dependencies>
public class LocalStackTestResource implements QuarkusTestResourceLifecycleManager {

    static DockerImageName dockerImageName = DockerImageName.parse("localstack/localstack:0.12.8");
    static LocalStackContainer localStackContainer = new LocalStackContainer(dockerImageName)
                    .withServices(LocalStackContainer.Service.S3);

    @Override
    public Map<String, String> start() {
        localStackContainer.start();
        HashMap<String, String> map = new HashMap<>();
        map.put("quarkus.s3.endpoint-override", localStackContainer.getEndpointOverride(LocalStackContainer.Service.S3).toString());
        map.put("quarkus.s3.aws.region", localStackContainer.getRegion());
        map.put("quarkus.s3.aws.credentials.type", "static");
        map.put("quarkus.s3.aws.credentials.static-provider.access-key-id", localStackContainer.getDefaultCredentialsProvider().getCredentials().getAWSAccessKeyId());
        map.put("quarkus.s3.aws.credentials.static-provider.secret-access-key", localStackContainer.getDefaultCredentialsProvider().getCredentials().getAWSSecretKey());
        return map;
    }

    @Override
    public void stop() {
        localStackContainer.stop();
    }

}

The QuarkusTestLifecycleManager gives you the ability to spin up the Testcontainer for LocalStack as well as substituting the runtime variables associated with the container back to the application properties. To include this manager in your tests, you just have to add it as an annotation to a single class in the scope of tests to be executed. The test framework will scan all the tests for this annotation prior to execution and will execute the manager once for all tests.

@QuarkusTest
@QuarkusTestResource(LocalStackTestResource.class)

If you want finer grained control, such as starting a clean container for every class, then you can revert back to the Rule implementations for JUnit 5 with Testcontainers.

Leave a Reply

Your email address will not be published. Required fields are marked *