Micronaut Test

Testing Framework Extensions for Micronaut

Version: 4.6.2

1 Introduction

One of the design goals of Micronaut was to eliminate the artificial separation imposed by traditional frameworks between function and unit tests due to slow startup times and memory consumption.

With that in mind it is generally pretty easy to start Micronaut in a unit test and one of the goals of Micronaut was to as much as possible not require a test framework to test Micronaut. For example in Spock you can simply do:

@Shared (1)
@AutoCleanup (2)
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
1 The field is declared as shared so to server is started only once for all methods in the class
2 The @AutoCleanup annotation ensures the server is shutdown after the test suite completes.

However, there are cases where having some additional features to test Micronaut come in handy, such as mocking bean definitions and so on.

This project includes a pretty simple set of extensions for JUnit 5, Spock and Kotest:

  • Automatically start and stop the server for the scope of a test suite

  • Use mocks to replace existing beans for the scope of a test suite

  • Allow dependency injection into a test instance

This is achieved through a set of annotations:

  • @MicronautTest - Can be added to any test:

    • io.micronaut.test.extensions.spock.annotation.MicronautTest for Spock.

    • io.micronaut.test.extensions.junit5.annotation.MicronautTest for JUnit 5.

    • io.micronaut.test.extensions.kotest.annotation.MicronautTest for Kotest.

    • io.micronaut.test.extensions.kotest5.annotation.MicronautTest for Kotest 5.

  • io.micronaut.test.annotation.@MockBean - Can be added to methods or inner classes of a test class to define mock beans that replace existing beans for the scope of the test.

These annotations use internal Micronaut features and do not mock any part of Micronaut itself. When you run a test within @MicronautTest it is running your real application.

In some tests you may need a reference to the ApplicationContext and/or the EmbeddedServer (for example, to create an instance of an HttpClient). Rather than defining these as properties of the test (such as a @Shared property in Spock), when using @MicronautTest you can reference the server/context that was started up for you, and inject them directly in your test.

@Inject
EmbeddedServer server //refers to the server that was started up for this test suite

@Inject
ApplicationContext context //refers to the current application context within the scope of the test

Eager Singleton Initialization

If you enable eager singleton initialization in your application, the Micronaut Framework eagerly initializes all singletons at startup time. This can be useful for applications that need to perform some initialization at startup time, such as registering a bean with a third party library.

However, as tests annotated with @MicronautTest are implicitly in the Singleton scope, this can cause problems injecting some beans (for example an HttpClient) into your test class.

To avoid this, you can either disable eager singleton initialization for your tests, or you will need to manually get an instance of the bean you would normally inject. As an example, to get an HttpClient you could do:

Using an HttpClient in a test with eager singleton initialization enabled
@Inject
EmbeddedServer server; (1)

Supplier<HttpClient> client = SupplierUtil.memoizedNonEmpty(() ->
    server.getApplicationContext().createBean(HttpClient.class, server.getURL())); (2)

@Test
void testEagerSingleton() {
    Assertions.assertEquals("eager", client.get().toBlocking().retrieve("/eager")); (3)
}
1 Inject the EmbeddedServer as normal
2 Create a Supplier that will create the HttpClient when it is first called
3 Use the Supplier to get the HttpClient and make the request

2 Release History

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

3 Testing with Spock

3.1 Setting up Spock

To get started using Spock you need the following dependencies in your build configuration:

build.gradle
testImplementation "io.micronaut.test:micronaut-test-spock"
testImplementation("org.spockframework:spock-core") {
    exclude group: "org.codehaus.groovy", module: "groovy-all"
}
If you plan to define mock beans you will also need micronaut-inject-groovy on your testImplementation classpath or micronaut-inject-java for Java or Kotlin (this should already be configured if you used mn create-app).

Or for Maven:

pom.xml
<dependency>
    <groupId>io.micronaut.test</groupId>
    <artifactId>micronaut-test-spock</artifactId>
    <scope>test</scope>
</dependency>

3.2 Writing a Micronaut Test with Spock

Let’s take a look at an example using Spock. Consider you have the following interface:

The MathService Interface
package io.micronaut.test.spock;

public interface MathService {

    Integer compute(Integer num);
}

And a simple implementation that computes the value times 4 and is defined as a Micronaut bean:

The MathService implementation
package io.micronaut.test.spock

import jakarta.inject.Singleton

@Singleton
class MathServiceImpl implements MathService {

    @Override
    Integer compute(Integer num) {
        return num * 4 // should never be called
    }
}

You can define the following test to test it:

The MathService specification
package io.micronaut.test.spock

import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.*
import jakarta.inject.Inject

@MicronautTest (1)
class MathServiceSpec extends Specification {

    @Inject
    MathService mathService (2)

    @Unroll
    void "should compute #num times 4"() { (3)
        when:
        def result = mathService.compute(num)

        then:
        result == expected

        where:
        num | expected
        2   | 8
        3   | 12
    }
}
1 The test is declared as Micronaut test with @MicronautTest
2 The @Inject annotation is used to inject the bean
3 The test itself tests the injected bean

3.3 Environments, Classpath Scanning etc.

The @MicronautTest annotation supports specifying the environment names the test should run with:

@MicronautTest(environments={"foo", "bar"})

In addition, although Micronaut itself doesn’t scan the classpath, some integrations do (such as JPA and GORM), for these cases you may wish to specify either the application class:

@MicronautTest(application=Application.class)

Or the packages:

@MicronautTest(packages="foo.bar")

To ensure that entities can be found during classpath scanning.

3.4 Transaction semantics

When using @MicronautTest each @Test method will be wrapped in a transaction that will be rolled back when the test finishes. This behaviour can be changed by using the transactional and rollback properties.

To avoid creating a transaction:

@MicronautTest(transactional = false)

To not rollback the transaction:

@MicronautTest(rollback = false)

Additionally, the transactionMode property can be used to further customize the way that transactions are handled for each test:

@MicronautTest(transactionMode = TransactionMode.SINGLE_TRANSACTION)

The following transaction modes are supported:

  • SEPARATE_TRANSACTIONS (default) - Each setup/cleanup method is wrapped in its own transaction, separate from that of the test. This transaction is always committed.

  • SINGLE_TRANSACTION - All setup methods are wrapped in the same transaction as the test. Cleanup methods are wrapped in separate transactions.

Setup and cleanup methods are not wrapped in transactions in Kotest currently. As a result, transaction modes have no effect in Kotest.

3.5 Using Spock Mocks

Now let’s say you want to replace the implementation with a Spock Mock. You can do so by defining a method that returns a Spock mock and is annotated with @MockBean, for example:

The MathService specification
package io.micronaut.test.spock

import io.micronaut.test.annotation.MockBean
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification
import spock.lang.Unroll

import jakarta.inject.Inject

@MicronautTest
class MathMockServiceSpec extends Specification {

    @Inject
    MathService mathService (3)

    @Unroll
    void "should compute #num to #square"() {
        when:
        def result = mathService.compute(num)

        then:
        1 * mathService.compute(num) >> Math.pow(num, 2)  (4)
        result == square

        where:
        num | square
        2   | 4
        3   | 9
    }

