Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Issue with testing Spring MVC slice in SpringBoot 1.4

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.

like image 401
user2693135 Avatar asked Jun 28 '16 19:06

user2693135


People also ask

What is test slice?

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.

What does SpringBootTest annotation do?

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 .

What does @RunWith SpringRunner class mean?

@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.


2 Answers

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 and HandlerMethodArgumentResolver 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.

like image 196
Antonio Avatar answered Sep 20 '22 17:09

Antonio


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())));

    }
}
like image 32
Andy Wilkinson Avatar answered Sep 21 '22 17:09

Andy Wilkinson