In my Spring Boot app, I have some tests that look like this
@ExtendWith(MockitoExtension.class)
class InviteServiceTests {
@Mock
private UserService userService;
@Mock
private EmailSendingService emailSendingService;
@InjectMocks
private InviteService inviteService;
@Test
void testSomething() {
inviteService.inviteUser("user@example.com");
verify(emailSendingService).sendEmail("user@example.com");
}
}
I have to declare userService
because it's a dependency of inviteService
that is called during inviteService.inviteUser
. However I don't need to mock or verify any userService
methods, so IntelliJ marks this as an unused field.
I'm aware I could tell IntelliJ to ignore any used fields annotated with @Mock
, but what I'd prefer is to not have to declare this field at all. In other words, when a dependency of InviteService
is found that does not have a corresponding @Mock
field, Mockito will automatically create a default mock for the dependency.
Currently, if the userService
field is removed, the dependency is set to null, which causes a NullPointerException
when the test runs.
To achieve this behavior, it seems that there is no out-of-the-box solution in Mockito. However, you can write your own custom extension that inspects your test class, finds a field annotated with @InjectMocks
, examines its fields, and if any of them are null, instantiates them with mock objects. It could look something like this:
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.mockito.InjectMocks;
import org.mockito.Mockito;
import org.mockito.exceptions.misusing.InjectMocksException;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Modifier;
import java.util.Arrays;
public class AutoMockitoExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) throws Exception {
var testInstance = context.getRequiredTestInstance();
var injectMocksField = Arrays.stream(testInstance.getClass().getDeclaredFields())
.filter(field -> field.isAnnotationPresent(InjectMocks.class))
.findFirst()
.orElseThrow(() -> new IllegalStateException("@InjectMocks field not found"));
injectMocksField.setAccessible(true);
var injectMocksObject = injectMocksField.get(testInstance);
if (injectMocksObject == null) {
throw new IllegalStateException("""
@InjectMocks field is null.
You may need to add MockitoExtension.class to the beginning of the @ExtendWith extensions list""");
}
// Assign a mock object to any null instance field of the @InjectMocks object
ReflectionUtils.doWithFields(injectMocksObject.getClass(),
field -> field.set(injectMocksObject, Mockito.mock(field.getType())),
field -> {
var isInstanceField = !Modifier.isStatic(field.getModifiers());
field.setAccessible(true);
try {
return isInstanceField && field.get(injectMocksObject) == null;
} catch (IllegalAccessException e) {
throw new InjectMocksException("Failed to access field: " + field, e);
}
});
}
}
And then test:
@ExtendWith({MockitoExtension.class, AutoMockitoExtension.class})
class InviteServiceTests {
@Mock
private EmailSendingService emailSendingService;
@InjectMocks
private InviteService inviteService;
@Test
void testSomething() {
inviteService.inviteUser("user@example.com");
verify(emailSendingService).sendEmail("user@example.com");
}
}
In this example, I haven’t accounted for all the possible details and pitfalls that might need to be considered, but it works.