53

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

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

你有什么想法吗?

谢谢!

4

13 回答 13

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,因为当应用程序变老时很难追踪问题。

于 2009-02-19T15:31:22.853 回答
16

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

或者,我们已经成功地使用了以下设置,我认为它非常接近您想要的,我强烈推荐它:

对于在不同上下文中需要不同实现的所有 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 列表中。这样做的原因是因为您经常以不同的上下文扫描配置结束(相信我,如果您正在进行网络测试,您至少再添加 1 个注释,这会产生 4 个相关组合)

然后你基本上使用

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

请注意,此设置不允许您交替组合存根/实时数据。我们试过这个,我认为这会导致一团糟,我不推荐任何人;)我们要么连接全套存根,要么提供全套实时服务。

在依赖关系通常非常大的东西附近测试 gui 时,我们主要使用自动连接的存根依赖关系。在更干净的代码区域,我们使用更常规的单元测试。

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

  • 用于常规网络制作
  • 仅用于使用存根启动 web
  • 用于集成测试(在junit中)
  • 用于单元测试(在junit中)
  • 用于 selenium web 测试(在 junit 中)

这意味着我们总共有 5 种不同的系统范围配置,我们可以使用它们来启动应用程序。因为我们只使用注解,所以即使是我们想要连接的单元测试,spring 也足够快地自动连接。我知道这是非传统的,但它真的很棒。

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 附近的地区。

于 2009-02-19T23:08:14.110 回答
7

请参阅带有 @InjectedMock 注释的本 教程

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

@Mock
SomeClass mockedSomeClass

@InjectMock
ClassUsingSomeClass service

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

你所有的问题都解决了。Mockito 将用 mock 替换 spring 依赖注入。我自己用过,效果很好。

于 2013-10-31T22:13:35.443 回答
6

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

但是有一种FAR,FAR 更简单的方法来完成 Stas 所要求的,除了测试方法中的一行代码之外,它不涉及修改任何内容。它适用于单元测试和 Spring 集成测试,适用于自动装配的依赖项、私有和受保护的字段。

这里是:

junitx.util.PrivateAccessor.setField(testSubject, "fieldName", mockObject);
于 2009-06-16T07:39:51.520 回答
4

简单的。您为单元测试使用自定义应用程序上下文,或者根本不使用,而是手动创建和注入 bean。

在我看来,您的测试可能有点过于广泛。单元测试是关于测试,嗯,单元。Spring bean 是一个很好的单元示例。你不应该需要一个完整的应用程序上下文。我发现如果你的单元测试太高级以至于你需要数百个 bean、数据库连接等,你有一个非常脆弱的单元测试,它将在下一次更改时中断,将难以维护,而且确实是没有增加很多价值。

于 2009-02-19T13:44:09.113 回答
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 到 in-内存数据库,另一个可能使用 JNDI 查找)。

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

于 2009-02-19T14:43:53.947 回答
2

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

于 2009-02-19T14:02:34.967 回答
1

我没有声望点来堆积达菲莫的答案,但我只是想插话并说他对我来说是“正确”的答案。

使用自定义 applicationContext.xml 在单元测试设置中实例化 FileSystemXmlApplicationContext。在该自定义 xml 中,在顶部执行 duffymo 指示的操作。然后声明您的模拟 bean、非 JNDI 数据源等,这将覆盖导入中声明的 id。

对我来说就像一个梦想一样工作。

于 2009-07-24T20:23:30.000 回答
1

您不需要使用任何测试上下文(不管是基于 XML 还是基于 Java)。自 Spring boot 1.4 以来,有可用的新注解@MockBean引入了对 Spring Bean 的模拟和间谍活动的本机支持。

于 2016-09-05T13:32:17.710 回答
0

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

于 2009-02-19T13:47:55.580 回答
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 实在是太麻烦了。我正在寻找用代码创建那个子工厂。然后每个测试都可以按照它想要的方式配置它的工厂。这样的工厂没有理由不工作。

于 2009-06-10T09:33:10.430 回答
0

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

于 2014-02-02T17:39:00.000 回答
0

自 OP 以来,这已经出现:Springockito

于 2015-01-30T10:53:04.027 回答