plugins {
id 'io.micronaut.application' version '3.6.0'
id 'io.micronaut.test-resources' version '3.6.0'
}
Table of Contents
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:
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 |
---|---|---|---|
Yes |
Yes |
mariadb |
|
Yes |
Yes |
mysql |
|
Yes |
Yes |
oracle-xe |
|
Yes |
Yes |
postgres |
|
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:
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
|
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 ofmariadb
,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 ofmariadb
,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 ofmariadb
,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:
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
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 thetest-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:
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):
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:
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:
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 |
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)
-
test resource resolvers are loaded via service loading
-
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 returndatasources
to this method. -
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:
-
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 thedatasources.default.db-type
property before it can resolve thedatasources.default.url
property, so that it can tell if it’s actually capable of handling that database type or not. -
the
resolve
method is called with the expression being resolved, the map of resolved properties from 1. and the whole configuration map of thetest-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: