1

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("[email protected]");
        verify(emailSendingService).sendEmail("[email protected]");
    }
}

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.

4
  • I don't think it's possible to do what you are asking for. You need to define all mocks instances that are required for your test to run. Commented Jun 12 at 14:10
  • Works-for-me... UserService is null in InviteService, so I assume the NPE is coming from within InviteService, which presumably interacts with UserService. Guessing, either you need some expectations on UserService (which will remove the unused field warning) or there might be some concerns that need separating. Commented Jun 12 at 15:30
  • @NickHolt I don't want UserService to be set to null, I want it to be set to a default mock, i.e. the same object that is returned by Mockito.mock(UserService.class) Commented Jun 12 at 18:38
  • @Dónal yeah, kind of weird that's not the default behaviour, seems that it'd be more useful than setting it to null. Would be nice to add a new field to @injectMocks that when set to true would do what @Georgii Lvov suggests. Commented Jun 13 at 7:57

2 Answers 2

1

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("[email protected]");
        verify(emailSendingService).sendEmail("[email protected]");
    }
}

In this example, I haven’t accounted for all the possible details and pitfalls that might need to be considered, but it works.

Sign up to request clarification or add additional context in comments.

4 Comments

This looks very promising. I'll try it out tomorrow, thanks. I wonder if you could ensure MockitoExtension always runs before AutoMockExtension by annotating the latter with @ExtendWith(MockitoExtension.class)
I updated the solution so that it handles fields in parent classes
By the way, the order of extension registrations is described in the javadoc for the @ExtendWith annotation, in the Registration Order section
Also, note that MockitoExtension does more than just initialize @Mock and @InjectMocks fields. Therefore, I recommend using custom extensions together with MockitoExtension
-2

You could achieve this with constructor injection.

In your service, make sure the dependencies are injected using the constructor:

private final UserService userService;
private final EmailSendingService emailService;

public InviteService(EmailSendingService emailService, UserService service) {
    this.emailService = emailService; 
    this.service = service;
}

In your test class you can call your constructor using the mocked services:

@Mock
private EmailSendingService emailSendingService;

@Mock
private UserService userService;

private InviteService inviteService = new InviteService(emailSendingService, userService);

3 Comments

userService still needs to be declared, though - I think your answer misses the point?
I thought the point is IntelliJ marks the field as unused. Instead of configuring to ignore any used fields annotated with Mock, I thought this might work.
@Hansvandenpol IntelliJ marking the field as unused is not the point. I can fix this just by telling IntelliJ not to mark any fields annotated with @Mock as unused

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.