我想将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值设置为无效的值,那么在启动应用程序上下文时就不会出现错误。


当前回答

为了记录,我所有的测试都是通过使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。

其他回答

如果您正在使用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的容器配置

为了记录,我所有的测试都是通过使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。

我使用了Markus T在回答中使用的方法和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方法中将模拟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();
    }
}

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

考虑到:

@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之前,此方法不适用于代理后面的服务(例如用@Transactional或Cacheable注释)。sprr -14050已经修复了这个问题。

对于早期版本,一种解决方案是打开代理,如文中所述:事务性注释避免模拟服务(这就是ReflectionTestUtils。setField现在默认执行)