I am testing ControllerAdvice class where i have EncodeUtil is autowired, this is always resolved as Null when I run using JUnit test, no issue when run via spring boot application, what am i missing here?
@ControllerAdvice
public class Base64EncodedResponseBodyAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private EncodeUtil encodeUtil;
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request,
ServerHttpResponse response) {
if(returnType.getMethod().getName().equalsIgnoreCase("doPNMImage"))
{
encodeUtil.encodeBase64(body);
response.getHeaders().add("ENCODE_TYPE", "Base64");
}
else return body;
}
}
Here is my Junit Class, i have
@ExtendWith(SpringExtension.class)
@WebMvcTest(PNMImageController.class)
@AutoConfigureMockMvc
public class Base64EncodedResponseBodyAdviceTest {
@Mock
private EncodeUtil encodeUtil;
@InjectMocks
PNMImageController pnmImageController;
@BeforeEach
void init(){
MockitoAnnotations.initMocks(pnmImageController);
mockMvc = MockMvcBuilders.standaloneSetup(pnmImageController)
.setControllerAdvice(Base64EncodedResponseBodyAdvice.class)
.build();
}
@Test
public void getAuthorizeTest() throws Exception {
ReflectionTestUtils.setField(encodeUtil, "salt", SALT);
Mockito.when(this.encodeUtil.encodeBase64()).thenReturn(TEST_BASE64_STR);
mockMvc.perform(post("http://localhost:8080/image/doPNMImage"))
.andExpect(status().isOk())
.andExpect(content().string(TEST_BASE64_STR))
.andExpect(header().string("ENCODE_TYPE", "Base64"));
}
}
The test fails with Nullpointer exception
Request processing failed; nested exception is java.lang.NullPointerException: Cannot invoke "com.ylm.cnpl.core.util.EncodeUtil.encodeBase64(String)" because "this.encodeUtil" is null
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.NullPointerException: Cannot invoke "com.ylm.cnpl.core.util.encodeBase64(String)" because "this.encodeUtil" is null
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
CodePudding user response:
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).
If we want EncodeUtil
as a bean in our tests: Import(configuration), configure/component-scan it.
easiest: use
@SpringBootTest
instead of@WebMvcTest
.to keep the test context "slim", we should:
Dedicate a (main) configuration for the affected bean(s) & import them into test.
Or/and dedicate a "test" configuration, easiest with:
@ExtendWith(SpringExtension.class) @WebMvcTest(PNMImageController.class) @AutoConfigureMockMvc public class Base64EncodedResponseBodyAdviceTest { @Configuration static class CustomTestConfig { @Bean EncodeUtil ... } ... }
If we want @InjectMocks
to work, we should (try) @MockBean
instead of @Mock
... Difference between @Mock, @MockBean and Mockito.mock() ... Difference between @Mock and @InjectMocks ...
In spring-test, I would use: (org.springfr...)@MockBean
and @Autowired
!;)
CodePudding user response:
Please add @SpringBootTest more: https://docs.spring.io/spring-boot/docs/2.1.5.RELEASE/reference/html/boot-features-testing.html#boot-features-testing-spring-boot-applications and you need specify which one configuration to load @ContextConfiguration (https://docs.spring.io/spring-boot/docs/1.4.0.M3/reference/htmlsingle/#boot-features-testing-spring-boot-applications-detecting-config)
CodePudding user response:
As you are trying to test one controller and controllerAdvice, it is a correct choice to use a @WebMvcTest(PNMImageController.class)
With @WebMvcTest, you rely on Spring to create the desired controller. You attempted to create a second one with @InjectMocks, but the one that Spring tries to build cannot be constructed.
See @WebMvcTest javadoc
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).
You need to provide the EncodeUtil
to Spring.
As you intend to use it a mock, a correct annotation to use is @MockBean
Secondly, you create an instance of mockMvc. There is no need to do it - you explicitly wrote @AutoConfigureMockMvc, so you can autowire it into your test. In fact @WebMvcTest is annotated with @AutoConfigureMockMvc, so you don't need it in your test. Same thing for @ExtendWith(SpringExtension.class).
Updated code
@WebMvcTest(PNMImageController.class)
public class Base64EncodedResponseBodyAdviceTest {
@MockBean
private EncodeUtil encodeUtil;
@Autowired
MockMvc mockMvc;
@Test
public void getAuthorizeTest() throws Exception {
// ReflectionTestUtils.setField(encodeUtil, "salt", SALT);
Mockito.when(this.encodeUtil.encodeBase64()).thenReturn(TEST_BASE64_STR);
mockMvc.perform(post("http://localhost:8080/image/doPNMImage"))
.andExpect(status().isOk())
.andExpect(content().string(TEST_BASE64_STR))
.andExpect(header().string("ENCODE_TYPE", "Base64"));
}
}
On top of that:
- setting salt field on encodeUtil looks suspicious. You shouldn't need to set fields on a mock.
IMHO changing your test to @SpringBootTest
is not the way to go:
SpringBootTest
is intended for integration testing, it will spin up your entire application. Of course it will get the job done - your controller will be spinned up as well, but you should strive to keep your tests as lean as possible - spinning only the desired controller will make your test faster and less brittle.