2

Here Iam testing my endpoint using WebMvcTest , MockMvc and mocking service using @MockBean. Without using the standaloneSetup method , the below code runs fine.

public class MessageControllerTest {

    @Nested
    @WebMvcTest
    class TestUsingMockServer {

        @MockBean
        MessageServiceImpl messageService;

        @Autowired
        MockMvc mockMvc;

        @Test
        public void test_to_return_id_with_message_json() throws Exception {

            when(messageService.findById(anyLong())).thenAnswer(invocation -> new Message("Hello World", (Long) invocation.getArguments()[0], LocalDateTime.now()));

            mockMvc.perform(get("/resources/messages/{id}", 3)
                                    .contextPath("/resources")
                                    .accept(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andExpect(result -> {
                        result.toString().contains("3");
                        result.toString().contains("Hello World");
                    });

        }


        @Test
        public void test_to_get_the_name_passed() throws Exception {

            when(messageService.getMessageByIdName(anyLong(), anyString())).thenAnswer(invocation -> new Message("Hello " + invocation.getArguments()[1],
                    (Long) invocation.getArguments()[0], LocalDateTime.now()));


            mockMvc.perform(get("/resources/messages/{id}", 3)
                                    .queryParam("name", "kaustubh")
                                    .contextPath("/resources")
                                    .accept(MediaType.APPLICATION_JSON))
                    .andDo(print())
                    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk())
                    .andExpect(result -> {
                        result.toString().contains("3");
                        result.toString().contains("kaustubh");
                    });

        }

    }
}

TO avoid repetition when I added standaloneSetup method , and ran the tests I get error which says MessageServiceImpl bean is not initialized (because of NullPointerException )

    public class MessageControllerTest {

        @Nested
        @WebMvcTest
        class TestUsingMockServer {

            @MockBean
            MessageServiceImpl messageService;

            @Autowired
            MockMvc mockMvc;

            @BeforeEach
            public void setUp(){
                mockMvc = standaloneSetup(new MessageController())
                                  .defaultRequest(get("/")
                                                          .contextPath("/resources")
                                                          .accept(MediaType.APPLICATION_JSON)
                                  ).build();
            }

            @Test
            public void test_to_return_id_with_message_json() throws Exception {

                when(messageService.findById(anyLong())).thenAnswer(invocation -> new Message("Hello World", (Long) invocation.getArguments()[0], LocalDateTime.now()));

                mockMvc.perform(get("/resources/messages/{id}",3))
                        .andDo(print())
                        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                        .andExpect(status().isOk())
                        .andExpect(result -> {
                            result.toString().contains("3");
                            result.toString().contains("Hello World");
                        });

            }
}
}

Giving the following error

Error showing NullPointerException in MessageController

Line 17 as menitoned , in the error , calls to MessageServiceImpl

@RestController
@RequestMapping("/messages")
public class MessageController {

    @Autowired
    MessageServiceImpl messageService;

    @GetMapping(path = "/{id}")
    public Message getMessageById(@PathVariable Long id) {
        return messageService.findById(id);  // LINE 17
    }

    @GetMapping(path = "/{id}", params = "name")
    public Message getMessageByIdName(@PathVariable Long id, @RequestParam(value = "name", defaultValue = "ST") String name) {
        return messageService.getMessageByIdName(id, name);
    }

}

Is happening because the MockMvc builder is setup before the service bean created?

Kaustubh
  • 117
  • 2
  • 14
  • It will reload the configuration for each test. So it seems like the mock configuration is gone after one test is ran. Try to remock it below the @BeforeEach annotation where you initialize the mockMVC – Pajala Apr 15 '20 at 09:34
  • MOckbean nor WebMvcTest work with manually setting up MockMVC. So it won't work. But which repetition ? If it is the request, you can prepare that inside an @Before method and reuse in your methods. – M. Deinum Apr 15 '20 at 10:19
  • For avoiding that request building in all the methods I am using the standaloneSetup method – Kaustubh Apr 15 '20 at 10:29
  • Again you can build it **once** in the setup and reuse that in your methods. You are making things overly complex and start to work around the framework instead of working with the framework. – M. Deinum Apr 15 '20 at 15:39

1 Answers1

1

There are a couple of things that you have to know about your setup of your tests.

In your first example:

@WebMvcTest
class TestUsingMockServer {

    @MockBean
    MessageServiceImpl messageService;

    @Autowired
    MockMvc mockMvc;

Here the test context is started by @WebMvcTest and so MockMvc can be autowired. @WebMvcTest will also look at @Mockbean to see what needs to be mocked.

In our second example:

    @WebMvcTest
    class TestUsingMockServer {

        @MockBean
        MessageServiceImpl messageService;

        @Autowired
        MockMvc mockMvc;

        @BeforeEach
        public void setUp(){
            mockMvc = standaloneSetup(new MessageController())
                              .defaultRequest(get("/")
                                                      .contextPath("/resources")
                                                      .accept(MediaType.APPLICATION_JSON)
                              ).build();
        }

Here your are overriding your @Autowired mockMvc object with another instance. And here your will setup the context,

        mockMvc = standaloneSetup(new MessageController())

This setup will skip any Autowiring, so in your MessageController class,

@Autowired
MessageServiceImpl messageService;

This will be skipped. and will be null.

So for a solution. An easy fix will be to do your autowiring by constructor:

@Autowired
public MessageController(MessageServiceImpl messageService) {
 this.messageService = messageService;
}

Now your can just add your mock in your test class:

mockMvc = standaloneSetup(new MessageController(messageService))

This will fix your problem.

Other advise that i want to give is that your shouldn't need to worry to much about repetition your tests. They will run very fast once the context is loaded in.

AliHA
  • 171
  • 4
  • But is it a good practice to have a constructor of a controller ? Also then what's the use of autowire? About repetition , I've recently started to stick to the DRY principle , so. – Kaustubh Apr 15 '20 at 10:35
  • Look at this for autowiring properties vs constructor: https://stackoverflow.com/questions/40620000/spring-autowire-on-properties-vs-constructor – AliHA Apr 15 '20 at 10:51
  • As for the DRY principle, what your did was but two different tests (they both test two different methods) into one test. That's not DRY since your are refactoring to much. What you can do is refactor the mvc.perform(...) part. In your example you can, because your can have the same response for both tests with different input. But still i'm not a big fan since you are obfuscating a bit and sometimes writing clear code is better that perfect 'good' code. – AliHA Apr 15 '20 at 10:59
  • Can I not inject the controller am testing , instead of initializing an object of it using a constructor? – Kaustubh Apr 20 '20 at 10:19
  • Yes, you can also do that. So you can just @Autowired your MessageController and that use that one in your standaloneSetup(***). – AliHA Apr 22 '20 at 15:25
  • So,autowiring controller in controller test is recommended? – Kaustubh Apr 30 '20 at 04:54
  • yes, this question discusses this: https://stackoverflow.com/questions/40620000/spring-autowire-on-properties-vs-constructor – AliHA Apr 30 '20 at 11:19