    @MockBean(MathServiceImpl) (1)
    MathService mathService() {
        Mock(MathService) (2)
    }
}
1 The @MockBean annotation is used to indicate the method returns a mock bean. The value to the method is the type being replaced.
2 Spock’s Mock(..) method creates the actual mock
3 The Mock is injected into the test
4 Spock is used to verify the mock is called

3.6 Mocking Collaborators

Note that in most cases you won’t define a @MockBean and inject it, only to verify interaction with the Mock directly. Instead, the Mock will be a collaborator within your application. For example say you have a MathController:

The MathController
package io.micronaut.test.spock

import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get


@Controller('/math')
class MathController {

    MathService mathService

    MathController(MathService mathService) {
        this.mathService = mathService
    }

    @Get(uri = '/compute/{number}', processes = MediaType.TEXT_PLAIN)
    String compute(Integer number) {
        return mathService.compute(number)
    }
}

The above controller uses the MathService to expose a /math/compute/{number] endpoint. See the following example for a test that tests interaction with the mock collaborator:

Mocking Collaborators
package io.micronaut.test.spock

import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.annotation.MockBean
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification
import spock.lang.Unroll

import jakarta.inject.Inject

@MicronautTest
class MathCollaboratorSpec extends Specification {

    @Inject
    MathService mathService (2)

    @Inject
    @Client('/')
    HttpClient client (3)

    @Unroll
    void "should compute #num to #square"() {
        when:
        Integer result = client.toBlocking().retrieve(HttpRequest.GET('/math/compute/10'), Integer) (3)

        then:
        1 * mathService.compute(10) >> Math.pow(num, 2)  (4)
        result == square

        where:
        num | square
        2   | 4
        3   | 9
    }

    @MockBean(MathServiceImpl) (1)
    MathService mathService() {
        Mock(MathService)
    }

}
1 Like the previous example a Mock is defined using @MockBean
2 This time we inject an instance of HttpClient to test the controller.
3 We invoke the controller and retrieve the result
4 The interaction with mock collaborator is verified.

The way this works is that @MicronautTest will inject the Mock(..) instance into the test, but the controller will have a proxy that points to the Mock(..) instance injected. For each iteration of the test the mock is refreshed (in fact it uses Micronaut’s built in RefreshScope).

3.7 Using `@Requires` on Tests

Since @MicronautTest turns tests into beans themselves, it means you can use the @Requires annotation on the test to enable/disable tests. For example:

@MicronautTest
@Requires(env = "my-env")
class RequiresSpec extends Specification {
    ...
}

The above test will only run if my-env is active (you can activate it by passing the system property micronaut.environments).

3.8 Defining Additional Test Specific Properties

You can define additional test specific properties using the @Property annotation. The following example demonstrates usage:

Using @Property
package io.micronaut.test.spock

import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Value
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification
import spock.lang.Stepwise

@MicronautTest
@Property(name = "foo.bar", value = "stuff")
@Stepwise
class PropertySpec extends Specification {


    @Value('${foo.bar}')
    String val

    void "test value"() {
        expect:
        val == 'stuff'
    }

    @Property(name = "foo.bar", value = "changed")
    void "test value changed"() {
        expect:
        val == 'changed'
    }

    void "test value restored"() {
        expect:
        val == 'stuff'
    }
}

Note that when a @Property is defined at the test method level, it causes a RefreshEvent to be triggered which will update any @ConfigurationProperties related to the property.

Alternatively you can specify additional propertySources in any supported format (YAML, JSON, Java properties file etc.) using the @MicronautTest annotation:

Using propertySources stored in files
package io.micronaut.test.spock

import io.micronaut.context.annotation.Property
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification

@MicronautTest(propertySources = "myprops.properties")
class PropertySourceSpec extends Specification {

    @Property(name = "foo.bar")
    String val

    void "test property source"() {
        expect:
        val == 'foo'
    }
}

The above example expects a file located at src/test/resources/io/micronaut/spock/myprops.properties. You can however use a prefix to indicate where the file should be searched for. The following are valid values:

  • file:myprops.properties - A relative path to a file somewhere on the file system.

  • classpath:myprops.properties - A file relative to the root of the classpath.

  • myprops.properties - A file relative on the classpath relative to the test being run.

3.9 Refreshing injected beans based on `@Requires` upon properties changes

You can use combine the use of @Requires and @Property, so that injected beans will be refreshed if there are configuration changes that affect their @Requires condition.

For that to work, the test must be annotated with @MicronautTest(rebuildContext = true). In that case, if there are changes in any property for a given test, the application context will be rebuilt so that @Requires conditions are re-evaluated again.

For example:

Combining @Requires and @Property in a @Refreshable test class.
package io.micronaut.test.spock

import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Issue
import spock.lang.Specification

import jakarta.inject.Inject
import jakarta.inject.Singleton

@Issue("https://github.com/micronaut-projects/micronaut-test/issues/91")
@MicronautTest(rebuildContext = true)
@Property(name = "foo.bar", value = "stuff")
class PropertyValueRequiresSpec extends Specification {
    @Inject
    MyService myService

    void "test initial value"() {
        expect:
        myService instanceof MyServiceStuff
    }

    @Property(name = "foo.bar", value = "changed")
    void "test value changed"() {
        expect:
        myService instanceof MyServiceChanged
    }

    void "test value restored"() {
        expect:
        myService instanceof MyServiceStuff
    }
}

interface MyService {}

@Singleton
@Requires(property = "foo.bar", value = "stuff")
class MyServiceStuff implements MyService {}


@Singleton
@Requires(property = "foo.bar", value = "changed")
class MyServiceChanged implements MyService {}

4 Testing with JUnit 5

4.1 Setting up JUnit 5

To get started using JUnit 5 you need the following dependencies in your build configuration:

build.gradle
dependencies {
    testAnnotationProcessor "io.micronaut:micronaut-inject-java"
    ...
    testImplementation("org.junit.jupiter:junit-jupiter-api")
    testImplementation("io.micronaut.test:micronaut-test-junit5")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
    testImplementation("org.junit.jupiter:junit-jupiter-engine")
}

// use JUnit 5 platform
test {
    useJUnitPlatform()
}
If you plan to define mock beans you will also need inject-groovy on your testCompile classpath or inject-java for Java or Kotlin (this should already be configured if you used mn create-app) and the testAnnotationProcessor.

Or for Maven:

pom.xml
<dependency>
    <groupId>io.micronaut.test</groupId>
    <artifactId>micronaut-test-junit5</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <scope>test</scope>
</dependency>

4.2 Writing a Micronaut Test with JUnit 5

Let’s take a look at an example using JUnit 5. Consider you have the following interface:

The MathService Interface
package io.micronaut.test.junit5;

public interface MathService {

    Integer compute(Integer num);
}

And a simple implementation that computes the value times 4 and is defined as a Micronaut bean:

The MathService implementation
package io.micronaut.test.junit5;

import jakarta.inject.Singleton;

@Singleton
class MathServiceImpl implements MathService {

    @Override
    public Integer compute(Integer num) {
        return num * 4;
    }
}

You can define the following test to test the implementation:

The MathService specification
package io.micronaut.test.junit5;

import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import jakarta.inject.Inject;


@MicronautTest (1)
class MathServiceTest {

    @Inject
    MathService mathService; (2)


