Инъекция 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, убедитесь, что ваши локальные переменные НЕ являются "окончательными"