Инъекция Mockito издевается в бобе Spring

Я хотел бы внедрить фиктивный объект Mockito в bean-компонент Spring (3+) для модульного тестирования с помощью JUnit. Мои зависимости бина в настоящее время вводятся с помощью @Autowired аннотация на полях закрытых членов.

Я рассмотрел использование ReflectionTestUtils.setField но экземпляр компонента, который я хочу внедрить, на самом деле является прокси и, следовательно, не объявляет поля закрытого члена целевого класса. Я не хочу создавать общедоступный установщик для зависимости, так как тогда я буду модифицировать свой интерфейс исключительно для целей тестирования.

Я следовал некоторым советам, данным сообществом Spring, но макет не создается, и автоматическое подключение не работает:

<bean id="dao" class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="com.package.Dao" />
</bean>

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

...
Caused by: org...NoSuchBeanDefinitionException:
    No matching bean of type [com.package.Dao] found for dependency:
    expected at least 1 bean which qualifies as autowire candidate for this dependency.
    Dependency annotations: {
        @org...Autowired(required=true),
        @org...Qualifier(value=dao)
    }
at org...DefaultListableBeanFactory.raiseNoSuchBeanDefinitionException(D...y.java:901)
at org...DefaultListableBeanFactory.doResolveDependency(D...y.java:770)

Если я установлю constructor-arg Значение чего-то недопустимого. Ошибка запуска при запуске контекста приложения.

24 ответа

Лучший способ это:

<bean id="dao" class="org.mockito.Mockito" factory-method="mock"> 
    <constructor-arg value="com.package.Dao" /> 
</bean> 

Обновить
В контекстном файле этот макет должен быть указан до того, как будет объявлено любое поле с автосвязью, в зависимости от того

@InjectMocks
private MyTestObject testObject;

@Mock
private MyDependentObject mockedObject;

@Before
public void setup() {
        MockitoAnnotations.initMocks(this);
}

Это добавит любые проверенные объекты в тестовый класс. В этом случае он вставит mockedObject в testObject. Это было упомянуто выше, но вот код.

У меня есть очень простое решение, использующее Spring Java Config и Mockito:

@Configuration
public class TestConfig {

    @Mock BeanA beanA;
    @Mock BeanB beanB;

    public TestConfig() {
        MockitoAnnotations.initMocks(this); //This is a key
    }

    //You basically generate getters and add @Bean annotation everywhere
    @Bean
    public BeanA getBeanA() {
        return beanA;
    }

    @Bean
    public BeanB getBeanB() {
        return beanB;
    }
}

Дано:

@Service
public class MyService {
    @Autowired
    private MyDAO myDAO;

    // etc
}

Вы можете загрузить тестируемый класс с помощью автопроводки, смоделировать зависимость с помощью Mockito, а затем использовать Spring ReflectionTestUtils, чтобы вставить макет в тестируемый класс.

@ContextConfiguration(classes = { MvcConfiguration.class })
@RunWith(SpringJUnit4ClassRunner.class)
public class MyServiceTest {
    @Autowired
    private MyService myService;

    private MyDAO myDAOMock;

    @Before
    public void before() {
        myDAOMock = Mockito.mock(MyDAO.class);
        ReflectionTestUtils.setField(myService, "myDAO", myDAOMock);
    }

    // etc
}

Обратите внимание, что до весны 4.3.1 этот метод не работал со службами за прокси-сервером (аннотирован @Transactional, или же Cacheable, например). Это было исправлено SPR-14050.

Для более ранних версий решение состоит в том, чтобы развернуть прокси-сервер, как описано там: Транзакционная аннотация предотвращает мошенничество служб (вот что ReflectionTestUtils.setField делает по умолчанию сейчас)

Если вы используете Spring Boot 1.4, у вас есть отличный способ сделать это. Просто используйте новый бренд @SpringBootTest в вашем классе и @MockBean на поле и Spring Boot создаст макет этого типа и вставит его в контекст (вместо внедрения оригинального):

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyTests {

    @MockBean
    private RemoteService remoteService;

    @Autowired
    private Reverser reverser;

    @Test
    public void exampleTest() {
        // RemoteService has been injected into the reverser bean
        given(this.remoteService.someCall()).willReturn("mock");
        String reverse = reverser.reverseSomeCall();
        assertThat(reverse).isEqualTo("kcom");
    }

}

С другой стороны, если вы не используете Spring Boot или используете предыдущую версию, вам придется проделать немного больше работы:

Создать @Configuration bean, который вставляет ваши макеты в контекст Spring:

@Configuration
@Profile("useMocks")
public class MockConfigurer {

    @Bean
    @Primary
    public MyBean myBeanSpy() {
        return mock(MyBean.class);
    }
}

С помощью @Primary примечание, вы говорите Spring, что этот бин имеет приоритет, если не указан спецификатор.

Убедитесь, что вы комментируете класс @Profile("useMocks") чтобы контролировать, какие классы будут использовать макет, а какие - настоящий бин.

Наконец, в вашем тесте активируйте userMocks профиль:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {Application.class})
@WebIntegrationTest
@ActiveProfiles(profiles={"useMocks"})
public class YourIntegrationTestIT {

    @Inject
    private MyBean myBean; //It will be the mock!


    @Test
    public void test() {
        ....
    }
}

Если вы не хотите использовать макет, а настоящий боб, просто не активируйте useMocks профиль:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = {Application.class})
@WebIntegrationTest
public class AnotherIntegrationTestIT {

    @Inject
    private MyBean myBean; //It will be the real implementation!


    @Test
    public void test() {
        ....
    }
}

Начиная с 1.8.3, у Mockito есть @InjectMocks - это невероятно полезно. Мои тесты JUnit - это @RunWith MockitoJUnitRunner, и я создаю объекты @Mock, которые удовлетворяют всем зависимостям для тестируемого класса, и все они внедряются, когда закрытый член аннотируется @InjectMocks.

Я @Run с SpringJUnit4Runner для интеграционных тестов только сейчас.

Отмечу, что он не может внедрить List таким же образом, как Spring. Он ищет только объект Mock, который удовлетворяет List, и не будет вводить список объектов Mock. Обходной путь для меня заключался в том, чтобы использовать @Spy для созданного вручную списка и вручную добавить фиктивные объекты в этот список для модульного тестирования. Возможно, это было сделано намеренно, потому что это, безусловно, заставило меня обратить пристальное внимание на то, что насмехалось вместе.

Обновление: теперь есть лучшие, более чистые решения этой проблемы. Пожалуйста, рассмотрите другие ответы в первую очередь.

В конце концов я нашел ответ на этот вопрос Ронена в своем блоге. Проблема у меня возникла из-за метода Mockito.mock(Class c) объявив тип возврата Object, Следовательно, Spring не может определить тип компонента из возвращаемого фабричным методом.

Решение Ронена состоит в том, чтобы создать FactoryBean реализация, которая возвращает макеты. FactoryBean Интерфейс позволяет Spring запрашивать тип объектов, созданных фабричным компонентом.

Моё определение бобового бина теперь выглядит так:

<bean id="mockDaoFactory" name="dao" class="com.package.test.MocksFactory">
    <property name="type" value="com.package.Dao" />
</bean>

С весны 3.2 это больше не проблема. Spring теперь поддерживает Autowiring результатов универсальных фабричных методов. См. Раздел "Общие заводские методы" в этом сообщении в блоге: http://spring.io/blog/2012/11/07/spring-framework-3-2-rc1-new-testing-features/.

Ключевым моментом является:

В Spring 3.2 общие возвращаемые типы для фабричных методов теперь правильно выводятся, и автоматическая разводка по типам для имитаций должна работать как положено. В результате пользовательские обходные пути, такие как MockitoFactoryBean, EasyMockFactoryBean или Springockito, вероятно, больше не нужны.

Что означает, что это должно работать из коробки:

<bean id="dao" class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="com.package.Dao" />
</bean>

Если вы используете пружину> = 3.0, попробуйте использовать пружины @Configuration аннотация для определения части контекста приложения

@Configuration
@ImportResource("com/blah/blurk/rest-of-config.xml")
public class DaoTestConfiguration {

    @Bean
    public ApplicationService applicationService() {
        return mock(ApplicationService.class);
    }

}

Если вы не хотите использовать @ImportResource, это можно сделать и наоборот:

<beans>
    <!-- rest of your config -->

    <!-- the container recognize this as a Configuration and adds it's beans 
         to the container -->
    <bean class="com.package.DaoTestConfiguration"/>
</beans>

Для получения дополнительной информации посмотрите на spring-framework-reference: конфигурация контейнера на основе Java

Приведенный ниже код работает с автоматической разводкой - это не самая короткая версия, но полезная, когда она должна работать только со стандартными пружинными / мокито банками.

<bean id="dao" class="org.springframework.aop.framework.ProxyFactoryBean">
   <property name="target"> <bean class="org.mockito.Mockito" factory-method="mock"> <constructor-arg value="com.package.Dao" /> </bean> </property>
   <property name="proxyInterfaces"> <value>com.package.Dao</value> </property>