    @ParameterizedTest
    @CsvSource({"2,8", "3,12"})
    void testComputeNumToSquare(Integer num, Integer square) {
        final Integer result = mathService.compute(num); (3)

        Assertions.assertEquals(
                square,
                result
        );
    }
}
1 The test is declared as Micronaut test with @MicronautTest
2 The @Inject annotation is used to inject the bean
3 The test itself tests the injected bean

4.3 Environments, Classpath Scanning etc.

The @MicronautTest annotation supports specifying the environment names the test should run with:

@MicronautTest(environments={"foo", "bar"})

In addition, although Micronaut itself doesn’t scan the classpath, some integrations do (such as JPA and GORM), for these cases you may wish to specify either the application class:

@MicronautTest(application=Application.class)

Or the packages:

@MicronautTest(packages="foo.bar")

To ensure that entities can be found during classpath scanning.

4.4 Transaction semantics

When using @MicronautTest each @Test method will be wrapped in a transaction that will be rolled back when the test finishes. This behaviour can be changed by using the transactional and rollback properties.

To avoid creating a transaction:

@MicronautTest(transactional = false)

To not rollback the transaction:

@MicronautTest(rollback = false)

Additionally, the transactionMode property can be used to further customize the way that transactions are handled for each test:

@MicronautTest(transactionMode = TransactionMode.SINGLE_TRANSACTION)

The following transaction modes are supported:

  • SEPARATE_TRANSACTIONS (default) - Each setup/cleanup method is wrapped in its own transaction, separate from that of the test. This transaction is always committed.

  • SINGLE_TRANSACTION - All setup methods are wrapped in the same transaction as the test. Cleanup methods are wrapped in separate transactions.

Setup and cleanup methods are not wrapped in transactions in Kotest currently. As a result, transaction modes have no effect in Kotest.

4.5 Using Mockito Mocks

Now let’s say you want to replace the implementation with a Mockito Mock. You can do so by defining a method that returns a mock and is annotated with @MockBean, for example:

The MathService specification
package io.micronaut.test.junit5;

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.util.StringUtils;
import io.micronaut.test.annotation.MockBean;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import jakarta.inject.Inject;

import static org.mockito.Mockito.*;

@MicronautTest
@Requires(property = "mockito.test.enabled", defaultValue = StringUtils.FALSE, value = StringUtils.TRUE)
class MathMockServiceTest {

    @Inject
    MathService mathService; (3)


    @ParameterizedTest
    @CsvSource({"2,4", "3,9"})
    void testComputeNumToSquare(Integer num, Integer square) {

        when(mathService.compute(10))
            .then(invocation -> Long.valueOf(Math.round(Math.pow(num, 2))).intValue());

        final Integer result = mathService.compute(10);

        Assertions.assertEquals(
                square,
                result
        );
        verify(mathService).compute(10); (4)
    }

    @MockBean(MathServiceImpl.class) (1)
    MathService mathService() {
        return mock(MathService.class); (2)
    }

}
1 The @MockBean annotation is used to indicate the method returns a mock bean. The value to the method is the type being replaced.
2 Mockito’s mock(..) method creates the actual mock
3 The Mock is injected into the test
4 Mockito is used to verify the mock is called

Note that because the bean is an inner class of the test, it will be active only for the scope of the test. This approach allows you to define beans that are isolated per test class.

4.6 Mocking Collaborators

Note that in most cases you won’t define a @MockBean and inject it, only to verify interaction with the Mock directly. Instead, the Mock will be a collaborator within your application. For example say you have a MathController:

The MathController
package io.micronaut.test.junit5;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/math")
public class MathController {
    MathService mathService;

    MathController(MathService mathService) {
        this.mathService = mathService;
    }

    @Get(uri = "/compute/{number}", processes = MediaType.TEXT_PLAIN)
    String compute(Integer number) {
        return String.valueOf(mathService.compute(number));
    }
}

The above controller uses the MathService to expose a /math/compute/{number} endpoint. See the following example for a test that tests interaction with the mock collaborator:

Mocking Collaborators
package io.micronaut.test.junit5;

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.annotation.MockBean;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import jakarta.inject.Inject;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.*;

@MicronautTest
@Requires(property = "mockito.test.enabled", defaultValue = StringUtils.FALSE, value = StringUtils.TRUE)
class MathCollaboratorTest {

    @Inject
    MathService mathService;

    @Inject
    @Client("/")
    HttpClient client; (2)


    @ParameterizedTest
    @CsvSource({"2,4", "3,9"})
    void testComputeNumToSquare(Integer num, Integer square) {

        when( mathService.compute(num) )
            .then(invocation -> Long.valueOf(Math.round(Math.pow(num, 2))).intValue());

        final Integer result = client.toBlocking().retrieve(HttpRequest.GET("/math/compute/" + num), Integer.class); (3)

        assertEquals(
                square,
                result
        );
        verify(mathService).compute(num); (4)
    }

    @MockBean(MathServiceImpl.class) (1)
    MathService mathService() {
        return mock(MathService.class);
    }

}
1 Like the previous example a Mock is defined using @MockBean
2 This time we inject an instance of HttpClient to test the controller.
3 We invoke the controller and retrieve the result
4 The interaction with mock collaborator is verified.

The way this works is that @MicronautTest will inject the Mock(..) instance into the test, but the controller will have a proxy that points to the Mock(..) instance injected. For each iteration of the test the mock is refreshed (in fact it uses Micronaut’s built in RefreshScope).

For Factory injected beans, you can use Factory Replacement to inject Mocks. Refer to the factory replacement documentation for more information.

4.7 Using `@Requires` on Tests

Since @MicronautTest turns tests into beans themselves, it means you can use the @Requires annotation on the test to enable/disable tests. For example:

@MicronautTest
@Requires(env = "my-env")
class RequiresTest {
    ...
}

The above test will only run if my-env is active (you can activate it by passing the system property micronaut.environments).

4.8 Defining Additional Test Specific Properties

You can define additional test specific properties using the @Property annotation. The following example demonstrates usage:

Using @Property
package io.micronaut.test.junit5;

import io.micronaut.context.annotation.Property;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import static org.junit.jupiter.api.Assertions.assertEquals;

@MicronautTest
@Property(name = "foo.bar", value = "stuff")
@TestMethodOrder(OrderAnnotation.class)
class PropertyValueTest {

    @Property(name = "foo.bar")
    String val;

    @Test
    @Order(1)
    void testInitialValue() {
        assertEquals("stuff", val);
    }

    @Property(name = "foo.bar", value = "changed")
    @Test
    @Order(2)
    void testValueChanged() {
        assertEquals("changed", val);
    }

    @Test
    @Order(3)
    void testValueRestored() {
        assertEquals("stuff", val);
    }
}

Note that when a @Property is defined at the test method level, it causes a RefreshEvent to be triggered which will update any @ConfigurationProperties related to the property.

Alternatively you can specify additional propertySources in any supported format (YAML, JSON, Java properties file etc.) using the @MicronautTest annotation:

Using propertySources stored in files
package io.micronaut.test.junit5;

import io.micronaut.context.annotation.Property;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

@MicronautTest(propertySources = "myprops.properties")
class PropertySourceTest {

    @Property(name = "foo.bar")
    String val;


