Search code examples
junitmockitospringmockito

Getting null pointer exception while using Mockito to mock an interface


I am getting null pointer exception while I am trying to mock

@WebMvcTest(IMnJobManager.class)
public class CMnJobManagerTest {

    @Autowired
    private MockMvc mockmvc;

    @Test
    public void testExample()throws Exception{
        IMnAllWorkFlows allWorkFlows = Mockito.mock(IMnAllWorkFlows.class);

        Mockito.doAnswer(
                invocation -> {
                    return Arrays.asList( "modn-ops");
                }).when(allWorkFlows).getAllTenants();

        mockmvc.perform(get("/v1/tenant"))
                .andExpect(status().isOk())
                .andExpect(content().string("modn-ops"))
                .andDo(print());
    }
}

I am getting following error:

java.lang.NullPointerException
        at com.test.manager.CMnJobManagerTest.testExample(CMnJobManagerTest.java:32)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:566)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
        at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
        at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
        at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
        at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
        at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
        at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
        at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:252)
        at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:141)
        at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:112)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:566)
        at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:189)
        at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:165)
        at org.apache.maven.surefire.booter.ProviderFactory.invokeProvider(ProviderFactory.java:85)
        at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:115)
        at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:75)

My interface implementation class looks like below:

@RestController
public class CMnJobManager implements IMnJobManager {
@Autowired
    public CMnJobManager(IMnAllWorkFlows allWorkFlows, IMnWorkflowService workflowService,
        IMnTemporalServiceClient temporalServiceClient, IMnWorkflowHistoryService workflowHistoryService,
        IMnSearchAttributeService searchAttributeService, IMnS3Tenant s3Tenant) {
    this.allWorkFlows = allWorkFlows;
    this.workflowService = workflowService;
    this.searchAttributeService = searchAttributeService;
    this.workflowHistoryService = workflowHistoryService;
    this.temporalServiceClient = temporalServiceClient;
    this.s3Tenant = s3Tenant;
    }
.
.
.

@Autowired
    private HttpServletRequest request;

    @Autowired
    private CMnCustomMetricService customMetricService;

}

The interface has many methods but I am trying to mock only one. The rest call, will make a call to the IMnJobManager interface and hence I have mocked it.

Error says the issue is at mockMvc.perform(...) and hence the NullPointerException.

When I remove Autowired from MockMvc, and add mockMvc = MockMvcBuilders.standaloneSetup(allWorkFlows).build();, it gives 404.

Any idea what might be wrong here?


Solution

  • There seems to be a lot of confusion what the annotations do and how to correctly use Mockito to set up mock objects/test doubles.

    To get started, I recommend to read the following posts:

    Let's look at some of the mistakes first and in the end come back to write a working test.

    @WebMvcTest(IMnJobManager.class)
    

    The value property of the @WebMvcTest annotation specifies the Controllers to load in the Spring context. IMnJobManager is not a controller, but your job manager interface. You want to pass CMnJobManager.class instead.

    @WebMvcTest only loads the web context of your application (such as the controllers). From the JavaDoc of the annotation:

    Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e. @Controller, @ControllerAdvice, @JsonComponent, Converter/GenericConverter, Filter, WebMvcConfigurer and HandlerMethodArgumentResolver beans but not @Component, @Service or @Repository beans).

    So none of your Service beans will be loaded.

    Further down in the same JavaDoc:

    Typically @WebMvcTest is used in combination with @MockBean or @Import to create any collaborators required by your @Controller beans.

    What does that mean?

    1. @WebMvcTest(IMnJobManager.class) will not create any endpoints, because IMnJobManager is not a controller and does not define any request mappings. This line explicitly tells Spring to not load your real controller. Trying to request any endpoint (with mockmvc.perform(get(…)) will result in a 404/NOT_FOUND.
    2. No services are loaded and we must (manually) create the collaborators for your controller. We'll handle this in the next section.
    @Test
    public void testExample()throws Exception{
        IMnAllWorkFlows allWorkFlows = Mockito.mock(IMnAllWorkFlows.class);
    
        doAnswer(invocation -> Arrays.asList("modn-ops"))
                .when(allWorkFlows)
                .getAllTenants();
    
        mockmvc.perform(get("/v1/tenant"))
                ...
    }
    

    This code creates a new Mockito mock object, stubs a method on it, and then … does nothing with it. The instance is local to the method and does not exist outside of it, nor is it accessible from the outside. It is not registered in the application context and it cannot be used by your controller. (Now's a good time to read the linked Stackoverflow post a second time).

    How do we make the instance available as collaborator to our controller? We do what the JavaDoc of the annotation tells us: define it as @MockBean:

    @MockBean
    private IMnAllWorkFlows allWorkFlows;
    

    And then stub the method in the setup/@BeforeEach method. In simple tests such as in the question, the stubbing could be configured in the test itself too.

    @BeforeEach
    void setUp() {
        when(allWorkFlows.getAllTenants()).thenReturn("modn-ops");
        // or: doAnswer(a -> Arrays.asList("modn-ops")).when(allWorkFlows).getAllTenants();
    }
    

    Fixing all of the above should:

    1. Load the web context
    2. Instantiate the controller
    3. Create collaborators
    4. Set up stubbings for the methods

    Here's a simple, yet full, demo project (i.e. a Minimal, Reproducible Example with all classes required to compile and execute the code) which shows that the solution outlined above does indeed work:

    DemoService.java:

    public interface DemoService {
        String get();
    }
    
    @Service
    class DemoServiceImpl implements DemoService {
        @Override
        public String get() { return Instant.now().toString(); }
    }
    

    DemoController.java:

    @RestController
    @RequiredArgsConstructor
    public class DemoController {
        private final DemoService demoService;
    
        @GetMapping("demo")
        public String demo() { return demoService.get(); }
    }
    

    WebApplicationTest.java:

    @WebMvcTest(DemoController.class) // only loads a single controller
    class WebApplicationTest {
        @Autowired private MockMvc mockMvc;
        @MockBean  private DemoService demoService; // add mock bean to context
    
        @BeforeEach
        void setUp() {
            // stub method:
            when(demoService.get()).thenReturn("stubbed value");
        }
    
        @Test
        void test() throws Exception {
            // perform GET request:
            mockMvc.perform(get("/demo"))
                    .andExpect(status().isOk())
                    .andExpect(content().string("stubbed value"))
                    .andDo(print());
        }
    }