【问题标题】:How do you unit test a class that is dependent on many other classes?如何对依赖于许多其他类的类进行单元测试?
【发布时间】:2008-12-06 14:57:11
【问题描述】:

我听说通过单元测试我们可以发现代码中的大部分错误,我真的相信这是真的。但我的问题是在每个类都依赖于许多其他类的大型项目中,你如何对类进行单元测试?由于复杂性和编写存根所需的时间,将所有其他类都存根没有多大意义。您对此有何看法?

【问题讨论】:

    标签: unit-testing


    【解决方案1】:

    使用测试的部分优势在于,它迫使您最小化依赖关系,从而创建可测试的代码。通过最小化依赖关系,您将提高代码的可维护性和可重用性,这两个都是非常理想的特性。

    由于您将测试引入现有的代码库,您无疑会遇到许多难以测试的情况,这些情况需要重构才能正确测试。这种重构将增加代码的可测试性,同时减少依赖关系。

    很难用测试改造代码的原因是为什么许多人提倡遵循测试驱动开发。如果您先编写测试,然后编写代码以通过测试,则默认情况下您的代码将更具可测试性和解耦性。

    【讨论】:

      【解决方案2】:

      使用模拟框架为您的类存根。模拟框架(我在 C#/.NET 中使用 Rhino Mocks)可以很容易地去除你的依赖。与依赖注入一起使用,您的类可以很容易地与其他类分离,并且不需要太多工作就可以做到这一点。请注意,有时“伪造”某些东西确实比模仿更容易。我最终伪造了一些类,但通常这些类很容易编写——它们只包含“好”或“坏”数据并返回它。不涉及逻辑。

      【讨论】:

        【解决方案3】:

        我没有跟上整个单元测试的速度,但我认为每个单元都应该代表某种测试。一个测试用例应该测试一些程序。一个过程可能不局限于一个类。

        例如,如果您的应用程序可以分解为原子组件,那么每个组件以及组件之间的每个适用的转换链都可能存在一个测试用例。

        【讨论】:

          【解决方案4】:

          在我们的项目中,开发人员从一开始就有责任编写和维护存根。

          尽管存根需要时间和金钱,但单元测试提供了一些不可否认的优势(您也同意了)。它可以实现测试过程的自动化,减少发现应用程序更复杂部分中包含的错误的难度,并且由于对每个单元都给予了关注,因此通常会提高测试覆盖率。

          根据我的经验,我发现在我们将单元测试置于低优先级的项目中,在后期阶段,单个更改要么会破坏事情,要么需要额外的测试。

          单元测试增加了我作为软件开发人员的信心。

          【讨论】:

            【解决方案5】:

            我更喜欢对功能进行单元测试,这些功能可能对应于个别类,也可能不对应。在我看来,这种单元测试的粒度在测试所需的工作、客户的反馈(因为功能是他们为之付出的代价)和单元测试的长期效用(体现需求)之间做出了更好的权衡,说明操作案例)

            因此,很少需要模拟,但有些测试跨越多个类

            【讨论】:

              【解决方案6】:

              但我的问题是大型项目 每个类都依赖于许多 其他课程你怎么学习单元 测试类?

              首先,您明白“每个类都依赖于许多其他类”是一件坏事,对吧?而且这不是一个大型项目的功能,而是糟糕的设计? (这是 TDD 的好处之一,它倾向于阻止这种高度耦合的代码。)

              剔除所有其他类不会 很有意义,因为两者 复杂性和所需的时间 写存根。

              更不用说它对设计问题没有帮助。更糟糕的是,对所有存根的投资实际上可能会阻碍重构,即使只是心理上的阻碍。

              更好的方法 (恕我直言) 是从内到外的类开始,在进行时更改设计。通常我会通过我所说的“内部依赖注入”来解决这个问题。这涉及保持方法签名不变,但从依赖项中提取所需的数据,然后提取保存实际逻辑的函数。一个简单的例子可能是:

              public int foo(A a, B b) {
                C c = a.getC();
                D d = b.getD();
              
                Bar bar = calculateBar(c, d);
              
                return calculateFoo(bar, this.baz);
              }
              
              Bar calculateBar(C c, D d) {
                ...
              }
              
              int calculateFoo(Bar bar, Baz baz) {
                ...
              }
              

              现在我可以为 calculateBar 和 calculateBaz 编写测试,但我无需担心设置内部状态 (this.baz),也无需担心创建 A 和 B 的模拟版本。

              在使用现有接口创建这样的测试后,我寻找机会将更改推送到接口,例如将 Foo 方法更改为采用 C 和 D 而不是 A 和 B。

              这些都有意义吗?

              【讨论】:

                【解决方案7】:

                你说得对,对你要测试的类所依赖的所有类进行存根是不值得的。如果您更改接口,您也必须维护您的模拟。

                如果您正在测试的类使用的类已经过测试,那么可以跨多个类进行测试。

                有时模拟对象更简单:何时

                • 对象是或使用外部资源,例如数据库或网络连接、磁盘
                • 对象是一个 GUI
                • 对象 [尚未] 可用
                • 对象行为不是确定性的
                • 对象的设置成本很高

                【讨论】:

                  【解决方案8】:

                  我觉得你可能需要重构以降低复杂性。

                  我发现重构类(即分解它们)非常有帮助,因此每个类都有一个单一的职责(不太复杂)。然后你可以对这个职责进行单元测试。您可以使用模拟框架轻松地模拟和初始化依赖项。我发现FakeItEasy 非常好并且易​​于使用,你可以找到很好的例子

                  【讨论】:

                    【解决方案9】:

                    以下原则对我们来说既可用于可测试性,又可用于大型企业项目中的稳健设计:

                    1. 通过类构造函数使依赖项显式化。

                    2. 不要将具体类作为依赖项,而是创建定义所需功能的接口,并将这些接口作为类构造函数的依赖项。这也允许您在需要时应用依赖注入模式。

                    3. 使用现有框架之一,如 Moq,因此您无需编写接口的完整测试实现,但在运行时,您可以从单元测试内部创建 Moq 对象。

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 2022-08-22
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多