简体   繁体   中英

Mockito Injecting Null values into a Spring bean when using @Mock?

As I am new to Spring Test MVC I don't understand this problem. I took the code below from http://markchensblog.blogspot.in/search/label/Spring

Variable mockproductService is not injected from Application Context and it contains null values while using @Mock annotation and getting assetion error.

The Assertion error I currently encounter is as follows:

java.lang.AssertionError: Model attribute 'Products' expected:<[com.pointel.spring.test.Product@e1b42, com.pointel.spring.test.Product@e1f03]> but was:<[]>
    at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:60)
    at org.springframework.test.util.AssertionErrors.assertEquals(AssertionErrors.java:89)
    at org.springframework.test.web.servlet.result.ModelResultMatchers$2.match(ModelResultMatchers.java:68)
    at org.springframework.test.web.servlet.MockMvc$1.andExpect(MockMvc.java:141)
    at com.pointel.spring.test.ProductControllerTest.testMethod(ProductControllerTest.java:84)

Note: If I use @Autowired instead of @Mock it is working fine.

Test Controller class

RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations={"classpath:mvc-dispatcher-servlet.xml"})
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class})
public class ProductControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

   @InjectMocks
    private ProductController productController;

    @Mock
    //@Autowired
    private ProductService mockproductService;


    @Before
    public void setup() {

    MockitoAnnotations.initMocks(this);

    List<Product> products = new ArrayList<Product>();
    Product product1 = new Product();
    product1.setId(new Long(1));

    Product product2 = new Product();
    product2.setId(new Long(2));

    products.add(product1);
    products.add(product2);

    Mockito.when(mockproductService.findAllProducts()).thenReturn(products);

    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();

    }

    @Test
    public void testMethod() throws Exception {

    List<Product> products = new ArrayList<Product>();

    Product product1 = new Product();
    product1.setId(new Long(1));

    Product product2 = new Product();
    product2.setId(new Long(2));

    products.add(product1);
    products.add(product2);

    RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/products");

    this.mockMvc.perform(requestBuilder).
        andExpect(MockMvcResultMatchers.status().isOk())
        .andExpect(MockMvcResultMatchers.model().attribute("Products", products))
           //.andExpect(MockMvcResultMatchers.model().size(2))
        .andExpect(MockMvcResultMatchers.view().name("show_products"));


    }
}

Controller class

@Controller
public class ProductController {

    @Autowired
    private ProductService productService;

    @RequestMapping("/products")
    public String testController(ModelMap model){
        model.addAttribute("Products",productService.findAllProducts());
        return "show_products";
    }
}

WebServletContext mvc-dispatcher-servlet.xml

<bean id="someDependencyMock" class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="com.pointel.spring.test.ProductService" />
</bean>
    <context:component-scan base-package="com.pointel.spring.test" />

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver" >     
    <property name="prefix" value="/WEB-INF/jsp/" />
    <property name="suffix" value=".jsp" />
</bean>

For me it's unclear how the combination of Spring and Mockito as you took it from the referenced blog source should work at all as expected. At least I may explain your observation:

  • Your test ( this.mockMvc.perform() ) is working on the web application context created by Spring. In that context ProductController was instantiated by Spring ( context:component-scan ). The productService was then autowired with the Mockito mock you created in mvc-dispatcher-servlet.xml as someDependencyMock .
  • If you inject the mockproductService via @Autowired , Spring injects the someDependencyMock instance from its context. So your Mockito.when() call works correctly on this instance, which was already correctly wired to the ProductController as mentioned before.
  • But if you inject the mockproductService via @Mock , Mockito injects a new instance of ProductService , not the one of the Spring context, since it knows nothing about Spring at all. So your Mockito.when() call does not operate on the mock which was autowired by Spring and thus someDependencyMock stays uninitialized.

So what's left unclear for me about how the original code from the blog worked at all is:

  • The productController property annotated with @InjectMocks will be initialized by Mockito and even correctly wired to the mockproductService in the test class. But Spring does not know anything about that object and won't use it in this.mockMvc.perform() calls. So I assume if you inject mockproductService only with @Autowired your test works as intended even if you delete both the productController property and the MockitoAnnotations.initMocks() call in your test class.

I think there's a problem with the answer that @Cebence provided in that it doesnt take into account the OP's usage of spring-webmvc-test @WebApplication. If you were you run the example provided with

@RunWith(MockitoJUnitRunner.class)

and you still have your

 @Autowired private WebApplicationContext wac;

Then the test will fail. I was experiencing the same problem as @Human Being and I found an easy solution was to set the service within the controller as not required. Not ideal but here's the solution:

