Micronaut Test Resources

Test resources integration (like Testcontainers) for Micronaut

Version:

1 Introduction

Micronaut Test Resources adds support for managing external resources which are required during development or testing.

For example, an application may need a database to run (say MySQL), but such a database may not be installed on the development machine or you may not want to handle the setup and tear down of the database manually.

Micronaut Test Resources offers a generic purpose solution for this problem, without any configuration when possible. In particular, it integrates with Testcontainers to provide throwaway containers for testing.

Test resources are only available during development (for example when running the Gradle run task or the Maven mn:run goal) and test execution: production code will require the resources to be available.

A key aspect of test resources to understand is that they are created in reaction to a missing property in configuration. For example, if the datasources.default.url property is missing, then a test resource will be responsible for resolving its value. If that property is set, then the test resource will not be used.

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Quick Start

Micronaut Test Resources are integrated via build plugins.

The recommended approach to get started is to use Micronaut Launch and select the test-resources feature.

If you wanted to integrate it manually, for Gradle you can use the micronaut-test-resources plugin:

Gradle
plugins {
    id 'io.micronaut.application' version '3.6.0'
    id 'io.micronaut.test-resources' version '3.6.0'
}

Please refer to the (Gradle plugin’s Test Resources documentation for more information about configuring the test resources plugin.

In the case of Maven, you can enable test resources support simply by setting the property micronaut.test.resources.enabled (either in your POM or via the command-line).

Please refer to the (Maven plugin’s Test Resources documentation for more information about configuring the test resources plugin.

4 Supported Modules

The following modules are provided with Micronaut Test Resources.

4.1 Databases

Databases

Micronaut Test Resources provides support for the following databases:

Database JDBC R2DBC Database identifier

MariaDB

Yes

Yes

mariadb

MySQL

Yes

Yes

mysql

Oracle Express Edition

Yes

Yes

oracle-xe

PostgreSQL

Yes

Yes

postgres

Microsoft SQL Server

Yes

Yes

mssql

Databases are supplied via a Testcontainers container. It is possible to override the default image of the container by setting the following property in your application configuration:

  • test-resources.containers.[db-type].image-name

For example, you can override the default image of the container for the MariaDB database by setting the following property:

application.yml
test-resources:
  containers:
    mariadb:
      image-name: mariadb:10.3

The db-type property value can be found in the table above.

Using the Microsoft SQL Server container will require you to accept its license. In order to do this, you must set the test-resources.containers.mssql.accept-license property to true:

test-resources:
  containers:
    mssql:
      accept-license: true

4.1.1 JDBC

The following properties will automatically be set when using a JDBC database:

  • datasources.*.url

  • datasources.*.username

  • datasources.*.password

  • datasources.*.dialect

In order for the database to be properly detected, one of the following properties has to be set:

  • datasources.*.db-type: the kind of database (preferred, one of mariadb, mysql, oracle, postgresql)

  • datasources.*.driverClassName: the class name of the driver (fallback)

  • datasources.*.dialect: the dialect to use for the database (fallback)

  • datasources.*.db-name: overrides the default test database name

  • datasources.*.username: overrides the default test user

  • datasources.*.password: overrides the default test password

  • datasources.*.init-script-path: a path to a SQL file on classpath, which will be executed at container startup

4.1.2 R2DBC

In addition to traditional JDBC support, Micronaut Test Resources also supports R2DBC databases.

The following properties will automatically be set when using a JDBC database:

  • r2dbc.datasources.*.url

  • r2dbc.datasources.*.username

  • r2dbc.datasources.*.password

In order for the database to be properly detected, one of the following properties has to be set:

  • r2dbc.datasources.*.db-type: the kind of database (preferred, one of mariadb, mysql, oracle, postgresql)

  • r2dbc.datasources.*.driverClassName: the class name of the driver (fallback)

  • r2dbc.datasources.*.dialect: the dialect to use for the database (fallback)

In addition, R2DBC databases can be configured simply by reading the traditional JDBC properties:

  • datasources.*.db-type: the kind of database (preferred, one of mariadb, mysql, oracle, postgresql)

  • datasources.*.driverClassName: the class name of the driver (fallback)

  • datasources.*.dialect: the dialect to use for the database (fallback)

  • datasources.*.db-name: overrides the default test database name

  • datasources.*.username: overrides the default test user

  • datasources.*.password: overrides the default test password

  • datasources.*.init-script-path: a path to a SQL file on classpath, which will be executed at container startup

In which case, the name of the datasource must match the name of the R2DBC datasource. This can be useful when using modules like Flyway which only support JDBC for updating schemas, but still have your application use R2DBC: in this case the container which will be used for R2DBC and JDBC will be the same.

4.2 Elasticsearch

Elasticsearch support will automatically start an Elasticsearch container and provide the value of the elasticsearch.hosts property.

The default image can be overwritten by setting the test-resources.containers.elasticsearch.image-name property. The default version can be overwritten by setting the test-resources.containers.elasticsearch.image-tag property.

4.3 Kafka

Kafka support will automatically start a Kafka container and provide the value of the kafka.bootstrap.servers property.

The default image can be overwritten by setting the test-resources.containers.kafka.image-name property.

4.4 MongoDB

MongoDB support will automatically start a MongoDB container and provide the value of the mongodb.uri property.

The default image can be overwritten by setting the test-resources.containers.mongodb.image-name property.

The default database name can be overwritten by setting the test-resources.containers.mongodb.db-name property.

4.5 MQTT

MQTT support is provided using a HiveMQ container by default.

It will automatically configure the mqtt.client.server-uri property.

Alternatively, you can setup a custom container. For example, you can use Mosquitto instead:

application.yml
mqtt:
  client:
    server-uri: tcp://${mqtt.host}:${mqtt.port}
    client-id: ${random.uuid}
test-resources:
  containers:
    mosquitto:
      image-name: eclipse-mosquitto
      hostnames:
        - mqtt.host
      exposed-ports:
        - mqtt.port: 1883
      ro-fs-bind:
        - "mosquitto.conf": /mosquitto/config/mosquitto.conf
mosquitto.conf
persistence false
allow_anonymous true
connection_messages true
log_type all
listener 1883

4.6 Neo4j

Neo4j support will automatically start a Neo4j container and provide the value of the neo4j.uri property.

The default image can be overwritten by setting the test-resources.containers.neo4j.image-name property.

4.7 RabbitMQ

RabbitMQ support will automatically start a RabbitMQ container and provide the value of the rabbitmq.uri property.

The default image can be overwritten by setting the test-resources.containers.rabbitmq.image-name property.

4.8 Redis

Redis support will automatically start a Redis container and provide the value of the redis.uri property.

The default image can be overwritten by setting the test-resources.containers.redis.image-name property.

4.9 Hashicorp Vault

Vault support will automatically start a Hashicorp Vault container and provide the value of vault.client.uri property.

  • The default image can be overwritten by setting the test-resources.containers.hashicorp-vault.image-name property.

  • The default Vault access token is vault-token but this can be overridden by setting the test-resources.containers.hashicorp-vault.token property.

  • Secrets should be inserted into Hashicorp Vault at startup by adding the config:

test-resources:
  containers:
    hashicorp-vault:
      path: 'secret/my-path'
      secrets:
        - "key1=value1"
        - "key2=value2"

4.10 Generic Testcontainers support

There are cases where the provided test resources modules don’t support your use case. For example, there is no built-in test resource for supplying a dummy SMTP server, but you may need such a resource for testing your application.

In this case, Micronaut Test Resources provide generic support for starting arbitrary test containers. Unlike the other resources, this will require adding configuration to your application to make it work.

Let’s illustrate how we can declare an SMTP test container (here using YAML configuration) for use with Micronaut Email:

application.yml
test-resources:
  containers:
    fakesmtp:                       (1)
      image-name: ghusta/fakesmtp   (2)
      hostnames:                    (3)
        - smtp.host                 (4)
      exposed-ports:                (5)
        - smtp.port: 25             (6)
1 The name of the test container
2 The image name to use
3 The hostnames declare what properties will be resolved with the value of the container host name
4 Declares that the smtp.host property will be resolved with the container host name
5 Declares what ports the container exposes
6 Declares that the property smtp.port will be set to the value of the mapped port 25

Then the values smtp.host and smtp.port can for example be used in a bean configuration:

@Singleton
public class JavamailSessionProvider implements SessionProvider {
    @Value("${smtp.host}")          (1)
    private String smtpHost;

    @Value("${smtp.port}")          (2)
    private String smtpPort;


    @Override
    public Session session() {
        Properties props = new Properties();
        props.put("mail.smtp.host", smtpHost);
        props.put("mail.smtp.port", smtpPort);
        return Session.getDefaultInstance(props);
    }
}
1 The smtp.host property that we exposed in the test container configuration
2 The smtp.port property that we exposed in the test container configuration

In addition, generic containers can bind file system resources (either read-only or read-write):

application.yml
test-resources:
  containers:
    mycontainer:
      image-name: my/container
      hostnames:
        - my.service.host
      exposed-ports:
        - my.service.port: 1883
      ro-fs-bind:
        - "path/to/local/readonly-file.conf": /path/to/container/readonly-file.conf
      rw-fs-bind:
        - "path/to/local/readwrite-file.conf": /path/to/container/readwrite-file.conf

It is also possible to copy files from the host to the container:

application.yml
test-resources:
  containers:
    mycontainer:
      image-name: my/container
      hostnames:
        - my.service.host
      exposed-ports:
        - my.service.port: 1883
      copy-to-container:
        - path/to/local/readonly-file.conf: /path/to/container/file.conf
        - classpath:/file/on/classpath.conf: /path/to/container/other.conf

In case you want to copy a file found on classpath, the source path must be prefixed with classpath:.

The following properties are also supported:

  • command: Set the command that should be run in the container

  • env: the map of environment variables

  • labels: the map of labels

  • startup-timeout: the container startup timeout

e.g:

application.yml
test-resources:
  containers:
    mycontainer:
      image-name: my/container
      hostnames:
        - my.service.host
      exposed-ports:
        - my.service.port: 1883
      command: "some.sh"
      env:
        - MY_ENV_VAR: "my value"
      labels:
        - my.label: "my value"
      startup-timeout: 120s

Please refer to the configuration properties reference for details.

Advanced networking

By default, each container is spawned in its own network. It is possible to let containers communicate with each other by declaring the network they belong to:

test-resources:
  containers:
    producer:
      image-name: alpine:3.14
      command:
        - /bin/sh
        - "-c"
        - "while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done"
      network: custom
      network-aliases: bob
      hostnames: producer.host
    consumer:
      image-name: alpine:3.14
      command: top
      network: custom
      network-aliases: alice
      hostnames: consumer.host

The network key is used to declare the network containers use. The network-aliases can be used to assign host names to the containers.

5 Architecture

Micronaut Test Resources essentially consists of:

  • a test resource server, which is responsible for service "test provisioning requests"

  • a thin test resource client, which is injected in the classpath of the application under test

  • test resources support modules, which are injected on the server classpath by build tools, based on inference and configuration

The client is responsible for resolving "missing properties". For example, if a bean needs the value of the datasources.default.url property and that this property isn’t set, then the client will issue a request on the test resources server to give the value of that property. As a side effect of providing the value, a database container may be started.

5.1 The test resources server

The Micronaut Test Resources server is a service which is responsible for handling the lifecycle of test resources.

For example, a MySQL database may need to be started when the tests start, and shutdown at the end of the build.

For this purpose, the test resource server must be started before the application under test starts, and shutdown after test resources are no longer needed.

It means, for example, that with Gradle continuous builds, the test resources server would outlive a single build, making it possible to develop your application while not paying the price of starting a container on each build.

5.1.1 Sharing containers between independent builds

By default, a server will be spawned for each build, and shutdown at the end of a build (or after several builds if using the continuous mode in Gradle).

However, what if you have several projects and that you want to share test resources between them? As an example, you might have a project which consists of a Kafka publisher and another project with a Kafka consumer. If they don’t use the same Kafka server, then they won’t be able to communicate with each other.

The solution to this problem is to use a shared server. By default builds would spawn a server per build on a different port, but if you specify a port explicitly, then both builds will use the same server.

If you use a shared server, then you must make sure that the first build to start the server provides all the support modules.

5.2 The test resources client

The Micronaut Test Resources client is a lightweight client which connects to the test resources server. It basically delegates requests of property resolution from Micronaut to the server.

This client is automatically injected on the application classpath in development mode or during tests. As a user, you should never have to deal with this module directly.

5.3 Embedded test resources

The embedded test resources module is used by the Test Resources Server internally. It is responsible for aggregating several test resources resolvers and providing a unified interface to the test resources.

It is possible for an application to use the embedded test resources module directly instead of using the server and the client. However, we don’t recommend it because it would enhance the application classpath with unnecessary dependencies: each of the test resource resolver would add dependencies on the application that you may not want, potentially introducing conflicts.

The client/server model is designed to avoid this situation, while offering a more fine-grained control over the lifecycle of the test resources.

5.4 Implementing a test resource

In case the provided test resources are not enough, or if you have to tweak the configuration of test resources in such a way that is not possible via simple configuration (for example, you might need to execute commands in a test container, which is not supported), you can implement your own test resource resolver.

In the general case, test resources are not loaded in the same process as your tests: they will be loaded in a service which runs independently. As a consequence, it is a mistake to put their implementation in your test source set (that is to say in src/test/java for example). Therefore, depending on the build tool you use, test resources must either be implemented in the src/testResources source set (Gradle) or in a separate project/module (Maven or Gradle).

A test resource resolver is loaded at application startup, before the ApplicationContext is available, and therefore cannot use traditional Micronaut dependency injection.

Therefore, a test resource resolver is a service implementing the TestResourcesResolver interface.

For test resources which make use of Testcontainers, you may extend the base AbstractTestContainersProvider class.

In addition, you need to declare the resolver for service loading, by adding your implementation type name in the META-INF/services/io.micronaut.testresources.core.TestResourcesResolver file.

Lifecycle

At application startup

Implementing a test resources resolver requires to understand the lifecycle of resolvers. An initial step is done when the resolvers are loaded (for example in the server process)

  1. test resource resolvers are loaded via service loading

  2. the TestResourcesResolver#getRequiredPropertyEntries() method is called on a resolver. This method should return a list of property entries that the resolver needs to know about in order to determine which properties it can resolve. For example, a datasource URL resolver must know the names of the datasources before it can tell what properties it can resolve. Therefore, it would return datasources to this method.

  3. the TestResourcesResolver#getResolvableProperties method is called, with a couple maps:

    • the first map contains at the key the name of a "required property entry" from the call in 2. For example, datasources. The value is the list of entries for that property. For example, [default, users, books].

    • the second map contains the whole test-resources configuration from the application, as a flat map of properties.

The resolver can then return a list of properties that it can resolve. For example, datasources.default.url, datasources.users.url, datasources.books.url, etc.

When a property needs to be resolved

At this stage, we know all properties that resolvers are able to resolve. So whenever Micronaut will encounter a property without value, it will ask the resolvers to resolve it. This is done in 2 steps:

  1. the TestResourceResolver#getRequiredProperties is called with the expression being resolved. It gives a chance to the resolver to tell what other properties it needs to read before resolving the expression. For example, a database resolver may need to read the datasources.default.db-type property before it can resolve the datasources.default.url property, so that it can tell if it’s actually capable of handling that database type or not.

  2. the resolve method is called with the expression being resolved, the map of resolved properties from 1. and the whole configuration map of the test-resources from the application.

It is the responsibility of the TestResourcesResolver to check, when resolve is called, that it can actually resolve the supplied expression. Do not assume that it will be called with an expression that it can resolve. If, for some reason, the resolver cannot resolve the expression then Optional#empty() should be returned, otherwise the test resource resolver can return the resolved value.

As part of the resolution, a test resource may be started (for example a container).

6 Repository

You can find the source code of this project in this repository: