Mockito Внедрение нулевых значений в компонент Spring при использовании @Mock?

Поскольку я новичок в Spring Test MVC, я не понимаю этой проблемы. Я взял приведенный ниже код с http://markchensblog.blogspot.in/search/label/Spring

Переменная mockproductService не вводится из контекста приложения и содержит null значений при использовании аннотации @Mock и получении ошибки актива.

Ошибка утверждения, с которой я сейчас сталкиваюсь, выглядит следующим образом:

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)

Примечание. Если я использую @Autowired вместо @Mock, все работает нормально.

Класс контроллера тестирования

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
public class ProductController {

    @Autowired
    private ProductService productService;

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

WebServletContext mvc-диспетчер-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>

person Human Being    schedule 17.04.2013    source источник
comment
См. stackoverflow.com/questions/ 2457239/   -  person Vadzim    schedule 17.10.2013


Ответы (3)


Для меня неясно, как комбинация Spring и Mockito, которую вы взяли из источника блога, на которую ссылаетесь, должна работать вообще так, как ожидалось. По крайней мере, я могу объяснить ваше наблюдение:

  • Ваш тест (this.mockMvc.perform()) работает с контекстом веб-приложения, созданным Spring. В этом контексте ProductController был создан Spring (context:component-scan). Затем productService был автоматически связан с макетом Mockito, который вы создали в mvc-dispatcher-servlet.xml как someDependencyMock.
  • Если вы внедряете mockproductService через @Autowired, Spring вставляет экземпляр someDependencyMock из своего контекста. Таким образом, ваш вызов Mockito.when() работает правильно на этом экземпляре, который уже был правильно подключен к ProductController, как упоминалось ранее.
  • Но если вы внедрите mockproductService через @Mock, Mockito вставит новый экземпляр ProductService, а не контекст Spring, поскольку он вообще ничего не знает о Spring. Таким образом, ваш вызов Mockito.when() не работает с макетом, который был автоматически подключен Spring, и поэтому someDependencyMock остается неинициализированным.

Итак, мне осталось неясным, как вообще работал исходный код из блога:

  • Свойство productController с аннотацией @InjectMocks будет инициализировано Mockito и даже правильно подключено к mockproductService в тестовом классе. Но Spring ничего не знает об этом объекте и не будет использовать его в вызовах this.mockMvc.perform(). Поэтому я предполагаю, что если вы вводите mockproductService только с @Autowired, ваш тест работает так, как предполагалось, даже если вы удалите как свойство productController, так и вызов MockitoAnnotations.initMocks() в своем тестовом классе.
person Heri    schedule 17.04.2013

Я не смотрел учебник, который вы упомянули, потому что код, который вы предоставили, достаточно говорит об опыте или его отсутствии у оригинального автора.

Общее правило тестирования заключается в том, что вы не смешиваете разные типы тестов. Первый уровень тестирования — это модульные тесты. Это означает, что вы тестируете отдельную единицу работы (обычно один класс). После прохождения модульных тестов вы пишете интеграционные тесты, которые объединяют определенные компоненты (классы) и проверяют их совместную работу.

Класс редко ни от чего не зависит, поэтому для создания действительно хорошего модульного теста необходимо макетировать все его зависимости.

@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);
    }
}

Вот как должен выглядеть юнит-тест. Сначала вы подготавливаете данные (объекты) — обратите внимание, что все они издеваются. Кроме того, использование final предотвращает случайные присвоения, то есть случайную перезапись существующего значения.

Во-вторых, вы настраиваете поведение каждого имитируемого объекта. Если для идентификатора будет запрошен Product, вы указываете, что в этом случае вернет издевательский экземпляр. Кстати, я действительно не вижу смысла устанавливать эти идентификаторы продуктов, поэтому первая часть теста может выглядеть так:

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

В-третьих, вызовите тестируемый метод и сохраните его результат:

        final String returned = productController.testController(mmap);

И, наконец, вы проверяете, вел ли тестируемый класс ожидаемое поведение. В этом случае метод addAttribute() ModelMap должен был быть вызван именно с этими точными значениями параметров. И возвращаемая строка не должна быть null, а должна быть "show_products" - обратите внимание на порядок параметров метода assertEquals(expected, actual), потому что в случае неудачного теста JUnit распечатает сообщение «Ожидал ЭТО, но получил ЭТО».

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

Удачи в тестировании!

P.S. Забыл объяснить начало:

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

Чтобы @InjectMocks работал как @Autowired Spring, тест должен быть запущен с классом MockitoJUnitRunner — он найдет все @Mock элементы, создаст их и внедрит нужные в член, отмеченный @InjectMocks.

person Cebence    schedule 17.04.2013

Я думаю, что есть проблема с ответом, который предоставил @Cebence, в том, что он не учитывает использование OP spring-webmvc-test @WebApplication. Если бы вы запустили пример, предоставленный с

@RunWith(MockitoJUnitRunner.class)

а у тебя еще есть

 @Autowired private WebApplicationContext wac;

Тогда тест провалится. У меня была та же проблема, что и у @Human Being, и я нашел простое решение — настроить службу в контроллере как не требуемую. Не идеально, но вот решение:

Контроллер:

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

Тест:

@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"));
    }

}

и контекст приложения:

<?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>
person Sparm    schedule 24.01.2014
comment
MockitoAnnotations.initMocks(this); — это ключ для вызова @InjectMocks. - person Arpit Aggarwal; 19.06.2016
comment
Ага. По сути, любые поля, отмеченные @Mock или @InjectMocks, не будут инициализированы, если вы не вызовете это. Всегда лучше делать это в @Before аннотированном методе - person Sparm; 13.07.2016