向Spring bean中注入Mockito mock

我想将Mockito模拟对象注入到Spring (3+) bean中,以便使用JUnit进行单元测试。我的bean依赖项目前是通过在私有成员字段上使用@Autowired注释注入的。

我考虑过使用ReflectionTestUtils.setField,但我希望注入的bean实例实际上是一个代理,因此没有声明目标类的私有成员字段。我不希望为依赖项创建公共setter,因为我将纯粹为了测试目的而修改我的接口。

我已经遵循了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值设置为无效的值,则在启动应用程序上下文时不会出现错误。

287859 次浏览

也许不是完美的解决方案,但我倾向于不使用spring来进行单元测试的DI。单个bean(被测试的类)的依赖关系通常不会太复杂,所以我只是直接在测试代码中进行注入。

现在有更好、更干净的解决方案来解决这个问题。请先考虑其他答案。

我最终在ronen的博客上找到了答案。我遇到的问题是由于方法Mockito.mock(Class c)声明返回类型为Object。因此,Spring无法从工厂方法返回类型推断出bean类型。

Ronen的解决方案是创建一个返回模拟对象的FactoryBean实现。FactoryBean接口允许Spring查询工厂bean创建的对象类型。

我的模拟bean定义现在看起来如下:

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

我可以使用Mockito做以下事情:

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

最好的方法是:

<bean id="dao" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="com.package.Dao" />
</bean>
< p > 更新
在上下文文件中,这个mock必须在任何自动连接字段之前列出,这取决于它的声明

因为1.8.3 Mockito有@InjectMocks -这是非常有用的。我的JUnit测试是@RunWithMockitoJUnitRunner,我构建了@Mock对象,它们满足被测试类的所有依赖关系,这些依赖关系都是在私有成员被@InjectMocks注释时注入的。

@RunWithSpringJUnit4Runner集成测试只是现在。

我要注意的是,它似乎不能像Spring那样注入List<T>。它只寻找满足List的Mock对象,而不会注入Mock对象列表。我的解决方法是对手动实例化的列表使用@Spy,并手动将模拟对象添加到该列表中进行单元测试。也许这是故意的,因为它确实迫使我密切关注被嘲笑的是什么。

今天我发现在Mockito bean之前我声明了一个spring上下文,无法加载。 移动After模拟后,应用程序上下文被成功加载。 小心:)

如果你正在使用Spring >= 3.0,请尝试使用Springs @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的容器配置

下面的代码使用自动装配-它不是最短的版本,但当它只适用于标准spring/mockito jar时很有用。

<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>
@InjectMocks
private MyTestObject testObject;


@Mock
private MyDependentObject mockedObject;


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

这将把任何模拟对象注入到测试类中。在本例中,它将把mockedObject注入到testObject中。上面提到过,下面是代码。

我找到了一个类似于teabot的答案,以创建一个MockFactory,提供模拟。我使用下面的例子来创建模拟工厂(因为到narkisr的链接是死的): http://hg.randompage.org/java/src/407e78aa08a0/projects/bookmarking/backend/spring/src/test/java/org/randompage/bookmarking/backend/testUtils/MocksFactory.java < / p >
<bean id="someFacade" class="nl.package.test.MockFactory">
<property name="type" value="nl.package.someFacade"/>
</bean>

这也有助于防止Spring想要解析来自模拟bean的注入。

我有一个非常简单的解决方案使用Spring Java配置和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;
}
}

基于上述方法,我将给出几个例子

春天:

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

为了记录,我所有的测试都是通过使fixture惰性初始化来正确工作的,例如:

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

我认为基本原理是Mattias解释的在这里(在文章的底部),一个变通方法是改变bean声明的顺序-延迟初始化是“某种程度上”在最后声明fixture。

更新 -这里的新答案:https://stackoverflow.com/a/19454282/411229。这个答案只适用于3.2之前的Spring版本。

我一直在寻找一个更明确的解决方案。这篇博客文章似乎涵盖了我的所有需求,并且不依赖于bean声明的顺序。这一切都要归功于Mattias Severson。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;
}
}

接下来更新你的spring配置如下:

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

从Spring 3.2开始,这不再是一个问题。Spring现在支持通用工厂方法结果的自动装配。请参阅本博客文章中“通用工厂方法”一节:http://spring.io/blog/2012/11/07/spring-framework-3-2-rc1-new-testing-features/

重点是:

在Spring 3.2中,工厂方法的泛型返回类型现在是 正确地推断,并按类型自动装配模拟应该工作为 预期。因此,自定义变通方法,如 MockitoFactoryBean、EasyMockFactoryBean或springckito可能不是 不再必要。< / p >

这意味着这应该是开箱即用的:

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

考虑到:

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

请注意,在Spring 4.3.1之前,此方法不适用于代理后面的服务(例如,用@TransactionalCacheable注释)。此问题已由spr - 14050修复。

对于早期版本,一种解决方案是打开代理,如此处所述:事务性注释避免了服务被模仿(这是ReflectionTestUtils.setField现在默认做的事情)

看看春暖花开的发展步伐未决问题数量,我现在有点担心把它引入我的测试套件堆栈。事实上,上一个版本是在Spring 4发布之前发布的,这带来了诸如“是否可能轻松地将其与Spring 4集成?”这样的问题。我不知道,因为我没试过。如果我需要在集成测试中模拟Spring bean,我更喜欢纯Spring方法。

有一个选项可以使用简单的Spring特性来伪造Spring bean。你需要为它使用@Primary@Profile@ActiveProfiles注释。我就这个话题写了一篇博客。

我使用了Markus T在回答中使用的方法和ImportBeanDefinitionRegistrar的一个简单的helper实现的组合,它寻找一个自定义注释(@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
}
}
}
要做到这一点,你需要定义两个简单的helper类——custom annotation (@MockedBeans)和custom ImportBeanDefinitionRegistrar实现。@MockedBeans注释定义需要用@Import(CustomImportBeanDefinitionRegistrar.class)进行注释,并且ImportBeanDefinitionRgistrar需要在它的registerBeanDefinitions方法中将模拟bean定义添加到配置中

如果你喜欢这种方法,你可以在我的博客上找到样本实现

我根据Kresimir Nesek的建议开发了一个解决方案。我添加了一个新的注释@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,它有一种很棒的方式来实现这一点。只要在你的类上使用新品牌@SpringBootTest,在字段上使用@MockBean, Spring Boot就会创建一个这种类型的mock,并将它注入到上下文中(而不是注入原来的那个):

@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,如果没有指定限定符,则此bean具有优先级。

确保用@Profile("useMocks")注释了类,以控制哪些类将使用mock,哪些类将使用真正的bean。

最后,在测试中,激活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() {
....
}
}

如果你不想使用mock而是真正的bean,只要不激活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() {
....
}
}

我建议将您的项目迁移到Spring Boot 1.4。之后,你可以使用新的注释@MockBean来伪造你的com.package.Dao

如果你正在使用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();
    }
}

免责声明:我创建这个库是为了避免由@MockBean/@SpringBean引起的Spring上下文重新创建,导致构建测试阶段变慢(参见在测试中使用@MockBean强制重新加载应用程序上下文 @MockBean的问题)