@Shared (1)
@AutoCleanup (2)
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
Table of Contents
Micronaut Test
Testing Framework Extensions for Micronaut
Version:
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:
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 and Spock:
-
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 found in the io.micronaut.test.annotation
package:
-
@MicronautTest
- Can be added to any Spock or JUnit 5 test. -
@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.
If you have either of the libraries for spock or JUnit 5 in your dependencies, you must include the appropriate dependency for the @MicronautTest
annotation to work. It is not enough to have just micronaut-test-spock
if you have JUnit 5 libraries loaded, you must also include micronaut-test-junit5
and vice versa, or remove the references to JUnit 5 completely.
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
Release History
1.1.x
-
Micronaut 1.1.x or above required
-
Upgrade to JUnit 5.5
-
Support for Dependency Injection in Constructors and Method Parameters for JUnit 5
-
Support for Kotlin Test
1.0.x
-
Initial Version with support for JUnit 5 and Spock
2 Testing with Spock
Setting up Spock
To get started using Spock you need the following dependencies in your build configuration:
testCompile "io.micronaut.test:micronaut-test-spock:1.1.5"
If you plan to define mock beans you will also need micronaut-inject-groovy on your testCompile classpath or micronaut-inject-java for Java or Kotlin (this should already be configured if you used mn create-app ).
|
Or for Maven:
<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>micronaut-test-spock</artifactId>
<version>{version}</version>
<scope>test</scope>
</dependency>
Writing a Micronaut Test with Spock
Let’s take a look at an example using Spock. Consider you have the following interface:
package io.micronaut.test.spock;
import javax.inject.Singleton;
interface MathService {
Integer compute(Integer num);
}
And a simple implementation that computes the value times 4 and is defined as Micronaut bean:
package io.micronaut.test.spock
import javax.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:
package io.micronaut.test.spock
import io.micronaut.test.annotation.MicronautTest
import spock.lang.*
import javax.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 |
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.
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:
package io.micronaut.test.spock
import io.micronaut.test.annotation.*
import spock.lang.*
import javax.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 |
Mocking Collaborators
Note that in most cases you won’t define a @MockBean
and then 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
:
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:
package io.micronaut.test.spock
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.annotation.*
import spock.lang.*
import javax.inject.Inject
@MicronautTest
class MathCollaboratorSpec extends Specification {
@Inject
MathService mathService (2)
@Inject
@Client('/')
RxHttpClient 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 RxHttpClient 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
).
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 active it by passing the system property micronaut.environments
).
Defining Additional Test Specific Properties
You can define additional test specific properties using the @Property
annotation. The following example demonstrates usage:
@Property
package io.micronaut.test.spock
import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Value
import io.micronaut.test.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:
propertySources
stored in filespackage io.micronaut.test.spock
import io.micronaut.context.annotation.Property
import io.micronaut.test.annotation.MicronautTest
import spock.lang.Specification
import javax.inject.Inject
@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.
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:
@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.annotation.MicronautTest
import spock.lang.Issue
import spock.lang.Specification
import javax.inject.Inject
import javax.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 {}
3 Testing with JUnit 5
Setting up JUnit 5
To get started using JUnit 5 you need the following dependencies in your build configuration:
dependencies {
testAnnotationProcessor "io.micronaut:micronaut-inject-java"
...
testCompile "io.micronaut.test:micronaut-test-junit5:1.1.5"
testCompile "org.mockito:mockito-junit-jupiter:2.22.0"
testRuntime "org.junit.jupiter:junit-jupiter-engine:5.1.0"
}
// 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:
<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>micronaut-test-junit5</artifactId>
<version>{version}</version>
<scope>test</scope>
</dependency>
Note that for Maven you will also need to configure the Surefire plugin to use JUnit platform:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19.1</version>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-surefire-provider</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.1.0</version>
</dependency>
</dependencies>
</plugin>
Writing a Micronaut Test with JUnit 5
Let’s take a look at an example using JUnit 5. Consider you have the following interface:
package io.micronaut.test.junit5;
interface MathService {
Integer compute(Integer num);
}
And a simple implementation that computes the value times 4 and is defined as Micronaut bean:
package io.micronaut.test.junit5;
import javax.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:
package io.micronaut.test.junit5;
import io.micronaut.test.annotation.MicronautTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import javax.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 |
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.
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:
package io.micronaut.test.junit5;
import io.micronaut.test.annotation.MicronautTest;
import io.micronaut.test.annotation.MockBean;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.stubbing.Answer;
import javax.inject.Inject;
import static org.mockito.Mockito.*;
@MicronautTest
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.
Mocking Collaborators
Note that in most cases you won’t define a @MockBean
and then 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
:
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:
package io.micronaut.test.junit5;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.client.RxHttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.test.annotation.*;
import io.micronaut.test.annotation.MockBean;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.mockito.Mockito.*;
import javax.inject.Inject;
@MicronautTest
class MathCollaboratorTest {
@Inject
MathService mathService;
@Inject
@Client("/")
RxHttpClient 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 RxHttpClient 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
).
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 active it by passing the system property micronaut.environments
).
Defining Additional Test Specific Properties
You can define additional test specific properties using the @Property
annotation. The following example demonstrates usage:
@Property
package io.micronaut.test.junit5;
import io.micronaut.context.annotation.Property;
import io.micronaut.test.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.*;
@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:
propertySources
stored in filespackage io.micronaut.test.junit5;
import io.micronaut.context.annotation.Property;
import io.micronaut.test.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:
TestPropertyProvider
interfacepackage io.micronaut.test.junit5;
import io.micronaut.context.annotation.Property;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.test.annotation.MicronautTest;
import io.micronaut.test.support.TestPropertyProvider;
import org.junit.jupiter.api.*;
import javax.annotation.Nonnull;
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 you test must be declared with JUnit’s @TestInstance(TestInstance.Lifecycle.PER_CLASS) annotation.
|
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:
@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.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 javax.inject.Inject;
import javax.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 Testing with KotlinTest
Setting up KotlinTest
To get started using KotlinTest you need the following dependencies in your build configuration:
dependencies {
kaptTest "io.micronaut:micronaut-inject-java"
testImplementation "io.micronaut.test:micronaut-test-kotlintest:1.1.5"
testImplementation "io.mockk:mockk:1.9.3"
testImplementation "io.kotlintest:kotlintest-runner-junit5:3.3.2"
}
// use JUnit 5 platform
test {
useJUnitPlatform()
}
Or for Maven:
<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>micronaut-test-kotlintest</artifactId>
<version>{version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>1.9.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.kotlintest</groupId>
<artifactId>kotlintest-runner-junit5</artifactId>
<version>3.3.2</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:
<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.3.1</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>1.3.20</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>
Before You Begin
Before you can get started writing tests with KotlinTest, it is necessary to inform KotlinTest of the Micronaut extensions. The way to do that is by providing a ProjectConfig
object in a specific package. Here is how to do so for Micronaut Test:
package io.kotlintest.provided
import io.kotlintest.AbstractProjectConfig
import io.micronaut.test.extensions.kotlintest.MicronautKotlinTestExtension
object ProjectConfig : AbstractProjectConfig() {
override fun listeners() = listOf(MicronautKotlinTestExtension)
override fun extensions() = listOf(MicronautKotlinTestExtension)
}
Writing a Micronaut Test with KotlinTest
Let’s take a look at an example using KotlinTest. Consider you have the following interface:
package io.micronaut.test.kotlintest
interface MathService {
fun compute(num: Int): Int
}
And a simple implementation that computes the value times 4 and is defined as Micronaut bean:
package io.micronaut.test.kotlintest
import javax.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:
package io.micronaut.test.kotlintest
import io.kotlintest.shouldBe
import io.kotlintest.specs.BehaviorSpec
import io.micronaut.test.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 |
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.
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:
package io.micronaut.test.kotlintest
import io.kotlintest.shouldBe
import io.kotlintest.specs.BehaviorSpec
import io.micronaut.test.annotation.MicronautTest
import io.micronaut.test.annotation.MockBean
import io.micronaut.test.extensions.kotlintest.MicronautKotlinTestExtension.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.
|
Mocking Collaborators
Note that in most cases you won’t define a @MockBean
and then 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
:
package io.micronaut.test.kotlintest
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:
package io.micronaut.test.kotlintest
import io.kotlintest.data.forall
import io.kotlintest.shouldBe
import io.kotlintest.specs.StringSpec
import io.kotlintest.tables.row
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.annotation.MicronautTest
import io.micronaut.test.annotation.MockBean
import io.micronaut.test.extensions.kotlintest.MicronautKotlinTestExtension.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: RxHttpClient (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 RxHttpClient 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
).
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 active it by passing the system property micronaut.environments
).
Defining Additional Test Specific Properties
You can define additional test specific properties using the @Property
annotation. The following example demonstrates usage:
@Property
package io.micronaut.test.kotlintest
import io.kotlintest.shouldBe
import io.kotlintest.specs.AnnotationSpec
import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Value
import io.micronaut.test.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:
propertySources
stored in filespackage io.micronaut.test.kotlintest
import io.kotlintest.shouldBe
import io.kotlintest.specs.BehaviorSpec
import io.micronaut.context.annotation.Property
import io.micronaut.test.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/kotlintest/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.
|
Constructor Injection Caveats
There are a couple caveats to using constructor injection to be aware of.
-
In order for
TestPropertyProvider
to work, test classes must have not have any constructor arguments. This is because the class needs constructed prior to bean creation in order to add the properties to the context. Fields and methods will still be injected. -
@Requires()
cannot be used with constructor injection because Kotlintest 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.
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:
@Requires
and @Property
in a @Refreshable
test class.package io.micronaut.test.kotlintest
import io.kotlintest.matchers.types.shouldBeInstanceOf
import io.kotlintest.specs.AnnotationSpec
import io.micronaut.context.annotation.Property
import io.micronaut.context.annotation.Requires
import io.micronaut.test.annotation.MicronautTest
import javax.inject.Inject
import javax.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
5 Repository
You can find the source code of this project in this repository: