Search code examples
javamicronaut

@MicronautTest - limit what is instantiated


Whenever I used @MicronautTest in my project, the whole application is started as a whole and because certain things need to be done at startup by services, they are executed also. I don't need the whole Singleton shebang when I'm testing a simple controller.

My question is how do I limit which controllers and singletons are loaded when testing Micronaut?

I have a small demo example which shows the problem

Pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>micronaut-test</artifactId>
    <version>0.1</version>
    <packaging>${packaging}</packaging>

    <parent>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-parent</artifactId>
        <version>2.5.1</version>
    </parent>

    <properties>
        <packaging>jar</packaging>
        <jdk.version>11</jdk.version>
        <release.version>11</release.version>
        <micronaut.version>2.5.1</micronaut.version>
        <exec.mainClass>com.example.Application</exec.mainClass>
        <micronaut.runtime>netty</micronaut.runtime>
    </properties>

    <repositories>
        <repository>
            <id>central</id>
            <url>https://repo.maven.apache.org/maven2</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>io.micronaut</groupId>
            <artifactId>micronaut-inject</artifactId>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>io.micronaut</groupId>
            <artifactId>micronaut-validation</artifactId>
            <scope>compile</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>
        <dependency>
            <groupId>io.micronaut.test</groupId>
            <artifactId>micronaut-test-junit5</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.micronaut</groupId>
            <artifactId>micronaut-http-client</artifactId>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>io.micronaut</groupId>
            <artifactId>micronaut-http-server-netty</artifactId>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>io.micronaut</groupId>
            <artifactId>micronaut-runtime</artifactId>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>io.micronaut.build</groupId>
                <artifactId>micronaut-maven-plugin</artifactId>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <!-- Uncomment to enable incremental compilation -->
                    <!-- <useIncrementalCompilation>false</useIncrementalCompilation> -->

                    <annotationProcessorPaths combine.children="append">
                    </annotationProcessorPaths>
                    <compilerArgs>
                        <arg>-Amicronaut.processing.group=com.example</arg>
                        <arg>-Amicronaut.processing.module=micronaut-test</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

application.java

package com.example;

import io.micronaut.runtime.Micronaut;

public class Application {

    public static void main(String[] args) {
        Micronaut.run(Application.class, args);
    }
}

A simple service that throws whenever it is instantiated to indicate the problem

@Singleton
public class SingletonService {

    @EventListener
    public void init(ApplicationStartupEvent event) {
        throw new RuntimeException("I was initialized");
    }
}

A simple controller

@Controller
public class TestController {

    @Get
    public Simple findAll() {
        return new Simple("ATest");
    }

    public static class Simple {
        private final String test;

        public Simple(String test) {
            this.test = test;
        }

        public String getTest() {
            return test;
        }
    }
}

And a straightforward test

@MicronautTest
class TestControllerTest {
    @Inject
    @Client("/")
    RxHttpClient rxHttpClient;

    @Test
    void controller() {
        HttpResponse<ByteBuffer> byteBufferHttpResponse = rxHttpClient.exchange("/").blockingFirst();
        assert byteBufferHttpResponse.getStatus().getCode() == 200;
    }
}

The test results are

[INFO] Running com.example.controller.TestControllerTest
11:01:52.804 [main] INFO  i.m.context.env.DefaultEnvironment - Established active environments: [test]
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 7.232 s <<< FAILURE! - in com.example.controller.TestControllerTest
[ERROR] com.example.controller.TestControllerTest  Time elapsed: 7.229 s  <<< ERROR!
java.lang.RuntimeException: I was initialized

How do I prevent the test from starting that SingletonService? Stubbing is an option but keep in mind that this is a simple demo illustrating the issue for a bigger project. Is there no other straightforward way? According to the docs of @MicronautTest, it shouldn't be scanning the classpath but it clearly is?

Here are some other configurations I've already attempted without any success:

Adding a ApplicationBuilder that disables eagerInit

@MicronautTest(application = TestControllerTest.TestApplication.class)
class TestControllerTest {
    public static class TestApplication {
        public static void main(String[] args) {
            Micronaut.build(args)
                    .eagerInitAnnotated(null)
                    .eagerInitSingletons(false)
                    .mainClass(TestApplication.class);
        }
    }
    @Inject
    @Client("/")
    RxHttpClient rxHttpClient;

    @Test
    void controller() {
        HttpResponse<ByteBuffer> byteBufferHttpResponse = rxHttpClient.exchange("/").blockingFirst();
        assert byteBufferHttpResponse.getStatus().getCode() == 200;
    }
}

Passing along a contextbuilder

@MicronautTest(contextBuilder = TestControllerTest.TestContextBuilder.class)
class TestControllerTest {

    public static class TestContextBuilder extends DefaultApplicationContextBuilder {
        public TestContextBuilder() {
            eagerInitSingletons(false);
            eagerInitAnnotated(null);
            eagerInitConfiguration(false);
        }
    }

    @Inject
    @Client("/")
    RxHttpClient rxHttpClient;

    @Test
    void controller() {
        HttpResponse<ByteBuffer> byteBufferHttpResponse = rxHttpClient.exchange("/").blockingFirst();
        assert byteBufferHttpResponse.getStatus().getCode() == 200;
    }
}

Which all yield the same response.

Hopefully someone knows how to limit the scope of bean instantation with the @MicronautTest or I'll most likely switch back to Spring boot

Thanks in advance


Solution

  • My question is how do I limit which controllers and singletons are loaded when testing Micronaut?

    At least there is the @Requires annotation that allows doing flexible configuration if a bean should be loaded within the current environment.

    For example, in your case it should be something like:

    import javax.inject.Singleton;
    import io.micronaut.context.annotation.Requires;
    
    @Singleton
    @Requires(notEnv = {Environment.TEST})
    public class SingletonService {
    
        @EventListener
        public void init(ApplicationStartupEvent event) {
            throw new RuntimeException("I was initialized");
        }
    }
    

    As a result, the exception from the service is not thrown.

    However, I don't really know the purpose why you need to exclude the service. And it could be that other approaches might be more convenient.

    For example, see Mocking Collaborators section from the micronaut test documentation: https://micronaut-projects.github.io/micronaut-test/latest/guide/