Issue with testing Spring MVC slice in SpringBoot 1.4
Who is interested in loading the full application should try using @SpringBootTest
combined with @AutoConfigureMockMvc
rather than the @WebMvcTest
.
I have been struggling with the problem for quite a while, but finally I got the complete picture.
The many tutorials on the internet, as well as the official Spring documentation I found so far , state that you can test your controllers using @WebMvcTest
; that's entirely correct, still omitting half of the story though.
As pointed out by the javadoc of such annotation, @WebMvcTest
is only intended to test your controllers, and won't load all your app's beans at all, and this is by design.
It is even incompatible with explicit bean scanning annotations like @Componentscan
.
I suggest anybody interested in the matter, to read the full javadoc of the annotation (which is just 30 lines long and stuffed of condensed useful info) but I'll extract a couple of gems relevant to my situation.
from Annotation Type WebMvcTest
Using this annotation will disable full auto-configuration and instead apply only configuration relevant to MVC tests (i.e.
@Controller
,@ControllerAdvice
,@JsonComponent
Filter,WebMvcConfigurer
andHandlerMethodArgumentResolver
beans but not@Component
,@Service
or@Repository
beans). [...] If you are looking to load your full application configuration and use MockMVC, you should consider@SpringBootTest
combined with@AutoConfigureMockMvc
rather than this annotation.
And actually, only @SpringBootTest
+ @AutoConfigureMockMvc
fixed my problem, all other approaches that made use of @WebMvcTest
failed to load some of the required beans.
EDIT
I take back my comment I made about Spring documentation, because I wasn't aware that a slice was implied when one uses a @WebMvcTest
; actually the MVC slice documentation put it clear that not all the app is loaded, which is by the very nature of a slice.
Custom test slice with Spring Boot 1.4
Test slicing is about segmenting the ApplicationContext that is created for your test. Typically, if you want to test a controller using MockMvc, surely you don’t want to bother with the data layer. Instead you’d probably want to mock the service that your controller uses and validate that all the web-related interaction works as expected.
You are using @WebMvcTest
while also manually configuring a MockMvc
instance. That doesn't make sense as one of the main purposes of @WebMvcTest
is to automatically configure a MockMvc
instance for you. Furthermore, in your manual configuration you're using standaloneSetup
which means that you need to fully configure the controller that's being tested, including injecting any dependencies into it. You're not doing that which causes the NullPointerException
.
If you want to use @WebMvcTest
, and I would recommend that you do, you can remove your setUp
method entirely and have an auto-configured MockMvc
instance injected instead using an @Autowired
field.
Then, to control the ProductService
that's used by ProductController
, you can use the new @MockBean
annotation to create a mock ProductService
that will then be injected into ProductController
.
These changes leave your test class looking like this:
package guru.springframework.controllers;
import guru.springframework.services.ProductService;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@WebMvcTest(ProductController.class)
public class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductService productService;
@Test
public void testList() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/products"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.view().name("products"))
.andExpect(MockMvcResultMatchers.model().attributeExists("products"))
.andExpect(MockMvcResultMatchers.model().attribute("products",
Matchers.is(Matchers.empty())));
}
}