    @Test
    void testPropertySource() {
        Assertions.assertEquals("foo", val);
    }
}

The above example expects a file located at src/test/resources/io/micronaut/junit5/myprops.properties. You can however use a prefix to indicate where the file should be searched for. The following are valid values:

  • file:myprops.properties - A relative path to a file somewhere on the file system

  • classpath:myprops.properties - A file relative to the root of the classpath

  • myprops.properties - A file relative on the classpath relative to the test being run.

If you need more dynamic property definition or the property you want to define requires some setup then you can implement the TestPropertyProvider interface in your test and do whatever setup is necessary then return the properties you want to expose the the application.

For example:

Using the TestPropertyProvider interface
package io.micronaut.test.junit5;

import io.micronaut.context.annotation.Property;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.micronaut.test.support.TestPropertyProvider;
import org.junit.jupiter.api.*;

import java.util.Map;

@MicronautTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PropertySourceMapTest implements TestPropertyProvider {

    @Property(name = "foo.bar")
    String val;

    @Test
    void testPropertySource() {
        Assertions.assertEquals("one", val);
    }

    @NonNull
    @Override
    public Map<String, String> getProperties() {
        return CollectionUtils.mapOf(
                "foo.bar", "one",
                "foo.baz", "two"
        );
    }
}
When using TestPropertyProvider your test must be declared with JUnit’s @TestInstance(TestInstance.Lifecycle.PER_CLASS) annotation.

4.9 Refreshing injected beans based on `@Requires` upon properties changes

You can use combine the use of @Requires and @Property, so that injected beans will be refreshed if there are configuration changes that affect their @Requires condition.

For that to work, the test must be annotated with @MicronautTest(rebuildContext = true). In that case, if there are changes in any property for a given test, the application context will be rebuilt so that @Requires conditions are re-evaluated again.

For example:

Combining @Requires and @Property in a @Refreshable test class.
package io.micronaut.test.junit5;

import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Requires;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.hamcrest.MatcherAssert;
import org.hamcrest.core.IsInstanceOf;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

// https://github.com/micronaut-projects/micronaut-test/issues/91
@MicronautTest(rebuildContext = true)
@Property(name = "foo.bar", value = "stuff")
@TestMethodOrder(OrderAnnotation.class)
class PropertyValueRequiresTest {

    @Inject
    MyService myService;

    @Test
    @Order(1)
    void testInitialValue() {
        MatcherAssert.assertThat(myService, IsInstanceOf.instanceOf(MyServiceStuff.class));
    }

    @Property(name = "foo.bar", value = "changed")
    @Test
    @Order(2)
    void testValueChanged() {
        MatcherAssert.assertThat(myService, IsInstanceOf.instanceOf(MyServiceChanged.class));
    }

    @Test
    @Order(3)
    void testValueRestored() {
        MatcherAssert.assertThat(myService, IsInstanceOf.instanceOf(MyServiceStuff.class));
    }

}

interface MyService {}

@Singleton
@Requires(property = "foo.bar", value = "stuff")
class MyServiceStuff implements MyService {}


@Singleton
@Requires(property = "foo.bar", value = "changed")
class MyServiceChanged implements MyService {}

4.10 Testing External Servers

You can write integration tests that test external servers in a couple of different ways.

One way is with the micronaut.test.server.executable property that allows you to specify the location of an executable JAR or native image of a server that should be started and shutdown for the lifecycle of test.

In this case Micronaut Test will replace the regular server with an instance of TestExecutableEmbeddedServer that executes the process to start the server and closes the process when the test ends.

For example:

Using micronaut.test.server.executable
@Property(
    name = TestExecutableEmbeddedServer.PROPERTY,
    value = "src/test/apps/test-app.jar"
)

Alternatively if you have independently started an EmbeddedServer instance programmatically you can also specify the URL to the server with the micronaut.test.server.url property.

4.11 Test Methods Injection

By default, with JUnit 5 the test method parameters will be resolved to beans if possible. As this behaviour can be problematic if in combination with the @ParameterizedTest annotation, it can be disabled.

package io.micronaut.test.junit5;

import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Requires;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import jakarta.inject.Singleton;
import java.util.stream.Stream;

@Property(name = "spec.name", value = "DisableResolveParametersTest")
@MicronautTest(resolveParameters = false)
class DisableResolveParametersTest {
    static Stream<Arguments> fooArgs() {
        return Stream.of(Arguments.of(new Foo()));
    }

    @ParameterizedTest
    @MethodSource("fooArgs")
    void foo(Foo arg) { (1)
        Assertions.assertNotNull(arg);
    }

    @Test
    @Disabled("Doesn't work with resolverParameters set to false") (2)
    void bar(Foo arg) {
        Assertions.assertNotNull(arg);
    }

    @Requires(property = "spec.name", value = "DisableResolveParametersTest")
    @Singleton
    static class Foo {
    }
}
1 Is executed with parameters that are generated by the fooArgs method
2 Throws exception since method injection is completely disabled by resolveParameters = false

5 Testing with Kotest 5

5.1 Setting up Kotest 5

To get started using Kotest 5 you need the following dependencies in your build configuration:

build.gradle
dependencies {
    kaptTest "io.micronaut:micronaut-inject-java"
    testImplementation "io.micronaut.test:micronaut-test-kotest5:4.6.2"
    testImplementation "io.mockk:mockk:{mockkVersion}"
    testImplementation "io.kotest:kotest-runner-junit5-jvm:{kotestVersion}"
}

// use JUnit 5 platform
test {
    useJUnitPlatform()
}

Or for Maven:

pom.xml
<dependency>
    <groupId>io.micronaut.test</groupId>
    <artifactId>micronaut-test-kotest5</artifactId>
    <version>4.6.2</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.mockk</groupId>
    <artifactId>mockk</artifactId>
    <version>{mockkVersion}</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.kotest</groupId>
    <artifactId>kotest-runner-junit5-jvm</artifactId>
    <version>{kotestVersion}</version>
    <scope>test</scope>
</dependency>

Note that for Maven you will also need to configure the Surefire plugin to use JUnit platform and configure the kotlin maven plugin:

pom.xml
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.22.2</version>
    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.6.2</version>
        </dependency>
    </dependencies>
