Spring bean在单元测试环境中重新定义

时间:2009-02-19 13:38:38

标签: spring unit-testing spring-test

我们将Spring用于我的应用程序目的,并使用Spring Testing框架进行单元测试。但是我们遇到一个小问题:应用程序代码从类路径中的位置列表(xml文件)加载Spring应用程序上下文。但是当我们运行单元测试时,我们希望一些Spring bean是模拟而不是完整的实现类。此外,对于某些单元测试,我们希望一些bean成为模拟,而对于其他单元测试,我们希望其他bean成为模拟,因为我们正在测试应用程序的不同层。

所有这些意味着我想重新定义应用程序上下文的特定bean并在需要时刷新上下文。在执行此操作时,我只想重新定义位于一个(或多个)原始xml bean定义文件中的一小部分bean。我找不到一个简单的方法来做到这一点。一直认为Spring是一个测试友好框架的单元,所以我必须在这里遗漏一些东西。

你有什么想法怎么做?

感谢。

13 个答案:

答案 0 :(得分:18)

我会提出一个自定义的TestClass和一些关于spring bean.xml位置的简单规则

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
    "classpath*:spring/*.xml",
    "classpath*:spring/persistence/*.xml",
    "classpath*:spring/mock/*.xml"})
@Transactional
@TestExecutionListeners({
    DependencyInjectionTestExecutionListener.class,
    TransactionalTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class})
public abstract class AbstractHibernateTests implements ApplicationContextAware 
{

    /**
     * Logger for Subclasses.
     */
    protected final Logger LOG = LoggerFactory.getLogger(getClass());

    /**
     * The {@link ApplicationContext} that was injected into this test instance
     * via {@link #setApplicationContext(ApplicationContext)}.
     */
    protected ApplicationContext applicationContext;

    /**
     * Set the {@link ApplicationContext} to be used by this test instance,
     * provided via {@link ApplicationContextAware} semantics.
     */
    @Override
    public final void setApplicationContext(
            final ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
}

如果指定位置有mock-bean.xml,它们将覆盖“普通”位置中的所有“真实”bean.xml - 您的正常位置可能不同

但是...当应用程序变老时,我永远不会混合模拟和非模拟bean,很难跟踪问题。

答案 1 :(得分:16)

spring被描述为测试友好的原因之一是因为在单元测试中可能很容易只是 new 或模拟东西。

或者我们使用了以下设置并取得了巨大的成功,我认为它非常接近你想要的,我会强烈推荐它:

对于在不同上下文中需要不同实现的所有bean,请切换到基于注释的布线。你可以按原样留下其他人。

实施以下注释集

 <context:component-scan base-package="com.foobar">
     <context:include-filter type="annotation" expression="com.foobar.annotations.StubRepository"/>
     <context:include-filter type="annotation" expression="com.foobar.annotations.TestScopedComponent"/>
     <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
 </context:component-scan>

然后使用@Repository注释您的实时实现,使用@StubRepository注释您的存根实现,只有@TestScopedComponent才能出现在单元测试夹具中的任何代码。您可能会遇到需要更多注释的问题,但这些都是一个很好的开始。

如果你有很多spring.xml,你可能需要制作一些基本上只包含组件扫描定义的新的spring xml文件。您通常只需将这些文件附加到常规的@ContextConfiguration列表中。这样做的原因是因为你经常最终得到上下文扫描的不同配置(相信我,如果你正在进行网络测试,你至少再做一次注释,这使得4相关组合)

然后你基本上使用

@ContextConfiguration(locations = { "classpath:/path/to/root-config.xml" })
@RunWith(SpringJUnit4ClassRunner.class)

请注意,此设置允许您具有存根/实时数据的交替组合。我们尝试了这个,我认为这导致了一个混乱,我不会推荐任何人;)我们要么连接整套存根或全套实时服务。

我们主要使用自动连线存根依赖关系测试gui附近的东西,其中依赖性通常相当大。在代码的清洁区域,我们使用更常规的单元测试。

在我们的系统中,我们有以下用于组件扫描的xml文件:

  • 用于常规网站制作
  • 仅用存根启动网页
  • 用于集成测试(在junit中)
  • 用于单元测试(在junit中)
  • 用于硒网测试(在junit中)

这意味着我们完全有5种不同的系统范围配置,我们可以启动应用程序。由于我们只使用注释,因此弹簧足够快,甚至可以自动连接我们想要连接的单元测试。我知道这是非传统的,但它真的很棒。

Out整合测试运行完整的实时设置,一两次我决定让真正实用,并希望有5条实时接线和一个模拟:

public class HybridTest {
   @Autowired
   MyTestSubject myTestSubject;


   @Test
   public void testWith5LiveServicesAndOneMock(){
     MyServiceLive service = myTestSubject.getMyService();
     try {
          MyService mock = EasyMock.create(...)
          myTestSubject.setMyService( mock);

           .. do funky test  with lots of live but one mock object

     } finally {
          myTestSubject.setMyService( service);
     }


   }
}