</bean> 

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

Я могу сделать следующее, используя Mockito:

<bean id="stateMachine" class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="com.abcd.StateMachine"/>
</bean>

Опубликовать несколько примеров, основанных на вышеуказанных подходах

С весны:

@ContextConfiguration(locations = { "classpath:context.xml" })
@RunWith(SpringJUnit4ClassRunner.class)
public class TestServiceTest {
    @InjectMocks
    private TestService testService;
    @Mock
    private TestService2 testService2;
}

Без весны:

@RunWith(MockitoJUnitRunner.class)
public class TestServiceTest {
    @InjectMocks
    private TestService testService = new TestServiceImpl();
    @Mock
    private TestService2 testService2;
}

Я использую комбинацию подхода, используемого в ответе Маркусом Т, и простой вспомогательной реализацией ImportBeanDefinitionRegistrar который ищет пользовательскую аннотацию (@MockedBeans) в котором можно указать, какие классы должны быть смоделированы. Я полагаю, что этот подход приводит к краткому модульному тестированию с удалением некоторого стандартного кода, связанного с имитацией.

Вот как выглядит примерный модульный тест с таким подходом:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=AnnotationConfigContextLoader.class)
public class ExampleServiceIntegrationTest {

    //our service under test, with mocked dependencies injected
    @Autowired
    ExampleService exampleService;

    //we can autowire mocked beans if we need to used them in tests
    @Autowired
    DependencyBeanA dependencyBeanA;

    @Test
    public void testSomeMethod() {
        ...
        exampleService.someMethod();
        ...
        verify(dependencyBeanA, times(1)).someDependencyMethod();
    }

    /**
     * Inner class configuration object for this test. Spring will read it thanks to
     * @ContextConfiguration(loader=AnnotationConfigContextLoader.class) annotation on the test class.
     */
    @Configuration
    @Import(TestAppConfig.class) //TestAppConfig may contain some common integration testing configuration
    @MockedBeans({DependencyBeanA.class, DependencyBeanB.class, AnotherDependency.class}) //Beans to be mocked
    static class ContextConfiguration {

        @Bean
        public ExampleService exampleService() {
            return new ExampleService(); //our service under test
        }
    }
}

Чтобы это произошло, вам нужно определить два простых вспомогательных класса - пользовательскую аннотацию (@MockedBeans) и обычай ImportBeanDefinitionRegistrar реализация. @MockedBeans определение аннотации должно быть аннотировано @Import(CustomImportBeanDefinitionRegistrar.class) и ImportBeanDefinitionRgistrar необходимо добавить определения фиктивных бинов в конфигурацию в registerBeanDefinitions метод.

Если вам нравится подход, вы можете найти примеры реализации в моем блоге.

Обновление - новый ответ здесь: /questions/16919899/inektsiya-mockito-izdevaetsya-v-bobe-spring/16919926#16919926. Этот ответ относится только к тем версиям Spring до 3.2.

Некоторое время я искал более окончательное решение этого. Это сообщение в блоге, кажется, покрывает все мои потребности и не зависит от упорядочения объявлений бобов. Все заслуги Маттиаса Северсона. http://www.jayway.com/2011/11/30/spring-integration-tests-part-i-creating-mock-objects/

В основном, реализовать FactoryBean

package com.jayway.springmock;

import org.mockito.Mockito;
import org.springframework.beans.factory.FactoryBean;

/**
 * A {@link FactoryBean} for creating mocked beans based on Mockito so that they 
 * can be {@link @Autowired} into Spring test configurations.
 *
 * @author Mattias Severson, Jayway
 *
 * @see FactoryBean
 * @see org.mockito.Mockito
 */
public class MockitoFactoryBean<T> implements FactoryBean<T> {

    private Class<T> classToBeMocked;

    /**
     * Creates a Mockito mock instance of the provided class.
     * @param classToBeMocked The class to be mocked.
     */
    public MockitoFactoryBean(Class<T> classToBeMocked) {
        this.classToBeMocked = classToBeMocked;
    }

    @Override
    public T getObject() throws Exception {
        return Mockito.mock(classToBeMocked);
    }

