单元测试组成

Unit testing composition

我们有一个任务 FooTask。我们正在创建一个 Foo class,从业务登录的角度来看,它有 1 个责任。但事实证明,FooTask 真的很复杂。它由几个 steps/subtasks 组成,然后可以用 classes Bar/Baz 责任来表示。那些 classes 在 Foo class 之外没有实际意义,它们不会被单独使用。所以他们可能会有一个包范围或类似的东西。

所以假设你的 Foo class,代表 FooTask 变成:

public class Foo {
    private final Bar bar;
    private final Baz baz;

    // all-args constructor

    public ReturnType doFooTask(InputParam input) {
        final SubResult subresult = bar.doBarSubTask(input);
        return baz.doBazSubTask(subresult);
    }
}

现在,您将如何测试它?我看到两个选项,但不知道哪个更好:

1) 通过 Foo 的 public API 测试 classes Bar 和 Baz。 我看到了这种方法的一个主要流程:您使用越多的子项目来组成您的对象,就会有越多的测试用例到达。

2) 自己为 classes Bar 和 Baz 编写完整的测试套件(它们是包私有的,所以只要我保持适当的测试包层次结构,我就可以毫无问题地测试它们)。

然后呢?我应该让我的 Foo class 未经测试吗?我应该重复我的测试吗(这导致了我之前的问题 - 很多测试用例,更糟糕的是,复制粘贴)?或者也许我应该模拟所有依赖项并断言,在调用 Foo 的方法时调用了我模拟中的正确方法?

首先为 Bar 和 Baz 编写完整的测试套件。

如果 Foo 真的很微不足道,就像在您的示例中一样,那么您可能不会为它编写测试。但是我不想让可测试的代码未经测试,所以我会为它编写测试。如果您确实为 Foo 编写测试,则只执行测试 Foo 所需的内容。单元测试的方式一般是写Bar和Baz的接口,然后mock它们,然后可以使用一些依赖注入技术将mock传入。如果 Bar 和 Baz 有其他依赖关系,或者尝试接触文件系统或与数据库对话或类似的东西,或者如果它们很复杂并且需要大量设置,这绝对是您想要采用的方法。

有些情况下您不需要这样做。如果 Bar 和 Baz 相当简单并且没有外部依赖项,那么您可以为 Foo 编写一个仅使用 Bar 和 Baz 实例的测试。您编写的这些测试将以确保 Foo 中编写的行为正确运行为中心,并且您编写时要牢记 Bar 和 Baz 正常运行的假设,因为您已经用单元测试覆盖了它们。您不会编写涵盖大量输入和输出组合的测试,因为您已经为 Bar 和 Baz 完成了测试,您只是希望获得 Foo 的分支覆盖并确保它从 Bar 发送正确的信息到 Baz在合适的条件下。如果您模拟了 Bar 和 Baz,那么您将编写相同数量的测试。

如果 Bar 和 Baz 很复杂,需要大量设置或具有必须模拟或初始化的外部依赖项,则模拟它们并使用模拟测试 Foo。此外,如果使用实际对象最终会在单元测试中花费很长时间 运行,则模拟它们。