Overriding a dependency in a Micronaut test
Injecting mock bean with @MockBean
annotation works if your dependency in ClassUnderTest
is represented by interface. Let's say Dependency
is a simple interface like:
package di.failure.example;
public interface Dependency {
void run();
}
Your application may provide an implementation for this interface called DependencyImpl
:
package di.failure.example;
import javax.inject.Singleton;
@Singleton
public class DependencyImpl implements Dependency {
@Override
public void run() {
throw new RuntimeException("I don't want this to load!");
}
}
Now, for test purpose you can define a mock that replaces DependencyImpl
:
package di.failure.example;
import io.micronaut.test.annotation.MicronautTest;
import io.micronaut.test.annotation.MockBean;
import org.junit.jupiter.api.Test;
import javax.inject.Inject;
import static org.mockito.Mockito.mock;
@MicronautTest
public class ClassUnderTestTest {
@Inject
ClassUnderTest classUnderTest;
@Test
public void test() {
classUnderTest.run();
}
@MockBean(DependencyImpl.class)
public Dependency dependency() {
return mock(Dependency.class);
}
}
This test executes and the mock returned by dependency()
method is used in place of DependencyImpl
.
Using @Replaces
annotation
As Sergio mentioned in the comments section you can replace class based bean dependency using @Replaces
annotation. Consider following example:
package di.failure.example;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.test.annotation.MicronautTest;
import org.junit.jupiter.api.Test;
import javax.inject.Inject;
import javax.inject.Singleton;
@MicronautTest
public class ClassUnderTestTest {
@Inject
ClassUnderTest classUnderTest;
@Test
public void test() {
classUnderTest.run();
}
@Replaces(Dependency.class)
@Singleton
public static class MockDependency extends Dependency {
public MockDependency() {
System.out.println("MockDependency.<init>");
}
@Override
void run() {
System.out.println("Does not throw any exception...");
}
}
}
In this example we have defined a class MockDependency
and we instruct Micronaut's DI mechanism to replace Dependency
bean with MockDependency
. However, there is one important thing we need to remember about - because our MockDependency
extends Dependency
class, parent construct gets invoked. The example you have shown in the question won't work in this case, because Dependency.<init>
throws RuntimeException
and the test fails. In this modified example I have used class like this one:
package di.failure.example;
import javax.inject.Singleton;
@Singleton
public class Dependency {
public Dependency() {
System.out.println("Dependency.<init>");
}
void run() {
throw new RuntimeException("I don't want this to load!");
}
}
When I run the test it passes and I see following console output:
Dependency.<init>
MockDependency.<init>
Does not throw any exception...
The main difference comparing to @MockBean
is that in case of @Replaces
you are using a concrete class object. As a workaround (if we really need a Mockito mock object) is to create a mock internally and delegate calls to this object, something like this:
@Replaces(Dependency.class)
@Singleton
public class MockDependency extends Dependency {
private final Dependency delegate;
public MockDependency() {
this.delegate = mock(Dependency.class);
}
@Override
void run() {
delegate.run();
}
}
Malcolm Crum
Updated on June 14, 2022Comments
-
Malcolm Crum almost 2 years
I'm testing a Micronaut class that has a bean injected into it. In my test I provide a
@MockBean
class to override it. However, it seems Micronaut still injects the real dependency.@MicronautTest public class ClassUnderTestTest { @Inject ClassUnderTest classUnderTest; @Test public void test() { } @MockBean Dependency dependency() { return mock(Dependency.class); } }
I uploaded a minimum repro to Github: https://github.com/crummy/micronaut-test-dependencies . The real dependency throws an exception, and the test does too. I would not have expected this to happen because of my
@MockBean
.If I change the annotation to be
@MockBean(Dependency.class)
then I get this error:Message: No bean of type [di.failure.example.Dependency] exists
. This seems even more confusing to me - now it doesn't resolve my real or my mock dependency? -
Malcolm Crum over 5 yearsOK, that's a shame (I prefer to avoid interfaces unless I have a practical need for them.) But I guess this is one.
-
Sergio del Amo over 5 yearsIf you need to replace a class you can always create a bean in the test classpath and use @Replaces
-
Szymon Stepniak over 5 yearsThanks @SergiodelAmo for the valuable input! I have updated the answer with
@Replaces
annotation use case. -
Abhijit Sarkar about 5 yearsWhat is I want to override a property value, not a whole bean? Typical examples are in-memory databases.
-
Jeff Scott Brown almost 3 years"What is I want to override a property value, not a whole bean?" - you can do that with environment specific config files like
src/main/resources/application-test.yml
. Tests marked with@MicronautTest
can also provide their own config values.