    @Override
    public Class<?> getObjectType() {
        return classToBeMocked;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

Затем обновите ваш весенний конфиг следующим образом:

<beans...>
    <context:component-scan base-package="com.jayway.example"/>

    <bean id="someDependencyMock" class="com.jayway.springmock.MockitoFactoryBean">
        <constructor-arg name="classToBeMocked" value="com.jayway.example.SomeDependency" />
    </bean>
</beans>

Глядя на темп развития Springockito и количество открытых вопросов, я бы немного волновался о том, чтобы внедрить его в свой стек тестовых наборов. Тот факт, что последний выпуск был сделан до релиза Spring 4, поднимает такие вопросы, как "Возможно ли легко интегрировать его с Spring 4?". Я не знаю, потому что я не пробовал это. Я предпочитаю чистый подход Spring, если мне нужно смоделировать Spring bean в интеграционном тесте.

Существует возможность подделать Spring bean с помощью простых функций Spring. Вам нужно использовать @Primary, @Profile а также @ActiveProfiles аннотации к нему. Я написал сообщение в блоге на эту тему.

Я нашел аналогичный ответ как teabot, чтобы создать MockFactory, который предоставляет макеты. Я использовал следующий пример для создания фиктивной фабрики (поскольку ссылка на narkisr мертва): http://hg.randompage.org/java/src/407e78aa08a0/projects/bookmarking/backend/spring/src/test/java/org/randompage/bookmarking/backend/testUtils/MocksFactory.java

<bean id="someFacade" class="nl.package.test.MockFactory">
    <property name="type" value="nl.package.someFacade"/>
</bean>

Это также помогает предотвратить то, что Spring хочет разрешить инъекции из осмеянного компонента.

<bean id="mockDaoFactory" name="dao" class="com.package.test.MocksFactory">
    <property name="type" value="com.package.Dao" />
</bean>

это ^ отлично работает, если объявлено первым / рано в файле XML. Mockito 1.9.0/Spring 3.0.5

Я разработал решение, основанное на предложении Кресимира Несека. Я добавил новую аннотацию @EnableMockedBean, чтобы сделать код немного чище и модульным.

@EnableMockedBean
@SpringBootApplication
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes=MockedBeanTest.class)
public class MockedBeanTest {

    @MockedBean
    private HelloWorldService helloWorldService;

    @Autowired
    private MiddleComponent middleComponent;

    @Test
    public void helloWorldIsCalledOnlyOnce() {

        middleComponent.getHelloMessage();

        // THEN HelloWorldService is called only once
        verify(helloWorldService, times(1)).getHelloMessage();
    }

}

Я написал пост, объясняющий это.

Я бы предложил перенести ваш проект на Spring Boot 1.4. После этого вы можете использовать новую аннотацию @MockBean подделать ваш com.package.Dao

Для справки, все мои тесты правильно работают, просто делая инициализацию lazy-инициализированной, например:

<bean id="fixture"
      class="it.tidalwave.northernwind.rca.embeddedserver.impl.DefaultEmbeddedServer"
      lazy-init="true" /> <!-- To solve Mockito + Spring problems -->

<bean class="it.tidalwave.messagebus.aspect.spring.MessageBusAdapterFactory" />

<bean id="applicationMessageBus"
      class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="it.tidalwave.messagebus.MessageBus" />
</bean>

<bean class="org.mockito.Mockito" factory-method="mock">
    <constructor-arg value="javax.servlet.ServletContext" />
</bean>

Я полагаю, что обоснование - это то, что Маттиас объясняет здесь (внизу поста), что обходной путь изменяет порядок объявления бинов - ленивая инициализация - это "своего рода" объявление объекта в конце.

Если вы используете Spring boot 2.2+, вы можете использовать @MockInBean в качестве альтернативы @MockBean и держите свой контекст Spring чистым:

      @SpringBootTest
public class MyServiceTest {

    @MockInBean(MyService.class)
    private ServiceToMock serviceToMock;

    @Autowired
    private MyService myService;

    @Test
    public void test() {
        Mockito.when(serviceToMock.returnSomething()).thenReturn(new Object());
        myService.doSomething();
    }
}

отказ от ответственности: я создал эту библиотеку, чтобы избежать повторного создания контекста Spring, вызванного @MockBean / @ SpringBean, что приводит к медленным этапам тестирования сборки (см. Использование @MockBean в тестах вызывает перезагрузку контекста приложения или проблему с @MockBean)

Сегодня я обнаружил, что весенний контекст, в котором я объявил перед бобами Mockito, не загружался. После перемещения ПОСЛЕ макетов контекст приложения был успешно загружен. Береги себя:)

Если вы используете Controller Injection, убедитесь, что ваши локальные переменные НЕ являются "окончательными"

Другие вопросы по тегам