</plugin>
<plugin>
    <artifactId>kotlin-maven-plugin</artifactId>
    <groupId>org.jetbrains.kotlin</groupId>
    <version>1.4.10</version>
    <configuration>
        <compilerPlugins>
            <plugin>all-open</plugin>
        </compilerPlugins>
        <pluginOptions>
            <option>all-open:annotation=io.micronaut.aop.Around</option>
        </pluginOptions>
    </configuration>
    <executions>
        <execution>
            <id>kapt</id>
            <goals>
                <goal>kapt</goal>
            </goals>
            <configuration>
                <sourceDirs>
                    <sourceDir>${project.baseDir}/src/main/kotlin</sourceDir>
                </sourceDirs>
                <annotationProcessorPaths>
                    <annotationProcessorPath>
                        <groupId>io.micronaut</groupId>
                        <artifactId>micronaut-inject-java</artifactId>
                        <version>${micronaut.version}</version>
                    </annotationProcessorPath>
                    <annotationProcessorPath>
                        <groupId>io.micronaut</groupId>
                        <artifactId>micronaut-validation</artifactId>
                        <version>${micronaut.version}</version>
                    </annotationProcessorPath>
                </annotationProcessorPaths>
            </configuration>
        </execution>
        <execution>
            <id>compile</id>
            <goals>
                <goal>compile</goal>
            </goals>
            <configuration>
                <sourceDirs>
                    <sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
                    <sourceDir>${project.basedir}/src/main/java</sourceDir>
                </sourceDirs>
            </configuration>
        </execution>
        <execution>
            <id>test-kapt</id>
            <goals>
                <goal>test-kapt</goal>
            </goals>
            <configuration>
                <sourceDirs>
                    <sourceDir>src/test/kotlin</sourceDir>
                </sourceDirs>
                <annotationProcessorPaths>
                    <annotationProcessorPath>
                        <groupId>io.micronaut</groupId>
                        <artifactId>micronaut-inject-java</artifactId>
                        <version>${micronaut.version}</version>
                    </annotationProcessorPath>
                </annotationProcessorPaths>
            </configuration>
        </execution>
        <execution>
            <id>test-compile</id>
            <goals>
                <goal>test-compile</goal>
            </goals>
            <configuration>
                <sourceDirs>
                    <sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
                    <sourceDir>${project.basedir}/target/generated-sources/kapt/test</sourceDir>
                </sourceDirs>
            </configuration>
        </execution>
    </executions>
    <dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-allopen</artifactId>
            <version>${kotlinVersion}</version>
        </dependency>
    </dependencies>
</plugin>

5.2 Before You Begin

Before you can get started writing tests with Kotest, it is necessary to inform Kotest of the Micronaut extensions. The way to do that is by providing a ProjectConfig. Here is how to do so for Micronaut Test:

package io.micronaut.test.kotest5

import io.kotest.core.config.AbstractProjectConfig
import io.micronaut.test.extensions.kotest5.MicronautKotest5Extension

@Suppress("unused")
object ProjectConfig : AbstractProjectConfig() {
    override fun extensions() = listOf(MicronautKotest5Extension)
}

5.3 Writing a Micronaut Test with Kotest

Let’s take a look at an example using Kotest. Consider you have the following interface:

The MathService Interface
package io.micronaut.test.kotest5

interface MathService {

    fun compute(num: Int): Int
}

And a simple implementation that computes the value times 4 and is defined as a Micronaut bean:

The MathService implementation
package io.micronaut.test.kotest5

import jakarta.inject.Singleton

@Singleton
internal class MathServiceImpl : MathService {

    override fun compute(num: Int): Int {
        return num * 4
    }
}

You can define the following test to test the implementation:

The MathService specification
package io.micronaut.test.kotest5

import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import io.micronaut.test.extensions.kotest5.annotation.MicronautTest

@MicronautTest (1)
class MathServiceTest(
    private val mathService: MathService (2)
) : BehaviorSpec({

    given("the math service") {

        `when`("the service is called with 2") {
            val result = mathService.compute(2) (3)
            then("the result is 8") {
                result shouldBe 8
            }
        }

        `when`("the service is called with 3") {
            val result = mathService.compute(3)
            then("the result is 12") {
                result shouldBe 12
            }
        }
    }
})
1 The test is declared as Micronaut test with @MicronautTest
2 The constructor is used to inject the bean
3 The test itself tests the injected bean

5.4 Environments, Classpath Scanning etc.

The @MicronautTest annotation supports specifying the environment names the test should run with:

@MicronautTest(environments={"foo", "bar"})

In addition, although Micronaut itself doesn’t scan the classpath, some integrations do (such as JPA and GORM), for these cases you may wish to specify either the application class:

@MicronautTest(application=Application.class)

Or the packages:

@MicronautTest(packages="foo.bar")

To ensure that entities can be found during classpath scanning.

5.5 Transaction semantics

When using @MicronautTest each @Test method will be wrapped in a transaction that will be rolled back when the test finishes. This behaviour can be changed by using the transactional and rollback properties.

To avoid creating a transaction:

@MicronautTest(transactional = false)

To not rollback the transaction:

@MicronautTest(rollback = false)

Additionally, the transactionMode property can be used to further customize the way that transactions are handled for each test:

@MicronautTest(transactionMode = TransactionMode.SINGLE_TRANSACTION)

The following transaction modes are supported:

  • SEPARATE_TRANSACTIONS (default) - Each setup/cleanup method is wrapped in its own transaction, separate from that of the test. This transaction is always committed.

  • SINGLE_TRANSACTION - All setup methods are wrapped in the same transaction as the test. Cleanup methods are wrapped in separate transactions.

Setup and cleanup methods are not wrapped in transactions in Kotest currently. As a result, transaction modes have no effect in Kotest.

5.6 Using Mockk Mocks

Now let’s say you want to replace the implementation with a Mockk. You can do so by defining a method that returns a mock and is annotated with @MockBean, for example:

The MathService specification
package io.micronaut.test.kotest5

import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import io.micronaut.test.extensions.kotest5.annotation.MicronautTest
import io.micronaut.test.annotation.MockBean
import io.micronaut.test.extensions.kotest5.MicronautKotest5Extension.getMock
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify

import kotlin.math.pow
import kotlin.math.roundToInt

@MicronautTest
class MathMockServiceTest(
    private val mathService: MathService (3)
) : BehaviorSpec({

    given("test compute num to square") {

        `when`("the mock is provided") {
            val mock = getMock(mathService) (4)
            every { mock.compute(any()) } answers {
                firstArg<Int>().toDouble().pow(2).roundToInt()
            }

            then("the mock implementation is used") {
                mock.compute(3) shouldBe 9
                verify { mock.compute(3) } (5)
            }
        }
    }

}) {

    @MockBean(MathServiceImpl::class) (1)
    fun mathService(): MathService {
        return mockk() (2)
    }
}
1 The @MockBean annotation is used to indicate the method returns a mock bean. The value to the method is the type being replaced.
2 Mockk’s mockk(..) method creates the actual mock
3 The math service proxy is injected into the test
4 The call to getMock is used to retrieve the underlying mock
5 Mockk is used to verify the mock is called

Note that because the bean is a method of the test, it will be active only for the scope of the test. This approach allows you to define beans that are isolated per test class.

Because Kotlin uses constructor injection, it’s not possible to automatically replace the mock proxy with the mock implementation as is done with the other test implementations. The getMock method was created to make retrieving the underlying mock object easier.

5.7 Mocking Collaborators

Note that in most cases you won’t define a @MockBean and inject it only to verify interaction with the Mock directly. Instead, the Mock will be a collaborator within your application. For example say you have a MathController:

The MathController
package io.micronaut.test.kotest5

import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/math")
class MathController internal constructor(internal var mathService: MathService) {

    @Get(uri = "/compute/{number}", processes = [MediaType.TEXT_PLAIN])
    internal fun compute(number: Int): String {
        return mathService.compute(number).toString()
    }
}

The above controller uses the MathService to expose a /math/compute/{number} endpoint. See the following example for a test that tests interaction with the mock collaborator:

Mocking Collaborators
package io.micronaut.test.kotest5

