将Mockito模拟对象注入Spring bean

304
我想在JUnit单元测试中将Mockito模拟对象注入到Spring(3+)bean中。目前,我的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的值设置为无效值,启动应用程序上下文时不会出现错误。


4
请看这个微小的生物: https://bitbucket.org/kubek2k/springockito/wiki/Home - kubek2k
2
你让我想起了Springockito注解。 - yihtserns
2
请注意 https://bitbucket.org/kubek2k/springockito/issue/37/spring-test-context-caching-confuses。 - Vadzim
2
对于那些使用Spring 4.*的人来说,截至2015年1月,这似乎无法与最新的Spring Mockito版本配合使用,而且该项目似乎处于停滞状态。 - Murali
1
@kubek2k:你的链接已经失效了,请更新一下吧? - Dominik Dorn
显示剩余3条评论
23个回答

134

最好的方法是:

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

更新
在上下文文件中,这个模拟对象必须在任何依赖它的自动装配字段声明之前列出。


4
@amra:在这种情况下,Spring不会推断返回的对象类型... https://dev59.com/I1rUa4cB1Zd3GeqPfwbd - lisak
8
不知道为什么这个回答得到了这么多赞,因为生成的 bean 无法自动装配,因为它的类型不正确。 - azerole
4
如果它在上下文文件中排在所有需要依赖它的字段声明之前,那么它就可以自动装配。 - Ryan Walls
3
从Spring 3.2开始,bean的顺序不再重要。请参阅此博文中标题为“通用工厂方法”的部分:http://spring.io/blog/2012/11/07/spring-framework-3-2-rc1-new-testing-features/ - Ryan Walls
1
问题说OP已经尝试过这个方法,但没有成功。提供一个只是重复OP已经做的事情的答案是没有意义的。100多个点赞者有没有仔细阅读问题? - Dawood ibn Kareem
显示剩余7条评论

117
@InjectMocks
private MyTestObject testObject;

@Mock
private MyDependentObject mockedObject;

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

这将把任何模拟对象注入到测试类中。在该例子中,它将把mockedObject注入到testObject中。如上所述,这里是代码:


1
如何对mockedObject的特定方法进行存根? - Jim Holden
47
请注意:如果我想在 MyTestObject 中部分使用自动装配和部分模拟,这种方法将无法奏效。 - raksja
1
这正是我一直在寻找的解决方案。非常简单优雅,而且能够胜任工作。不确定为什么没有更多的点赞。 - jalbr74
11
我不知道为什么这个问题没有得到更高的投票。如果我看到更多包含 XML 的答案,我就要吐了。 - MarkOfHall
3
为什么不在这个mockedObject上使用Mockito.spy(...),然后使用when(mockedObject.execute).thenReturn(objToReturn)或者doReturn(objToReturn).when(mockedObject).execute()呢?第二种方法不会调用真实的方法。你也可以查看Mockito.doCallRealMethod()文档。 - Tomasz Przybylski
显示剩余3条评论

65

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

5
以某种方式,采用这种方法时,Spring 仍然试图创建实际的 bean(而不是模拟对象),并因此出现错误… 我做错了什么? - Daniel Gruszczyk
1
我有同样的问题。 - Korobko Alex
3
不是Spring而是Mockito会在你模拟一个类时尝试实例化一个实际的bean。 如果您需要在测试中模拟任何bean,则它们应该是接口的实现,并通过该接口进行注入。 然后,如果您模拟接口(而不是类),Mockito将不会尝试实例化该类。 - Daniel Gruszczyk
10
为什么要这样做?为什么要在initMocks中添加注释字段和构造函数?为什么不只是在getBeanA中返回Mockito.mock(BeanA.class)?这样更简单,代码也更少。我错过了什么吗? - Oleg
1
@Oleg,听起来你有自己的解决方案,你应该将其发布为答案,这样社区就可以对其进行投票。 - Dawood ibn Kareem
显示剩余3条评论

57

假设:

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

    // etc
}

你可以通过自动装配(autowiring)加载被测试的类,使用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得到修复。

对于早期版本,解决方案是取消代理的包装,如Transactional annotation avoids services being mocked所述(这就是现在默认情况下ReflectionTestUtils.setField所做的)。


Double @RunWith(SpringJUnit4ClassRunner.class) 我使用不同的测试类注释(相同的运行器),但这种方法对我有效,谢谢。 - user1317422
1
我受到了“请注意,在Spring 4.3.1之前,此方法无法与代理后面的服务一起使用(例如带有@Transactional或@Cacheable注释)。这已经被SPR-14050修复”的启发。我正好遇到了这个问题,直到看到这些话才有所线索。非常感谢! - snowfox
1
这个解决方案处理了当您已经连接了整个应用程序上下文并且出于测试目的想要在上下文中的任意bean中注入模拟对象的情况。我使用了这个答案来模拟一个Feign客户端bean,以避免在模块测试中对其他模块进行REST调用。我只能在将要测试的bean中注入模拟对象时才能使InjectMock注释起作用,而不能在Spring应用程序配置创建的bean中注入。 - Andreas Lundgren
2
差不多整整一天都在试图让@MockBean正常工作,而不重置上下文,然后我找到了这个宝石。正是我所需要的,感谢。 - Matt R
可以工作,但要注意由于缓存而替换的字段可能不会重置,一些无关的测试可能会失败。例如,在我的测试中,我用模拟密码编码器替换了它,结果由于授权失败,还有其他几个测试也出现了问题。 - alextsil
@MattR:你可以使用@MockInBean作为@MockBean的替代,它不会重置上下文并保持干净。请参阅我的答案 - Antoine Meyer

