在两者都适用的情况下,端到端测试是否比单元测试更好?

Are end-to-end tests better than unit tests, in situations where both are applicable?

首先让我定义单元测试和端到端测试的含义。假设您有一个包含一堆 Java classes 的程序:A 调用 B,B 调用 C,依此类推。

单元测试是A模拟B的测试,B模拟C的测试,依此类推。

端到端测试是针对 A 的测试,它测试 A 并传递测试 B 和 C。

为简单起见,并使讨论集中在手头的主题上而不是被次要细节分散注意力,我们假设整个系统是无状态的:您使用输入调用顶层 (A) ,你会得到一个输出。给定的输入只有一个有效输出。

需要说明的是,我这里不包括外部系统,例如到其他服务器的 RPC、数据库、文件系统等外部状态、任何类型的 UI ("assert that programmatically tapping the Delete button deletes the current document") 等。我们只是在谈论关于同一进程中的一堆 classes。

现在,可以采取两种方法:

  1. 编写端到端测试,尝试涵盖所有可能的输入和状态。仅在需要时编写单元测试,例如端到端测试未对特定 class 进行充分测试,或者端到端测试失败并且您发现编写单元测试很有帮助定位错误。但总的来说,目标是进行彻底的端到端测试。

  2. 编写单元测试以详尽地测试每个 class 或组件。事后写一个端到端测试,或者根本不写。即使你写了,也不要试图穷尽所有可能的输入。

我更喜欢 (1),因为如果端到端测试通过并且详尽无遗,我 知道 整个系统适用于我的所有情况测试。然而,如果每个 class 或组件都正常工作,它们之间的集成点仍然可能存在错误,这是我读到的大多数错误发生的地方(抱歉,我现在没有参考)。

那么,哪一个更适合您——进行全面的端到端测试,还是进行全面的单元测试?为什么?请给出具体原因,以便我和其他读者可以自己评估答案。

如果这个问题更适合 programmers.stackexchange.com,请将它移到那里(版主)。

没有正确答案(这就是为什么我投票关闭主要基于意见)。最好的方法主要是视情况而定,并且在某种程度上取决于您选择如何定义一个单元。根据您对问题域的描述,您有 class A,调用 class B,调用 class C。您将所有 classes 视为单元。这是一种方法,但是 it’s not the only approach。您可以选择将模块视为一个单元。如果 class A 是模块的 public 接口的一部分而 classes B+C 不是,那么您就没有理由必须进行专门针对的测试classes B+C,因为可访问功能将通过构成 public 界面的其他 classes 进行测试。

第一种方法的主要问题是它的可扩展性。我将用一些非常简单的代码进行演示(它是用 C# 编写的,但我相信你可以翻译)。

public class C {
    public int Method3(int cParam) {
        if (cParam > 0) {
            return cParam + 1;
        }
        else {
            return cParam - 1;
        }
    }
}

public class B {
    C _cInstance = new C();
    public int Method2(int bParam, int cParam) {
        if (bParam > 0) {
            return bParam + 1 * _cInstance.Method3(cParam);
        }
        else {
            return bParam - 1 * _cInstance.Method3(cParam);
        }
    }
}

public class A {
    B _bInstance = new B();
    public int Method1(int aParam, int bParam, int cParam) {
        if (aParam > 0) {
            return aParam + 1 * _bInstance.Method2(bParam, cParam);
        }
        else {
            return aParam - 1 * _bInstance.Method2(bParam, cParam);
        }
    }
}

要使用基于 class 的单元测试来测试所有分支,每个 class 中至少要测试 2 个分支,因此您有 2+2+2=6 个测试个案。如果您使用方法 1,通过 class A 对所有 classes 进行详尽测试,那么您不需要将每个操作的分支相加,而是将其相乘,因此您将拥有2*2*2=8。在这种情况下没有太大区别,但如果您进行了额外的验证,例如参数的 MAX/MIN int 值和 0,您将为每个值测试 5 个场景,这使您接近 15 对 125 个场景。您通过高水平测试的 branches/values 越多,您的测试场景成倍增加的速度就越快。现在考虑如果 class C 中的操作很慢(比如 1 秒)的情况。对于选项 2,这会影响 2 个测试,对于选项 1,这会影响所有 8 个测试,测试开始变得痛苦。如果某些需求发生变化并且创建了 class D 会怎样?它还使用 class B,随后使用 class C。对于选项 1,您需要再次重新定位所有不同的值以使用 class B+C。您实际上是在为同一件事再次编写测试。使用选项 1 的测试现在真的开始受到伤害,运行 测试速度慢得令人痛苦,而且甚至没有考虑攻击任何外部系统。使用选项 2,您模拟 B 并仅测试 D 的功能。

根据我的经验,如果你沿着路线 1 走,那么你很可能会创建太多不必要的测试,这会使你的测试套件变慢 and/or 你将开始丢失场景。

我要将选项 2 重写为

Write unit tests that test each class or component exhaustively. Write appropriate end-to-end tests to validate data flows through the system as expected. Don't try to exhaustively test all possible inputs.

正如我所说,对于您的特定情况,将 classes A、B 和 C 视为单个 unit/component 可能是合适的。但如果不是,那么单独测试单个 classes 有助于控制需要测试的场景数量,从而控制彻底测试所需的工作量。它还有助于减少编写测试的个人需要记住的问题域。查看被测 class 中的分支比尝试记住您正在测试的 class 调用的所有代码中的所有可能分支要容易得多。

我修改选项 2 的原因是将单元测试作为您的主要关注点并不意味着您会自动将端到端测试减少到第二个 class 公民,但他们确实如此有不同的工作。例如,如果您要写入数据库,您可能会进行单元测试以验证您不会尝试写入超过某个值的字符串,然后进行集成测试以验证您是否可以写入那么长的字符串。两者协同工作,并非孤立无援。

我会考虑未来的变化和复杂性来决定选择哪一个:

  • 如果 A 的行为可能是固定的(说它是一个 API),而 B 和 C 在不久的将来可能是 replaced/changed(说它们是使用外部库的内部组件接近它的新发布日期),那么我将对 A 进行端到端测试。
  • 如果 A 的端到端测试会产生非常详尽的测试用例列表,或者如果测试用例太多难以构建,我会选择 A、B 的单元测试和 C.

我发现自己经常混合使用端到端测试和单元测试。端到端测试涵盖关键案例(相当于所有参与 类 的大约 60-70% 的覆盖率),而单元测试涵盖其余部分(例外情况,非常 rare/deep 执行路径),或每当我想对自己的逻辑更有信心时,我都会加入。

虽然不可能为此类问题提供一般性答案,但根据经验,您应该考虑 Test Pyramid:

  • 一些系统测试
  • 一些集成测试
  • 很多单元测试

原因是 outlined by J.B. Rainsberger,但其要点是对于任何相当复杂的应用程序,覆盖所有可能行为的 组合爆炸 会阻止有效覆盖除了单元测试之外的任何东西。您必须编写数万或数十万个集成测试才能知道您的系统是否正常工作。