【问题标题】:Spring beans redefinition in unit test environmentSpring beans在单元测试环境中的重新定义
【发布时间】:2010-10-08 14:20:11
【问题描述】:

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

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

你有什么想法吗?

谢谢!

【问题讨论】:

  • Spring 让单元测试变得非常简单。那是您缺少的部分-您正在进行集成测试而不是单元测试。在真正的单元测试中,任何依赖的 bean 都应该是模拟的,因为您只测试一个单元而不是整个系统。
  • 如何单元测试一个spring应用:confessionsofanagilecoach.blogspot.com/2016/05/…

标签: spring unit-testing spring-test


【解决方案1】:

我会提出一个自定义的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,因为当应用程序变老时很难追踪问题。

【讨论】:

    【解决方案2】:

    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注释你的live实现,用@StubRepository注释你的stub实现,任何应该出现在单元测试夹具中的代码只能用@TestScopedComponent .您可能会遇到需要更多注释的情况,但这是一个很好的开始。

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

    那你基本上就用了

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

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

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

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

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

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

    在完整的实时设置下运行集成测试,有一次或两次我决定真正务实并希望拥有 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 附近的地区。

    【讨论】:

    • 只是好奇,这不会导致针对每个 Testclass 的单个 @ContextConfiguration 的一些维护地狱吗?
    • 不,我们在一个大型应用程序中总共只有 5 个系统范围的@ConfigurationContxt。我重新编辑以使其更清晰。
    • 感谢您的回答,我成功应用了它。需要注意的一点是,这种方式非常灵活,因为您可以使用多个 context:component-scan 标签(例如,每个模块或每个包一个)。此外,设置 'use-default-filters="false" 可能会有所帮助。
    【解决方案3】:

    看到这个tutorial with @InjectedMock annotation

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

    @Mock
    SomeClass mockedSomeClass
    
    @InjectMock
    ClassUsingSomeClass service
    
    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }
    

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

    【讨论】:

      【解决方案4】:

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

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

      这里是:

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

      【讨论】:

      • 或者,如果您使用的是 spring:org.springframework.test.util.ReflectionTestUtils.setField(testSubject, "fieldName", mockObject);
      • 假设您可以轻松访问注入依赖项的对象。
      • 这假定使用类不会在其构造函数或初始化方法中访问使用的类。在这种情况下,通过反射替换 bean 的字段就太晚了。
      【解决方案5】:

      您还可以编写单元测试以完全不需要任何查找:

      @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() {
              ...
          }
      }
      

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

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

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

      【讨论】:

      • 这会导致许多重复的 bean.xml 用于测试,对于仅模拟测试我宁愿提出 easyMock 或类似的模拟框架
      • 是的 - 我们的一些测试使用 EasyMock 而不是这种方法。恕我直言,EasyMock 倾向于在真正的单元测试中大放异彩。更多“类集成”测试受益于 ContextConfiguration 方法。
      【解决方案6】:

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

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

      【讨论】:

      • 你说得对,我们使用的是系统测试而不是单元测试,我们实际上是在我们的应用服务器上测试从 API 到 DB 的流程。我意识到编写单元测试是一种更好的方法,但目前还不可行。
      【解决方案7】:

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

      【讨论】:

        【解决方案8】:

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

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

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

        【讨论】:

          【解决方案9】:

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

          【讨论】:

            【解决方案10】:

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

            【讨论】:

              【解决方案11】:

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

              我们使用的当前机制是相当手动的,但它确实有效。

              例如,您希望模拟出 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 实在是太麻烦了。我正在寻找用代码创建那个子工厂。然后每个测试都可以按照它想要的方式配置它的工厂。这样的工厂没有理由不工作。

              【讨论】:

                【解决方案12】:

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

                【讨论】:

                  【解决方案13】:

                  自从 OP 出现后:Springockito

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2017-03-25
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2015-03-24
                    • 2010-10-03
                    • 2021-05-12
                    • 1970-01-01
                    相关资源
                    最近更新 更多