54
如果您正在使用Spring Boot 1.4,它有一种很棒的方法来实现这个。只需在类上使用新的@SpringBootTest,并在字段上使用@MockBean,Spring Boot将创建此类型的模拟对象,并将其注入到上下文中(而不是注入原始对象):

如果您正在使用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,如果没有指定限定符,这个bean具有优先级。

确保使用@Profile("useMocks")注解类,以控制哪些类将使用模拟对象,哪些类将使用真实的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() {
        ....
    }
}

如果您不想使用模拟bean而是真正的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() {
        ....
    }
}

6
这个答案应该排在前面 - Spring Boot中的@MockBean支持也可以在不使用Spring Boot的情况下使用。你只能在单元测试中使用它,因此它适用于所有Spring应用程序! - bedrin
2
@Profile注解也可以设置在bean定义方法上,以避免创建单独的配置类。 - marcin
1
太棒了!我稍作修改,使它能够与我的老式 web.xml 和 AnnotationConfigWebApplicationContext设置一起工作。必须使用 @WebAppConfiguration 替代 @WebIntegrationTest,并使用 @ContextHierarchy@ContextConfiguration 替代 @SpringApplicationConfiguration - UTF_or_Death
1
我不得不为我的情况添加@Primary注解,因为在@PostConstruct中有一个失败的调用,我想要模拟它,但是@PostConstruct的bean在我的模拟之前就被创建了,所以它没有使用模拟(直到我添加了@Primary)。 - dragi
1
@MockBean有一项显著的缺点:它可能导致在构建测试阶段重新创建Spring上下文。(请参见https://dev59.com/plcO5IYBdhLWcg3wkif2或@MockBean的问题(https://www.baeldung.com/spring-tests#2-the-problems-withmockbean))。您可以使用@MockInBean作为@MockBean的替代方案,它不会重置上下文。请参见我的答案 - Antoine Meyer
1
很棒的解决方案,也适用于部分模拟(即当您想要模拟一个bean,但保留其余依赖项不变时)。 - nightblade9

20

自从 1.8.3 版本以来,Mockito引入了@InjectMocks注解——这非常有用。我的JUnit测试使用MockitoJUnitRunner@RunWith注解,并构建@Mock对象,这些对象满足被测试类的所有依赖项,当私有成员变量被注解为@InjectMocks时,它们都被注入。

现在,我只会对集成测试使用SpringJUnit4Runner@RunWith注解。

需要注意的是,它似乎不能像Spring那样以相同的方式注入List<T>。它只查找满足List的Mock对象,并不会注入一组Mock对象的List。对我来说,解决办法是使用一个@Spy来手动实例化一个List,并手动将Mock对象添加到该列表中进行单元测试。也许这是有意为之,因为它确实迫使我密切关注被mock的内容。


是的,这是最好的方法。 在我的情况下,Springockito由于某种原因实际上不会注入mocks。 - chaostheory

14

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

我最终在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>

1
更新了Ronen的解决方案链接:http://narkisr.com/blog/2008/2647754885089732945 - Jeff Martin
我不明白,工厂方法的返回类型是Object ... 但是Amra的解决方案具有通用返回类型,因此Spring应该能够识别它... 但是Amra的解决方案对我不起作用。 - lisak
这个解决方案中,Spring无法推断从factoryBean返回的bean类型,因此没有匹配到类型为[com.package.Dao]的bean... - lisak
1
时光机:http://web.archive.org/web/20120806223839/http://javadevelopmentforthemasses.blogspot.com/2008/07/mocking-spring-tests.html - Daniel Kaplan
这个链接仍然有效:http://javadevelopmentforthemasses.blogspot.com/2008/07/mocking-spring-tests.html 只需在浏览器中禁用链接重定向,您就可以看到它,而不是被迫查看他新博客上的404页面。 - approxiblue
或者按照我所做的。访问原始链接 - 然后在内容消失并被新博客的404消息替换之前,您大约有0.2秒钟的时间。这足以按下Ctrl-A Ctrl-C来复制内容。然后将其粘贴到文本编辑器中进行阅读。 - Dawood ibn Kareem

12

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

关键点是:

在Spring 3.2中,工厂方法的通用返回类型现在已经被正确推断,并且按类型进行mock对象的自动装配应该能够正常工作。因此,诸如MockitoFactoryBean、EasyMockFactoryBean或Springockito等自定义解决方案可能不再必要。

这意味着这应该可以直接使用:

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

10

如果您使用的是 spring >= 3.0 版本,建议尝试使用 Spring 的 @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的容器配置


不错。当我在实际测试用例中使用 @Autowired 进行测试时,我使用了这个。 - enkor

9
以下代码适用于自动装配 - 它不是最短的版本,但在仅使用标准的Spring/Mockito Jars时非常有用。
<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> 

对我有用。我必须在我的测试中取消代理以按照这里描述的方式进行验证:http://forum.spring.io/forum/spring-projects/aop/52011-need-to-unwrap-a-proxy-to-get-the-object-being-proxied - Holgzn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接