【问题标题】:How to handle unreachable Exceptions with TDD如何使用 TDD 处理无法访问的异​​常
【发布时间】:2017-11-11 02:58:53
【问题描述】:

我仍在思考 TDD 的部分内容。我有一个正在编写的新库,所以这似乎是一个尝试的好机会。

我在 TDD 上读到的内容宣传 100% 的代码覆盖率,但这似乎有点象牙塔,所以我将 JaCoco 配置为要求 90% 的代码覆盖率给我一些喘息的空间。

我开始编写加载 KeyStore 的代码。有很多样板代码和大量检查异常。所以从这里开始让我的生活更轻松。一切看起来都很好,我的测试通过了。但代码覆盖率仅为 49%。浏览代码,除了我称之为“不可能的异常”之外的所有内容都包括在内:

public void saveKey(Key key, String alias) {
    KeyStore.SecretKeyEntry entry = new KeyStore.SecretKeyEntry(new SecretKeySpec(key.getMaterial(), "AES"));

    try {
        keyStore.setEntry(alias, entry, new KeyStore.PasswordProtection(password));
    } catch (KeyStoreException e) {
        throw new UnexpectedErrorException("Failed to save the key", e);
    }
}

在这种特殊情况下,根据文档,如果 keyStore 尚未初始化,则会引发 KeyStoreException。我在进行防御性编码,并保证此时将初始化 keyStore。所以 KeyStoreException 不能被抛出。但这是一个检查异常,所以我必须处理它,所以我将它包装在自定义 RuntimeException 中。

问题是我无法在单元测试中触发此错误。事实上,我已尽我所能确保它不会发生。

在这种情况下,TDD 如何实现神话般的 100% 覆盖率?

我可以模拟 KeyStore,但 Mockito 的建议是“不要模拟你不拥有的类型”。所以我宁愿不要。此外,KeyStore 依赖于 Mockito 没有帮助的几个静态方法,我不想为简单的案例引入 PowerMock,而且我不相信在问题上投入更多的库是一个理想的解决方案。

所以:

  • TDD 的 100% 代码覆盖率是神话吗?
  • 是否有一种技术可以通过代码分析来识别此代码已被覆盖?

我目前预期的解决方案是将我配置的 90% 代码覆盖率限制降低到 40% 或 50%,直到我有更多类来提高我的总体平均覆盖率。但在我这样做之前,我还缺少什么吗?

【问题讨论】:

  • 在单独的方法中粘贴以下行:KeyStore.SecretKeyEntry entry = new KeyStore.SecretKeyEntry(new SecretKeySpec(key.getMaterial(), "AES")); 并模拟该方法以抛出 KeyStoreException
  • 这确实是一个公平的论点,但它把我们拖到了完全不同的地方:当你有一个依赖项时,你还应该对它进行集成测试,当合约 (API) 发生变化时,这些测试应该会中断。跨度>
  • 顺便说一句,在您拥有的任何依赖项周围有一个(通常很薄的)包装器是一个好习惯,例如,当您决定切换到另一个库时,添加这一抽象层可以为您节省大量工作.第二个优势,是我们的例子——现在你在嘲笑你的包装器——而不是库!
  • 这是一个很好的观点。这就是我正在考虑的包装类。但它可能不够薄,这给我带来了麻烦。 TDD 带来了更好的设计……去看看吧! :)

标签: java unit-testing tdd


【解决方案1】:

与编程的大多数方面一样,测试需要深思熟虑。 TDD 是一个非常有用但肯定不够的工具,可以帮助您获得良好的测试。如果您的测试经过深思熟虑,我预计覆盖率会在 80 年代或 90 年代以上。我会怀疑 100% 之类的东西——它会闻到有人编写测试以使覆盖率数字满意,但不考虑他们在做什么。 -- Martin Fowler, 2012

对于程序的核心,100% 的覆盖率可能是一个可以实现的目标;毕竟,你永远不会在没有失败测试的情况下引入代码,并且“重构”不应该引入未使用的分支。

但是与边界交互的代码... 100% 覆盖会变得更加昂贵,并且您最终会达到临界点:

我是通过有效的代码而不是测试获得报酬,所以我的理念是尽可能少地进行测试以达到给定的信心水平... -- Kent Beck, 2008

如果未选中 KeyStoreException,您就不会使用这个特定示例;检查异常的问题在某种程度上是 Java 独有的。

David Parnas,writing in 1972,给了我们一些关于如何解决这个问题的提示。您可以设计您的解决方案,以便隐藏您使用java.security.KeyStore 的决定。换句话说,您创建一个接口来描述您希望密钥库拥有的 API,并将所有代码写入该接口。只有实现需要知道异常管理的细节;只有您的实现需要知道您决定 KeyStore 异常是不可恢复的。

对同一想法的另一种思考方式;我们试图将代码分成两堆:core 包含易于测试的复杂代码; 边界 包含难以测试的简单代码。您的边界代码试金石是 Hoare:“如此简单,显然没有缺陷”。

使用适合每种情况的测试覆盖启发式。

我目前预期的解决方案是将我配置的 90% 代码覆盖率限制降低到 40% 或 50%,直到我有更多类来提高我的总体平均覆盖率。

使用棘轮来防止覆盖率统计回归是一个好主意。

【讨论】:

    【解决方案2】:

    我在 TDD 上读到的内容宣传 100% 的代码覆盖率。

    这是一个常见的误解。

    当我们进行 TDD 时,我们以 100% 代码覆盖率为目标。我们的目标是实现 100%需求覆盖。不:100% 的代码覆盖率确实 意味着 100% 的需求覆盖率,反之亦然...

    很遗憾,我们无法衡量需求覆盖率。因此,获得它的唯一方法就是执行 TDD。


    在这种特殊情况下,根据文档,如果 keyStore 尚未初始化,则会引发 KeyStoreException。我在进行防御性编码,并保证此时将初始化 keyStore。所以无法抛出 KeyStoreException。

    在这种情况下,TDD 如何实现神话般的 100% 覆盖率?

    我可以模拟 KeyStore,但来自 Mockito 的建议是“不要模拟你不拥有的类型。”

    UnitTests 验证你的单元的行为独立,这意味着任何其他单元你的被测代码需要被替换为测试双打。那里有一个讨论,我们应该在哪个细节级别削减我们的单位,但“你不拥有的类型”绝对是的一部分你的代码。不嘲笑它们意味着你的测试依赖于这个外部代码。如果失败,您不知道您的代码是否有问题或外部代码。
    因此 “不要模拟你不拥有的类型。” 是一个相当糟糕的建议。

    【讨论】:

      【解决方案3】:

      实际上 TDD 与代码覆盖率无关。
      TDD 的规则之一是

      您不得编写任何生产代码,除非是为了制作 失败的单元测试通过。

      因此,如果您练习测试驱动开发并遵循上述规则,那么您将始终拥有 100% 的代码覆盖率。

      代码覆盖率是单元测试的衡量工具,用于在编写生产代码后编写测试的情况。通过代码覆盖,您可以“检查”您是否忘记为逻辑中的某些情况编写测试。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-11-19
        • 2012-08-09
        • 2013-09-22
        • 2017-03-04
        • 2020-09-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多