javaspringspring-bootmockitojunit5

@MockitoBean is null while testing spring boot rest controller


I am trying to test Spring Boot's Rest controller.

Problem:

I am using @MockitoBean annotation for mocking the ContactService.java class. The Rest controller depends on this ContactService.java class.

Following the docs on setting up MockMvc, I have the following code:

public class ContactRestControllerTest {

    private MockMvc mockMvc;

    @MockitoBean
    private ContactService contactService;

    @BeforeEach
    void setup() {
        MockitoAnnotations.openMocks(this);
        this.objectMapper = new ObjectMapper();
        this.mockMvc = MockMvcBuilders
                .standaloneSetup(new ContactRestController(contactService))
                .build();
    }

    @Test
    void getMessagesByStatus() throws Exception {
        final String STATUS = "Open";

        String messageListStr = "[ { ... } ]";

        Page<List<ContactMessage>> pagedContactMessageList = // omitted for brevity...
        
        Mockito.when(
                this.contactService.getContactMessages(Mockito.eq(STATUS), Mockito.anyMap())
        ).thenReturn(pagedContactMessageList);

        mockMvc.perform(...); // omitted for brevity
    }
}

The above code throws the NullPointerException:

java.lang.NullPointerException: Cannot invoke "...ContactService.getContactMessages(String, java.util.Map)" because "this.contactService" is null

    at ....ContactRestControllerTest.getMessagesByStatus(ContactRestControllerTest.java:151)
    at java.base/java.lang.reflect.Method.invoke(Method.java:580)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

Question:

Why is contactService null even through it is annotated with @MockitoBean and MockitoAnnotations.openMocks(this); is also called?

If I use the @WebMvcTest(ContactRestController.class) annotation over the test class, the test runs successfully. In this case, I don't even need to call MockitoAnnotations.openMocks(this). Everything just works fine.

If this is the correct way, then why do the docs on setting up MockMvc show a different way and what needs to be done to make that work? What am I missing here?

Spring boot version: 3.4.3


Solution

  • You are mixing tutorials/documentation that you shouldn't be mixing.

    TL:DR

    If this is the correct way, then why do the docs on setting up MockMvc show a different way and what needs to be done to make that work? What am I missing here?

    The documentation you link to for setting up MockMVC is for plain Spring not for Spring Boot. The @WebMvcTest and Spring Boot testing in general adds additional support (like automatic mocking and setting up MockMvc for you).

    Longer Explanation:

    The @MockitoBean annotation is from Spring and not from Mockito, adding MockitoAnnotations.openMocks(this); or @ExtendsWith(MockitoExtension.class) to your test will not create a mock for that annotation. If you want to you should replace the @MockitoBean with @Mock.

    public class ContactRestControllerTest {
    
        private MockMvc mockMvc;
    
        @Mock
        private ContactService contactService;
    
        @BeforeEach
        void setup() {
            MockitoAnnotations.openMocks(this);
            this.objectMapper = new ObjectMapper();
            this.mockMvc = MockMvcBuilders
                    .standaloneSetup(new ContactRestController(contactService))
                    .build();
        }
    

    Another option is, as you apparently want to use Spring, is to add @ExtendsWith(SpringExtension.class) to your test to make your test identified by the Spring Test Context framework and it will handle the @MockitoBean annotation and create a mock.

    @ExtendsWith(SpringExtension.class)
    public class ContactRestControllerTest {
    
        private MockMvc mockMvc;
    
        @MockitoBean
        private ContactService contactService;
    
        @BeforeEach
        void setup() {
            this.objectMapper = new ObjectMapper();
            this.mockMvc = MockMvcBuilders
                    .standaloneSetup(new ContactRestController(contactService))
                    .build();
        }
    

    However as you are using Spring Boot what you really should do is use @WebMvcTest and not do manual setup at all. You let Spring Boot handle the creation of the controller and Mock MVC setup, which yuou can now autowire and it will create a mock for you.

    @WebMvcTest(ContactRestController.class)
    public class ContactRestControllerTest {
    
        @Autowired
        private MockMvc mockMvc;
    
        @MockitoBean
        private ContactService contactService;
    
        @BeforeEach
        void setup() {
            this.objectMapper = new ObjectMapper();
        }
    
        @Test
        void getMessagesByStatus() throws Exception {
            final String STATUS = "Open";
    
            String messageListStr = "[ { ... } ]";
    
            Page<List<ContactMessage>> pagedContactMessageList = // omitted for brevity...
            
            Mockito.when(
                    this.contactService.getContactMessages(Mockito.eq(STATUS), Mockito.anyMap())
            ).thenReturn(pagedContactMessageList);
    
            mockMvc.perform(...); // omitted for brevity
        }
    }