I'm trying out the new Spring Boot 1.4 MVC testing features. I have the following controller.
@Controller
public class ProductController {
private ProductService productService;
@Autowired
public void setProductService(ProductService productService) {
this.productService = productService;
}
@RequestMapping(value = "/products", method = RequestMethod.GET)
public String list(Model model){
model.addAttribute("products", productService.listAllProducts());
return "products";
}
}
My minimal ProductService implementation is:
@Service
public class ProductServiceImpl implements ProductService {
private ProductRepository productRepository;
@Autowired
public void setProductRepository(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public Iterable<Product> listAllProducts() {
return productRepository.findAll();
}
}
The code of ProductRepository is:
public interface ProductRepository extends CrudRepository<Product,
Integer>{
}
I'm trying to use the new @WebMvcTest to test the conroller. My view is a thymeleaf teamplate. And my controller test is this:
@RunWith(SpringRunner.class)
@WebMvcTest(ProductController.class)
public class ProductControllerTest {
private MockMvc mockMvc;
@Before
public void setUp() {
ProductController productController= new ProductController();
mockMvc = MockMvcBuilders.standaloneSetup(productController).build();
}
@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"));
}
}
But, on running the test I get this error.
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'productController': Unsatisfied dependency expressed through method 'setProductService' parameter 0: No qualifying bean of type [guru.springframework.services.ProductService] found for dependency [guru.springframework.services.ProductService]: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type [guru.springframework.services.ProductService] found for dependency [guru.springframework.services.ProductService]: expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {}
I need help to resolve the issue to properly test ProductController. Suggestions for additional andExpect() for more thorough testing of the controller will be highly appreciated.
Thanks in advance.
Test Slices is a solution for the slowly running tests. Most of the unit tests don't require complete application bootstrap but rather some slices (layers of the application): Most MVC layers. Database / repositories. Whole application.
Spring Boot provides a @SpringBootTest annotation, which can be used as an alternative to the standard spring-test @ContextConfiguration annotation when you need Spring Boot features. The annotation works by creating the ApplicationContext used in your tests through SpringApplication .
@RunWith(SpringRunner. class) tells JUnit to run using Spring's testing support. SpringRunner is the new name for SpringJUnit4ClassRunner , it's just a bit easier on the eye. @SpringBootTest is saying “bootstrap with Spring Boot's support” (e.g. load application.
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.
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())));
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With