The controller:

@Controller
public class MyController
{
    @Autowired(required=false)
    MyService myService;
    .
    .
    .
}

The Test:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:/META-INF/spring/applicationContext-test.xml")
@WebAppConfiguration
public class MyControllerTest
{
    // This is the backend service we are going to mock
    @Mock
    MyService myService;

    // This is the component we are testing and we inject our mocked
    // objects into it
    @InjectMocks
    @Resource
    private MyController myController;

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;


    @Before
    public void setup()
    {
        MockitoAnnotations.initMocks(this);
        this.mockMvc = webAppContextSetup(this.wac).build();

        List<Object> data = new ArrayList<Object>();

        // Mock one of the service mthods
        when(myService.getAll()).thenReturn(datasets);   
    }

    @Test
    public void testQuery() throws Exception
    {
        this.mockMvc.perform(get("/data").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(content().contentType("application/json;charset=UTF-8"))
                .andExpect(jsonPath("$.value").value("Hello"));
    }

}

and application context:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:neo4j="http://www.springframework.org/schema/data/neo4j"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/data/neo4j
http://www.springframework.org/schema/data/neo4j/spring-neo4j.xsd
http://www.springframework.org/schema/mvc 
http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd 
">



    <mvc:annotation-driven/>
    <context:annotation-config/>
    <context:component-scan base-package="com.me.controller" /> 

</beans>

I haven't looked at the tutorial you mentioned because the code you provided says enough on expertize, or lack of it, of the original author.

General rule of testing is that you don't mix different types of tests. First level of testing are unit tests which mean that you are testing a single unit of work (usually a single class). Once the unit tests pass you write integration tests that combine certain components (classes) and test how they work together.

A class rarely depends on nothing so to create a real good unit test you need to mock all its dependencies .

@RunWith(MockitoJUnitRunner.class)
public class ProductControllerTest {
    @Mock private ProductService mockproductService;
    @InjectMocks private ProductController productController;

    @Test
    public void testMethod() throws Exception {
        // Prepare sample test data.
        final Product product1 = Mockito.mock(Product.class);
        final Product product2 = Mockito.mock(Product.class);
        final ArrayList<Product> products = new ArrayList<Product>();
        products.add(product1);
        products.add(product2);
        final ModelMap mmap = Mockito.mock(ModelMap.class);

        // Configure the mocked objects.
        Mockito.when(product1.getId()).thenReturn(new Long(1));
        Mockito.when(product2.getId()).thenReturn(new Long(2));
        Mockito.when(mockproductService.findAllProducts()).thenReturn(products);
        final mmap = Mockito.mock(ModelMap.class);

        // Call the method under test.
        final String returned = productController.testController(mmap);

        // Check if everything went as planned.
        Mockito.verify(mmap).addAttribute("Products", products);
        assertNotNull(returned);
        assertEquals("show_products", returned);
    }
}

That is how a unit test should look like. First you prepare the data (objects) - notice they are all mocked. Also, using final prevents accidental assignments, ie to overwrite existing value by accident.

Second, you configure every mocked object's behavior. If a Product will be asked for ID then you specify what the mocked instance will return in that case. BTW I really don't see the purpose of setting those product IDs so the first part of the test could look like this:

        final Product product1 = Mockito.mock(Product.class);
        final Product product2 = Mockito.mock(Product.class);
        final ArrayList<Product> products = new ArrayList<Product>();
        products.add(product1);
        products.add(product2);
        final mmap = Mockito.mock(ModelMap.class);

        // Configure the mocked objects.
        Mockito.when(mockproductService.findAllProducts()).thenReturn(products);
        final mmap = Mockito.mock(ModelMap.class);

Third, call the method under test and store its result:

        final String returned = productController.testController(mmap);

And finally you check if the class under test behaved as expected. In this case ModelMap 's addAttribute() method should have been called with those exact parameter values. And returned string should not be null , and should be "show_products" - note the parameter order of assertEquals(expected, actual) method because, in case of failed test, JUnit will print out a message saying "Expected THIS but got THAT.".

        Mockito.verify(mmap).addAttribute("Products", products);
        assertNotNull(returned);
        assertEquals("show_products", returned);

Good luck testing!

PS I forgot to explain the beginning:

    @RunWith(MockitoJUnitRunner.class)
    public class ProductControllerTest {
        @Mock private ProductService mockproductService;
        @InjectMocks private ProductController productController;

In order for the @InjectMocks to work like Spring's @Autowired the test must be ran with MockitoJUnitRunner class - it will locate all @Mock members, create them and inject the right ones into the member marked with @InjectMocks .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM