javajunitmockito

Simplify mocking dependencies with @InjectMocks


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.


Solution

  • 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.MockitoAnnotations;
    import org.mockito.exceptions.misusing.InjectMocksException;
    
    import org.springframework.util.ReflectionUtils;
    
    import java.lang.reflect.Field;
    import java.lang.reflect.Modifier;
    import java.util.Arrays;
    
    public class AutoMockExtension implements BeforeEachCallback {
    
        @Override
        public void beforeEach(ExtensionContext context) throws Exception {
            var testInstance = context.getRequiredTestInstance();
    
            /*
            Initialize any @Mock field of the test instance to a mock object. This effectively does this same job as
            @ExtendWith(MockitoExtension.class), which is why it is unnecessary to use MockitoExtension and
            AutoMockitoExtension together
             */
            MockitoAnnotations.openMocks(testInstance);
    
            Field 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);
    
            // 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(AutoMockExtension.class)
    // or @ExtendWith({MockitoExtension.class, AutoMockExtension.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.