import io.kotest.core.spec.style.StringSpec
import io.kotest.data.blocking.forAll
import io.kotest.data.row
import io.kotest.matchers.shouldBe
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.kotest5.annotation.MicronautTest
import io.micronaut.test.annotation.MockBean
import io.micronaut.test.extensions.kotest5.MicronautKotest5Extension.getMock
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlin.math.pow
import kotlin.math.roundToInt

@MicronautTest
class MathCollaboratorTest(
    private val mathService: MathService,
    @Client("/") private val client: HttpClient (2)
) : StringSpec({

    "test compute num to square" {
        val mock = getMock(mathService)

        every { mock.compute(any()) } answers {
            firstArg<Int>().toDouble().pow(2).roundToInt()
        }

        forAll(
            row(2, 4),
            row(3, 9)
        ) { a: Int, b: Int ->
            val result = client.toBlocking().retrieve("/math/compute/$a", Int::class.java) (3)
            result shouldBe b
            verify { mock.compute(a) } (4)
        }

    }

}) {

    @MockBean(MathServiceImpl::class) (1)
    fun mathService(): MathService {
        return mockk()
    }
}
1 Like the previous example a Mock is defined using @MockBean
2 This time we inject an instance of HttpClient to test the controller.
3 We invoke the controller and retrieve the result
4 The interaction with mock collaborator is verified.

The way this works is that @MicronautTest will inject a proxy that points to the mock instance. For each iteration of the test the mock is refreshed (in fact it uses Micronaut’s built in RefreshScope).

5.8 Using `@Requires` on Tests

Since @MicronautTest turns tests into beans themselves, it means you can use the @Requires annotation on the test to enable/disable tests. For example:

@MicronautTest
@Requires(env = "my-env")
class RequiresTest {
    ...
}

The above test will only run if my-env is active (you can activate it by passing the system property micronaut.environments).

5.9 Defining Additional Test Specific Properties

You can define additional test specific properties using the @Property annotation. The following example demonstrates usage:

Using @Property
package io.micronaut.test.kotest5

import io.kotest.core.spec.style.AnnotationSpec
import io.kotest.matchers.shouldBe
import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Value
import io.micronaut.test.extensions.kotest5.annotation.MicronautTest

@MicronautTest
@Property(name = "foo.bar", value = "stuff")
class PropertyValueTest: AnnotationSpec() {

    @Value("\${foo.bar}")
    lateinit var value: String

    @Test
    fun testInitialValue() {
        value shouldBe "stuff"
    }

    @Property(name = "foo.bar", value = "changed")
    @Test
    fun testValueChanged() {
        value shouldBe "changed"
    }

    @Test
    fun testValueRestored() {
        value shouldBe "stuff"
    }
}

Alternatively you can specify additional propertySources in any supported format (YAML, JSON, Java properties file etc.) using the @MicronautTest annotation:

Using propertySources stored in files
package io.micronaut.test.kotest5

import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import io.micronaut.context.annotation.Property
import io.micronaut.test.extensions.kotest5.annotation.MicronautTest

@MicronautTest(propertySources = ["myprops.properties"])
@Property(name = "supplied.value", value = "hello")
class PropertySourceTest(@Property(name = "foo.bar") val value: String,
                         @Property(name = "supplied.value") val suppliedValue: String) : BehaviorSpec({

    given("a property source") {
        `when`("the value is injected") {
            then("the correct value is injected") {
                value shouldBe "foo"
                suppliedValue shouldBe "hello"
            }
        }
    }

})

The above example expects a file located at src/test/resources/io/micronaut/kotest/myprops.properties. You can however use a prefix to indicate where the file should be searched for. The following are valid values:

  • file:myprops.properties - A relative path to a file somewhere on the file system

  • classpath:myprops.properties - A file relative to the root of the classpath

  • myprops.properties - A file relative on the classpath relative to the test being run.

Because Kotlin doesn’t support multiple annotations, the @PropertySource annotation must be used to define multiple properties.

5.10 Constructor Injection Caveats

There are a couple caveats to using constructor injection to be aware of.

  1. In order for TestPropertyProvider to work, test classes must have not have any constructor arguments. This is because the class needs to be constructed prior to bean creation, in order to add the properties to the context. Fields and methods will still be injected.

  2. @Requires() cannot be used with constructor injection because Kotest requires the instance to be created regardless if the test should be ignored or not. If the requirements disable the bean, it cannot be created from the context and thus construction responsibility will be delegated to the default behavior.

5.11 Refreshing injected beans based on `@Requires` upon properties changes

You can use combine the use of @Requires and @Property, so that injected beans will be refreshed if there are configuration changes that affect their @Requires condition.

For that to work, the test must be annotated with @MicronautTest(rebuildContext = true). In that case, if there are changes in any property for a given test, the application context will be rebuilt so that @Requires conditions are re-evaluated again.

For example:

Combining @Requires and @Property in a @Refreshable test class.
package io.micronaut.test.kotest5

import io.kotest.core.spec.style.AnnotationSpec
import io.kotest.matchers.types.shouldBeInstanceOf
import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.test.extensions.kotest5.annotation.MicronautTest
import jakarta.inject.Inject
import jakarta.inject.Singleton

@MicronautTest(rebuildContext = true)
@Property(name = "foo.bar", value = "stuff")
class PropertyValueRequiresTest: AnnotationSpec() {

    @Inject
    lateinit var myService: MyService

    @Test
    fun testInitialValue() {
        myService.shouldBeInstanceOf<MyServiceStuff>()
    }

    @Property(name = "foo.bar", value = "changed")
    @Test
    fun testValueChanged() {
        myService.shouldBeInstanceOf<MyServiceChanged>()
    }

    @Test
    fun testValueRestored() {
        myService.shouldBeInstanceOf<MyServiceStuff>()
    }
}

interface MyService

@Singleton
@Requires(property = "foo.bar", value = "stuff")
open class MyServiceStuff : MyService


@Singleton
@Requires(property = "foo.bar", value = "changed")
open class MyServiceChanged : MyService

6 REST Assured

A small utility module exists that helps integrate the REST-assured library. Simply add the following dependency:

testImplementation("io.micronaut.test:micronaut-test-rest-assured")
<dependency>
    <groupId>io.micronaut.test</groupId>
    <artifactId>micronaut-test-rest-assured</artifactId>
    <scope>test</scope>
</dependency>

You can then inject instances of RequestSpecification into test fields or method parameters (parameters are only supported with JUnit 5):

REST-Assured Example
package io.micronaut.test.rest.assured;

import static org.hamcrest.CoreMatchers.is;
import org.junit.jupiter.api.Test;

import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import io.restassured.specification.RequestSpecification;

@MicronautTest 
public class RestAssuredHelloWorldTest {
    @Test
    void testHelloWorld(RequestSpecification spec) {
        spec
            .when().get("/hello/world")
            .then().statusCode(200)
                .body(is("Hello World"));
    }
}

7 Loading SQL before tests

When performing a test with a backing database, often some data is required in the database prior to running the tests. As of micronaut-test version 4.1.0, there is an annotation Sql.

This annotation can be used to specify the location of one or more sql files to be executed at one of four phases in your test execution:

  • BEFORE_CLASS - executed once before the tests are run (the default).

  • BEFORE_METHOD - executed before each test method.

  • AFTER_METHOD - executed after each test method.

  • AFTER_CLASS - executed once after all the tests are run.

