【问题标题】:How to avoid duplicating test code如何避免重复测试代码
【发布时间】:2009-06-10 14:39:09
【问题描述】:

我已经编写了我的单元测试,并且在需要外部资源的地方使用假货来处理。

到目前为止一切都很好。现在我面临着其他测试阶段,主要是集成,我想针对真实的外部资源(例如数据库)重复单元测试方法。

那么,对于构建单元与集成测试的测试项目有哪些建议?我知道有些人更喜欢单元和集成的单独程序集?

如何在两个程序集之间共享通用测试代码?我应该创建一个包含所有抽象测试类的第三程序集并让单元和集成继承吗?我正在寻找最大的可重用性...

我听到很多关于依赖注入 (StructureMap) 的噪音,如何在给定的单元 + 集成测试设置中使用这样的工具?

谁能分享一些智慧?谢谢

【问题讨论】:

    标签: unit-testing testing integration-testing


    【解决方案1】:

    我认为您不应该在物理上将两者分开。一个好的解决方案是将 Microsoft.TeamFoundation.PowerTools.Tasks.CategoryAttribute 放在您的测试之上,以识别常规测试和集成测试。运行测试时(即使使用 MSBuild),您可以决定只运行您感兴趣的测试。

    或者,您可以将它们放在单独的命名空间中。

    【讨论】:

    • 我认为它与另一条评论中提到的困境相同。我不会仍然创建两组测试方法(一组用于单元和集成)..是否不可能有某种抽象,所以测试方法只定义一次,并且可以由任何继承的测试类执行?
    【解决方案2】:

    对于将在设置和拆卸阶段执行的代码,基类方法可以很好地工作。对于集成测试,您可以将单元测试的功能提取到参数化良好的非测试方法中(最好放在另一个命名空间中),并从单元测试和集成测试中调用这些“通用”方法。将单元测试、集成测试和常用方法放入单独的命名空间就足够了,不需要额外的程序集。

    【讨论】:

      【解决方案3】:

      一种方法是使用可在多个测试上下文中使用的辅助方法创建一个单独的文件,然后将该文件包含在您的单元测试和功能测试中。对于变化的部分,您可以使用依赖注入 - 例如,通过传入不同的工厂。在单元测试中,工厂可以构建一个假对象,而在功能测试中,它可以将一个真实对象插入到您的测试数据库中。

      【讨论】:

        【解决方案4】:

        您是将测试分成两个项目还是将它们放在一个项目中可能取决于您拥有的类/测试的数量。单个项目中的类太多会使挖掘变得困难。如果您确实将它们分开,则可以将辅助/通用方法放入第三个程序集中,或者您可以在单元测试程序集中将它们公开,并让集成程序集引用该程序集。让事情变得尽可能复杂。

        【讨论】:

        • 通过公开单元测试方法,我不必两次声明相同的测试方法。一次是在单元测试中,一次是在集成测试中,尽管调用基本方法来进行断言?
        • 如果您指的是常用方法,则可以将它们声明为静态。或者你可以实例化你的助手类。您不必从另一个类派生一个类。
        【解决方案5】:

        在我们的项目中,我们将集成测试和单元测试放在一起,但位于不同的文件夹中。我们的项目布局是这样的,我们为主要部分(域、服务等)提供了单独的程序集。每个组件都有一个匹配的测试组件。允许测试程序集引用其他测试程序集。

        这意味着 Services.Test 可以引用 Domain.Test,这对我们来说很有意义,因为 Services 在实际代码中引用了 Domain。

        就可重复使用的部分而言,我们拥有

        • Builders - 这些提供了一个流畅的界面来创建我们领域中最重要/最复杂的对象。这些位于我们域的主测试文件夹中。我们的域测试程序集被所有其他测试程序集引用。

        • Mothers - 这些将数据插入数据库。他们为插入的行返回一个 ID,如果需要,可以使用它来加载对象。它们位于我们服务的主测试文件夹中。

        • Helpers - 这些人在我们的测试过程中会做一些小事。例如,我们更喜欢允许通过 IEnuermable 访问集合,因此我们有一个 CollectionHelper.AssertCountIsEqualTo<_t>(int count, IEnumerable<_t> collection, string message),它将 IEnumerable 包装在一个 List 中并声明一个计数。我们的 Helpers 都生活在一个共同的测试中,其他测试都会引用该测试。

        至于 IoC 容器,如果您可以在您的项目中使用它,那么它们不仅可以在测试(通过自动模拟)方面提供巨大帮助,而且在一般开发方面也有很大帮助。无意中听到使用包含注册所有内容,尽管这对于仅测试而言可能有点多。

        【讨论】:

        • 我认为您的单元测试不涉及数据库?如果是这样,如何使用模拟启动单元测试以及如何使用数据库启动集成测试?您是否重复使用相同的代码来执行单元和集成测试。例如,您如何为这两个测试调用此代码.. [TestMethod] public void CanGetData() { Assert.IsTrue() //Somehthing }
        • 根据定义,单元测试不涉及任何外部资源。事实上,也应该避免接触其他类(尽管对这一类教条主义可能代价高昂)。如果一个测试需要接触数据库,它就变成了一个集成测试。我有一个集成测试类的基类,它提供所有数据库访问。
        【解决方案6】:

        经过一些试验,这是您可以重用测试方法的方法:

            public abstract class TestBase
            {
                [TestMethod]
                public void BaseTestMethod()
                {
                    Assert.IsTrue(true);
                }
            }
        
        
            [TestClass]
            public class UnitTest : TestBase
            {
            }
        
            [TestClass]
            public class IntegrationTest : TestBase
            {
            }
        

        单元和集成测试类将选取基类测试方法并将它们作为两个单独的测试运行。

        您应该能够在基类上创建一个参数化的构造函数来注入您的模拟或资源。

        我认为这种方法可以在同一个程序集中与类一起使用。所以看起来现在必须使用单一的组装方法。

        感谢大家的建议!

        【讨论】:

        • 请记住,您可以在没有 [TestMethod] 属性的方法中使用 Assert 语句。因此,您可以通过调用两者都引用的第三个方法来让两个测试执行部分相同的功能,无论是在同一个程序集中还是在不同的程序集中。
        【解决方案7】:

        如果您的许多单元测试和相应的集成测试之间的唯一区别是后者使用“真实”资源而不是假货(模拟),则一种方法如下:

        1. 制作一个标志 is_unit_test 可从外部供您的测试类使用
        2. 在课程设置中,根据标志提供虚假或真实资源。例如,如果您需要使用真实的(DBreal 类的实例)或假的(DBfake 类的实例)的 DB API,您的初始化可能类似于if is_unit_test then this.dbapi = new DBfake else this.dbapi = new DBreal。 (DBrealDBfake需要符合同一个接口,我们称之为DBapi。)
        3. 从您的测试方法的角度来看,第 2 步相当于(手动)依赖注入:该方法不知道哪个类实际实现了它的依赖(DB API)。相反,依赖项是从外部注入到方法中的。
        4. 如果您的测试用例需要 DB API,它们使用this.dbapi
        5. 现在您执行一个相同的测试类,其中设置了用于单元测试的标志,而没有设置用于集成测试的标志。 (如何使标志可用取决于您的单元测试框架。)
        6. 显然,如果您在测试类中需要多个资源,则可以使用相同的方法。
        7. 有些人会发现第 2 步中显式的 if 很难看。为了使其更“优雅”,您可以使用 Inversion of Control (IoC) 容器(在 Java 中,例如 Spring 或 PicoContainer)来半自动化依赖注入。然后初始化看起来像this.dbapi = myContainer.create(DBapi)
        8. 简单情况中,IoC 容器只会使事情复杂化,因为配置容器并非易事,涉及学习,可能会出现新一类错误,并且涉及额外文件。李>
        9. 然而,在更复杂的情况中,容器使事情变得更容易,因为如果您的资源的创建还需要其他资源,容器也会处理它们的初始化,而复杂性确实会降低.但除非你真的到了那里,否则我建议KISS
        10. 除非你有一个重要的理由分开组装,否则它们违反了 KISS。我建议先等待这个原因。

        (请注意,有些人可能会告诉您依赖注入仅在类级别完成。 我认为这是毫无根据的教条主义。注入只是意味着调用者不知道它正在调用的确切类,无论它如何获得对象。在类级别应用时,它通常会变得更有用,但根据您的测试框架,这可能会使上述情况变得过于复杂。请注意,一些测试框架有自己的注入功能。)

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-08-29
          • 1970-01-01
          • 1970-01-01
          • 2018-08-16
          • 2014-03-07
          相关资源
          最近更新 更多