我知道测试纯粹主义者会为此而全力以赴。但有时它只是一个非常务实的解决方案,当替代方案真的很难看时,它会变得非常优雅。再次,它通常在那些gui-near区域。

答案 2 :(得分:7)

请参阅此tutorial with @InjectedMock annotation

它为我节省了很多时间。你只需使用

@Mock
SomeClass mockedSomeClass

@InjectMock
ClassUsingSomeClass service

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

你所有的问题都解决了。 Mockito将用模拟替换弹簧依赖注入。我自己用它并且效果很好。

答案 3 :(得分:6)

这里列出了一些非常复杂和强大的解决方案。

但是有一种 FAR,FAR更简单的方式来完成Stas提出的问题,这不涉及修改测试方法中除一行代码之外的任何内容。它适用于单元测试和Spring集成测试,适用于自动连接的依赖项,私有和受保护的字段。

这是:

junitx.util.PrivateAccessor.setField(testSubject, "fieldName", mockObject);

答案 4 :(得分:4)

您也可以编写单元测试,根本不需要任何查找:

@ContextConfiguration(locations = { "classpath:/path/to/test-config.xml" })
@RunWith(SpringJUnit4ClassRunner.class)
public class MyBeanTest {

    @Autowired
    private MyBean myBean; // the component under test

    @Test
    public void testMyBean() {
        ...
    }
}

这提供了一种简单的方法来将真实配置文件与测试配置文件混合和匹配。

例如,当使用hibernate时,我可能将sessionFactory bean放在一个配置文件中(在测试和主应用程序中都使用),并在另一个配置文件中使用dataSource bean(可能使用DriverManagerDataSource来一个内存中的数据库,另一个可能使用JNDI查找。)

但是,一定要注意@cletus's警告; - )

答案 5 :(得分:3)

易。您可以使用自定义应用程序上下文进行单元测试。或者你根本不使用一个,你手动创建并注入你的bean。

听起来我的测试可能有点过于宽泛。单元测试是关于测试,单元。 Spring bean是一个非常好的单元示例。您不应该需要整个应用程序上下文。我发现,如果您的单元测试水平如此之高,以至于您需要数百个bean,数据库连接等,那么您将有一个非常脆弱的单元测试,它将在下一次更改时中断,将难以维护并且真的不是'增加了很多价值。

答案 6 :(得分:2)

您可以使用测试应用上下文中的import功能加载产品bean并覆盖您想要的产品。例如,我的prod数据源通常是通过JNDI查找获取的,但是当我测试时我使用的是DriverManager数据源,因此我不必启动应用服务器进行测试。

答案 7 :(得分:1)

我没有在duffymo的答案上留下名声点,但我只想插话并说他对我来说是“正确的”答案。

使用自定义applicationContext.xml在单元测试的设置中实例化FileSystemXmlApplicationContext。在那个自定义xml中,在顶部,做一个duffymo指示。然后声明你的模拟bean,非JNDI数据源等,它们将覆盖导入中声明的id。

对我来说就像一个梦想。

答案 8 :(得分:1)

您不需要使用任何测试上下文(基于XML或Java无关紧要)。从Spring boot 1.4开始,可以使用新的注释@MockBean,它引入了对Spring Beans的模拟和间谍的原生支持。

答案 9 :(得分:0)

也许你可以为你的bean使用限定符?您将在单独的应用程序上下文中重新定义要模拟的bean,并使用限定符“test”标记它们。在单元测试中,在连接bean时,请始终指定限定符“test”以使用模拟。

答案 10 :(得分:0)

我想做同样的事情,我们发现它很重要。

我们使用的当前机制是相当手动但它可以工作。

例如,假设您想模拟Y类型的bean。我们所做的是每个具有该依赖关系的bean都实现了一个接口 - “IHasY”。这个界面是

interface IHasY {
   public void setY(Y y);
}

然后在我们的测试中,我们调用util方法......

 public static void insertMock(Y y) {
        Map invokers = BeanFactory.getInstance().getFactory("core").getBeansOfType(IHasY.class);
        for (Iterator iterator = invokers.values().iterator(); iterator.hasNext();) {
            IHasY invoker = (IHasY) iterator.next();
            invoker.setY(y);
        }
    }

我不想创建一个完整的xml文件只是为了注入这个新的依赖项,这就是我喜欢这个的原因。

如果您愿意创建一个xml配置文件,那么可以采用模拟bean创建一个新工厂,并使您的默认工厂成为该工厂的父工具。确保然后从新的子工厂加载所有bean。执行此操作时,当bean ID相同时,子工厂将覆盖父工厂中的bean。

现在,如果在我的测试中,如果我能以编程方式创建一个工厂,那将是非常棒的。必须使用xml太麻烦了。我想用代码创建那个子工厂。然后,每个测试都可以按照自己的方式配置工厂。这样的工厂没有理由不起作用。

答案 11 :(得分:0)

spring-reinject旨在用模拟代替bean。

答案 12 :(得分:0)

自OP出现以来:Springockito