The files are executed in the order specified in the annotation.

For example given the two SQL scripts

test/resources/create.sql
CREATE TABLE MyTable (
    ID INT NOT NULL,
    NAME VARCHAR(255),
    PRIMARY KEY (ID)
);

and

test/resources/datasource_1_insert.sql
INSERT INTO MyTable (ID, NAME) VALUES (1, 'Aardvark');
INSERT INTO MyTable (ID, NAME) VALUES (2, 'Albatross');

We can annotate a test to run these two scripts prior to the test.

@MicronautTest(transactionMode = TransactionMode.SINGLE_TRANSACTION)
@Property(name = "datasources.default.dialect", value = "H2")
@Property(name = "datasources.default.driverClassName", value = "org.h2.Driver")
@Property(name = "datasources.default.schema-generate", value = "CREATE_DROP")
@Property(name = "datasources.default.url", value = "jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
@Property(name = "datasources.default.username", value = "sa")

@Sql({"classpath:create.sql", "classpath:datasource_1_insert.sql"}) (1)
class SqlDatasourceTest {

    @Inject
    DataSource dataSource;

    @Test
    void dataIsInserted() throws Exception {
        assertEquals(List.of("Aardvark", "Albatross"), readAllNames(dataSource));
    }

    List<String> readAllNames(DataSource dataSource) throws SQLException {
        var result = new ArrayList<String>();
        try (
                Connection ds = dataSource.getConnection();
                PreparedStatement ps = ds.prepareStatement("select name from MyTable");
                ResultSet rslt = ps.executeQuery()
        ) {
            while(rslt.next()) {
                result.add(rslt.getString(1));
            }
        }
        return result;
    }
}
@MicronautTest
@Property(name = "datasources.default.dialect", value = "H2")
@Property(name = "datasources.default.driverClassName", value = "org.h2.Driver")
@Property(name = "datasources.default.schema-generate", value = "CREATE_DROP")
@Property(name = "datasources.default.url", value = "jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
@Property(name = "datasources.default.username", value = "sa")

@Sql(["classpath:create.sql", "classpath:datasource_1_insert.sql"]) (1)
class SqlDatasourceSpec extends Specification {

    @Inject
    DataSource dataSource

    def "data is inserted"() {
        expect:
        readAllNames(dataSource) == ["Aardvark", "Albatross"]
    }

    List<String> readAllNames(DataSource dataSource) {
        dataSource.getConnection().withCloseable {
            it.prepareStatement("select name from MyTable").withCloseable {
                it.executeQuery().withCloseable {
                    def names = []
                    while (it.next()) {
                        names << it.getString(1)
                    }
                    names
                }
            }
        }
    }
}
@MicronautTest
@Property(name = "datasources.default.dialect", value = "H2")
@Property(name = "datasources.default.driverClassName", value = "org.h2.Driver")
@Property(name = "datasources.default.schema-generate", value = "CREATE_DROP")
@Property(name = "datasources.default.url", value = "jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
@Property(name = "datasources.default.username", value = "sa")

@Sql("classpath:create.sql", "classpath:datasource_1_insert.sql") (1)
class SqlDatasourceTest(
    private val dataSource: DataSource
): BehaviorSpec({

    fun readAllNames(dataSource: DataSource): List<String> {
        val result = mutableListOf<String>()
        dataSource.connection.use { ds ->
            ds.prepareStatement("select name from MyTable").use { ps ->
                ps.executeQuery().use { rslt ->
                    while (rslt.next()) {
                        result.add(rslt.getString(1))
                    }
                }
            }
        }
        return result
    }

    given("a test with the Sql annotation") {
        then("the data is inserted as expected") {
            readAllNames(dataSource) shouldBe listOf("Aardvark", "Albatross")
        }
    }
})
1 Specify the location of the SQL scripts to be executed for a DataSource with the name default.

Phases

The default phase for the scripts to be executed is BEFORE_CLASS. To run the scripts at a different phase, we can specify the phase attribute of the annotation.

@Sql(scripts = "classpath:rollbacktwoproducts.sql", phase = Sql.Phase.AFTER_EACH) (1)
1 A script to run after each test in the specification.

Named Datasources

If you have multiple datasources configured, you can specify the datasource name to use for the SQL scripts.

@MicronautTest

@Sql(dataSourceName = "one", value = {"classpath:create.sql", "classpath:datasource_1_insert.sql"}) (1)
@Property(name = "datasources.one.dialect", value = "H2")
@Property(name = "datasources.one.driverClassName", value = "org.h2.Driver")
@Property(name = "datasources.one.schema-generate", value = "CREATE_DROP")
@Property(name = "datasources.one.url", value = "jdbc:h2:mem:databaseOne;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
@Property(name = "datasources.one.username", value = "sa")

@Sql(dataSourceName = "two", scripts = {"classpath:create.sql", "classpath:datasource_2_insert.sql"}) (1)
@Property(name = "datasources.two.dialect", value = "H2")
@Property(name = "datasources.two.driverClassName", value = "org.h2.Driver")
@Property(name = "datasources.two.schema-generate", value = "CREATE_DROP")
@Property(name = "datasources.two.url", value = "jdbc:h2:mem:databaseTwo;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
@Property(name = "datasources.two.username", value = "sa")
class SqlNamedDatasourceTest {

    @Inject
    @Named("one")
    DataSource dataSource1;

    @Inject
    @Named("two")
    DataSource dataSource2;

    @Test
    void dataIsInserted() throws Exception {
        assertEquals(List.of("Aardvark", "Albatross"), readAllNames(dataSource1));
        assertEquals(List.of("Bear", "Bumblebee"), readAllNames(dataSource2));
    }

    List<String> readAllNames(DataSource dataSource) throws SQLException {
        var result = new ArrayList<String>();
        try (
                Connection ds = dataSource.getConnection();
                PreparedStatement ps = ds.prepareStatement("select name from MyTable");
                ResultSet rslt = ps.executeQuery()
        ) {
            while(rslt.next()) {
                result.add(rslt.getString(1));
            }
        }
        return result;
    }
}
@MicronautTest
@Sql(dataSourceName = "one", value = ["classpath:create.sql", "classpath:datasource_1_insert.sql"]) (1)
@Property(name = "datasources.one.dialect", value = "H2")
@Property(name = "datasources.one.driverClassName", value = "org.h2.Driver")
@Property(name = "datasources.one.schema-generate", value = "CREATE_DROP")
@Property(name = "datasources.one.url", value = "jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
@Property(name = "datasources.one.username", value = "sa")

@Sql(dataSourceName = "two", value = ["classpath:create.sql", "classpath:datasource_2_insert.sql"]) (1)
@Property(name = "datasources.two.dialect", value = "H2")
@Property(name = "datasources.two.driverClassName", value = "org.h2.Driver")
@Property(name = "datasources.two.schema-generate", value = "CREATE_DROP")
@Property(name = "datasources.two.url", value = "jdbc:h2:mem:devDb2;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
@Property(name = "datasources.two.username", value = "sa")
class SqlNamedDatasourceSpec extends Specification {

    @Inject
    @Named("one")
    DataSource dataSource1

    @Inject
    @Named("two")
    DataSource dataSource2

    def "data is inserted"() {
        expect:
        readAllNames(dataSource1) == ["Aardvark", "Albatross"]

        and:
        readAllNames(dataSource2) == ["Bear", "Bumblebee"]
    }

    List<String> readAllNames(DataSource dataSource) {
        dataSource.getConnection().withCloseable {
            it.prepareStatement("select name from MyTable").withCloseable {
                it.executeQuery().withCloseable {
                    def names = []
                    while (it.next()) {
                        names << it.getString(1)
                    }
                    names
                }
            }
        }
    }
}
@MicronautTest
@Sql(dataSourceName = "one", value = ["classpath:create.sql", "classpath:datasource_1_insert.sql"]) (1)
@Property(name = "datasources.one.dialect", value = "H2")
@Property(name = "datasources.one.driverClassName", value = "org.h2.Driver")
@Property(name = "datasources.one.schema-generate", value = "CREATE_DROP")
@Property(name = "datasources.one.url", value = "jdbc:h2:mem:devDb;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
@Property(name = "datasources.one.username", value = "sa")

@Sql(dataSourceName = "two", value = ["classpath:create.sql", "classpath:datasource_2_insert.sql"]) (1)
@Property(name = "datasources.two.dialect", value = "H2")
@Property(name = "datasources.two.driverClassName", value = "org.h2.Driver")
@Property(name = "datasources.two.schema-generate", value = "CREATE_DROP")
@Property(name = "datasources.two.url", value = "jdbc:h2:mem:devDb2;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE")
@Property(name = "datasources.two.username", value = "sa")
class SqlNamedDatasourceTest(
    @Named("one") private val dataSource1: DataSource,
    @Named("two") private val dataSource2: DataSource
): BehaviorSpec({

    fun readAllNames(dataSource: DataSource): List<String> {
        val result = mutableListOf<String>()
        println("Testing datasource: $dataSource")
        dataSource.connection.use { ds ->
            ds.prepareStatement("select name from MyTable").use { ps ->
                ps.executeQuery().use { rslt ->
                    while (rslt.next()) {
                        result.add(rslt.getString(1))
                    }
                }
            }
        }
        return result
    }

    given("a test with the Sql annotation") {
        then("the data is inserted as expected") {
            readAllNames(dataSource1) shouldBe listOf("Aardvark", "Albatross")
            readAllNames(dataSource2) shouldBe listOf("Bear", "Bumblebee")
        }
    }
})
1 Specify the location of the SQL scripts to be executed for a DataSource with the given name.

R2DBC

For R2DBC, the Sql annotation can be used in the same way as for JDBC however it is required to pass the resourceType as ConnectionFactory.class.

@MicronautTest
@Property(name = "r2dbc.datasources.default.db-type", value = "mysql")
@Sql(value = {"classpath:create.sql", "classpath:datasource_1_insert.sql"}, resourceType = ConnectionFactory.class)
@Testcontainers(disabledWithoutDocker = true)
class MySqlConnectionTest  {

    @Inject
    ConnectionFactory connectionFactory;

    @Test
    void testSqlHasBeenInjected() {
        var f = Flux.from(connectionFactory.create());

        var result = f.flatMap(connection ->
            connection.createStatement("SELECT name from MyTable where id = 2").execute()
        ).flatMap(rslt ->
            rslt.map((row, metadata) -> row.get(0, String.class))
        ).blockFirst();

        assertEquals("Albatross", result);
    }
}

8 Type pollution

Before OpenJDK 23, the Hotspot runtime has a performance issue relating to interface type checks. When the JVM has to do a type check against an interface, such as in arrayList instanceof List, it has to do a fairly complicated iteration of the whole inheritance tree of arrayList. To make this fast, if the check succeeds, the interface is stored in a secondary_super_cache on the JVM Klass structure representing the object type, in this case ArrayList. The next time the JVM has to do a similar type check of an ArrayList against List, the JVM can just check this field instead and avoid walking the whole type tree.

The issue arises when the same concrete type (i.e. ArrayList) is checked against multiple interface types (e.g. List but also Collection) repeatedly. An instanceof List will set the secondary_super_cache to List, then an instanceof Collection will set the cache to Collection, and an instanceof List will set it back to List. Not only can the second type check not take advantage of the cache because it had been overwritten, but crucially, each type checks writes the cache field anew. If this happens concurrently on multi-core machines, this can lead to major inter-core traffic for cache coherency of the cache line where the secondary_super_cache is located. This is called a "scalability issue" because it can lead to poor performance in code running in parallel.

This bug can be triggered by the interplay of multiple usually independent libraries and is very hard to reproduce in practice. For this reason, the folks at RedHat built a type pollution agent that transforms the bytecode of all classes of an application to specifically track these type checks and report any scenarios where the secondary_super_cache may be switching back and forth between multiple types.

Testing for type pollution

micronaut-test-type-pollution is an adaptation of the RedHat type pollution agent that expands its covered type checks somewhat, but also makes it easy to use from tests. The covered type checks are:

  • instanceof

  • casts

  • reflective Class.cast

  • reflective Class.isAssignableFrom

  • reflective Class.isInstance

  • reflective Method.invoke

  • reflective Constructor.newInstance

  • reflective Field.set

This is not an exhaustive list of all the code in the JDK that can trigger the JVM to do a type check, but this is meant to cover most type checks that happen in practice.

Missing entries in this list are considered a bug, even though we know about them. That means this list may be expanded in a patch release of micronaut-test, causing your tests to fail if there were previously undiscovered type pollution sites.
The type pollution test uses a Java agent to instrument all classes of the test suite. This does not play well with other agents, in particular jacoco. It is recommended to run type pollution tests in a separate module / source root with no other Java agents.

When a type check changes the secondary_super_cache (or rather the agent’s model of it), what we call a "focus event", it invokes a static FocusListener. A ThresholdFocusListener will count these events and keep track of their exact stack trace. You then run the code that will be hot in production repeatedly. At the end of the test case, you call the checkThresholds method to verify that there were not too many focus events:

private static final int THRESHOLD = 10_000;
private ThresholdFocusListener focusListener;

@BeforeAll
static void setupAgent() {
    TypePollutionTransformer.install(net.bytebuddy.agent.ByteBuddyAgent.install()); (1)
}

@BeforeEach
void setUp() {
    focusListener = new ThresholdFocusListener(); (2)
    FocusListener.setFocusListener(focusListener);
}

@AfterEach
void verifyNoTypeThrashing() {
    FocusListener.setFocusListener(null); (4)
    Assertions.assertTrue(focusListener.checkThresholds(THRESHOLD), "Threshold exceeded, check logs.");
}

@Test
public void sample() { (3)
    Object c = new Concrete();
    int j = 0;
    for (int i = 0; i < THRESHOLD * 2; i++) {
        if (c instanceof A) {
            j++;
        }
        if (c instanceof B) {
            j++;
        }
    }
    System.out.println(j);
}

interface A { (5)
}

interface B {
}

static class Concrete implements A, B {
}
1 Install the agent into the JVM running the tests
2 Set up the ThresholdFocusListener for the current test
3 Run the code that should be tested
4 Clear the focus listener, and check whether the threshold for this test was exceeded
5 Types used for this example only

9 Repository

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