@Shared (1)
@AutoCleanup (2)
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer)
Micronaut Test
Testing Framework Extensions for Micronaut
Version: 1.0.0.M3
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.
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.
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:test-spock:1.0.0.M3
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 ).
|
Or for Maven:
<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>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;
@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 |
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
).
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 {
...
testCompile "io.micronaut.test:test-junit5:1.0.0.M3"
testCompile "org.mockito:mockito-junit-jupiter:2.22.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 ).
|
Or for Maven:
<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>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 |
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()); (4)
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
).