关于依赖注入:将Mockito模拟注入Spring bean

Injecting Mockito mocks into a Spring bean

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

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

我遵循了Spring社区提供的一些建议,但模拟模型没有创建,自动布线失败:

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

我目前遇到的错误如下:

1
2
3
4
5
6
7
8
9
10
...
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值设置为无效值,那么在启动应用程序上下文时不会出现错误。


最好的方法是:

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

更新< BR/>在上下文文件中,此模拟必须在声明依赖于它的任何自动连线字段之前列出。


1
2
3
4
5
6
7
8
9
10
@InjectMocks
private MyTestObject testObject;

@Mock
private MyDependentObject mockedObject;

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

这将向测试类中注入任何模拟对象。在这种情况下,它将把mockedObject注入到testObject中。这是上面提到的,但这里是代码。


我有一个非常简单的解决方案,使用Spring Java CONFIG和Mockito:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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;
    }
}


鉴于:

1
2
3
4
5
6
7
@Service
public class MyService {
    @Autowired
    private MyDAO myDAO;

    // etc
}

您可以让正在测试的类通过自动布线加载,用mockito模拟依赖关系,然后使用Spring的ReflectionTestUtils将mock注入正在测试的类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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
}

请注意,在Spring4.3.1之前,此方法不适用于代理背后的服务(例如,用@TransactionalCacheable注释)。这已由SPR-14050修复。

对于早期版本,解决方案是解包代理,如前所述:事务性注释避免服务被模拟(这是ReflectionTestUtils.setField现在默认的做法)


如果您使用的是SpringBoot1.4,那么它有一种很棒的方法。只需在课堂上使用新品牌@SpringBootTest,在场地上使用@MockBean,Spring Boot将创建这种类型的模拟,并将其注入上下文(而不是注入原始的模拟):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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或使用的是以前的版本,则需要做更多的工作:

创建一个@Configurationbean,将模拟注入到Spring上下文中:

1
2
3
4
5
6
7
8
9
10
@Configuration
@Profile("useMocks")
public class MockConfigurer {

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

使用@Primary注释,您将告诉Spring,如果没有指定限定符,则此bean具有优先级。

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

最后,在测试中,激活userMocks配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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() {
        ....
    }
}

如果您不想使用模拟而使用真正的bean,只需不激活useMocks配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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 the mockitojunitrunner,我构建了@mock对象,它满足被测试类的所有依赖项,当私有成员用@injectmocks注释时,这些依赖项都被注入。

i@runwith the springjunit4runner for integration tests only now.

我会注意到,它似乎不能像Spring那样以同样的方式注入列表。它只查找满足列表的模拟对象,而不会插入模拟对象列表。对于我来说,解决方法是对手动实例化的列表使用@spy,然后手动将模拟对象添加到该列表中进行单元测试。也许这是故意的,因为它确实迫使我密切关注被嘲笑的东西。


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

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

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

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

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


从Spring3.2开始,这不再是一个问题。现在Spring支持通用工厂方法结果的自动连接。请参阅本博客中标题为"通用工厂方法"的部分:http://spring.io/blog/2012/11/07/spring-framework-3-2-rc1-new-testing-features/。

关键是:

In Spring 3.2, generic return types for factory methods are now
properly inferred, and autowiring by type for mocks should work as
expected. As a result, custom work-arounds such as a
MockitoFactoryBean, EasyMockFactoryBean, or Springockito are likely no
longer necessary.

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

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

如果使用的是spring>=3.0,请尝试使用spring @Configuration注释来定义应用程序上下文的一部分。

1
2
3
4
5
6
7
8
9
10
@Configuration
@ImportResource("com/blah/blurk/rest-of-config.xml")
public class DaoTestConfiguration {

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

}

如果您不想使用@importresource,也可以用另一种方法:

1
2
3
4
5
6
7
<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框架参考:基于Java的容器配置


下面的代码适用于自动布线——它不是最短的版本,但在只适用于标准的Spring/Mockito JAR时非常有用。

1
2
3
4
<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>


我可以使用mockito执行以下操作:

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


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


基于上述方法发布几个示例

带弹簧:

1
2
3
4
5
6
7
8
@ContextConfiguration(locations = {"classpath:context.xml" })
@RunWith(SpringJUnit4ClassRunner.class)
public class TestServiceTest {
    @InjectMocks
    private TestService testService;
    @Mock
    private TestService2 testService2;
}

没有春天:

1
2
3
4
5
6
7
@RunWith(MockitoJUnitRunner.class)
public class TestServiceTest {
    @InjectMocks
    private TestService testService = new TestServiceImpl();
    @Mock
    private TestService2 testService2;
}

更新-新答案:https://stackoverflow.com/a/19454282/411229。此答案仅适用于3.2之前的弹簧版本。

我已经找了一段时间来寻找更明确的解决办法。这篇博文似乎涵盖了我所有的需求,并不依赖于bean声明的排序。全部归功于马蒂亚斯·塞弗森。http://www.jayway.com/2011/11/30/spring-integration-tests-part-i-creating-mock-objects/

基本上,实现FactoryBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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配置:

1
2
3
4
5
6
7
<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>


考虑到SpringLockito的开发速度和开放问题的数量,我现在有点担心将它引入我的测试套件堆栈。上一次发布是在Spring4发布之前完成的,这一事实带来了这样的问题:"是否可以轻松地将其与Spring4集成?"我不知道,因为我没试过。如果需要在集成测试中模拟SpringBean,我更喜欢纯Spring方法。

有一个选项可以用简单的弹簧特性来伪造SpringBean。您需要使用@Primary@Profile@ActiveProfiles注释。我写了一篇关于这个主题的博文。


我找到了与Teabot相似的答案,创建了一个提供模拟的MockFactory。我使用下面的示例创建了模拟工厂(因为到narkisr的链接已断开):http://hg.randompage.org/java/src/407e78aa08a00/projects/bookmarking/backend/spring/src/test/java/org/randompage/bookmarking/backend/testuils/mocksfactory.java

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

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


我结合了MarkusT在应答中使用的方法和一个简单的ImportBeanDefinitionRegistrar的助手实现,它查找一个自定义注释(@MockedBeans),其中可以指定要模拟哪些类。我相信这种方法会导致一个简洁的单元测试,删除一些与模拟相关的样板代码。

下面是使用这种方法的示例单元测试的外观:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@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方法的配置中添加模拟bean定义。

如果您喜欢这种方法,可以在我的blogpost上找到示例实现。


我根据Kresimir Nesek的建议开发了一个解决方案。我添加了一个新的注释@EnableMockedBean,以便使代码更清晰和模块化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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();
    }

}

我写了一篇文章解释它。


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


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

如果在XML文件的第一个/早期声明,则此^非常有效。Mockito 1.9.0/弹簧3.0.5


对于记录,我的所有测试都是通过使fixture lazy初始化来正确工作的,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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。


今天我发现我在mockito beans之前声明的一个Spring上下文加载失败。移动完模拟后,应用程序上下文被成功加载。小心:


如果使用控制器注入,请确保局部变量不是"最终"变量。