如何使用TDD处理无法达到的异常

3

我还在努力理解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%,直到我有更多的类来提高我的总体平均覆盖率。但在我这样做之前,是否有什么我错过的东西?

可能是你是否只应该模拟自己拥有的类型?的重复问题。 - tsolakp
我很好奇,是谁制定了这些“规则”?根据我所处的情况,我会随意模拟任何东西。当我在进行(单元)测试时遇到困难时,通常意味着应该重构一些东西。在你的情况下,你可以用一个方法包装那个依赖项,并模拟那个方法来生成测试所需的行为。如果你能给我展示一个好的理由,说明这是一个不好的想法,我会感谢你教给我新的东西! - Nir Alfasi
1
这确实是一个公正的论点,但它将我们带到了完全不同的领域:当你有一个依赖时,你也应该对其进行集成测试,这些测试应该在合同(API)更改时失败。 - Nir Alfasi
1
顺便提一下,拥有对任何依赖项的(通常是薄的)包装器是一个好习惯,添加这个抽象层可以在你决定切换到另一个库时节省大量工作。其次,我们的优势在于 - 现在你正在模拟你的包装器 - 而不是库! - Nir Alfasi
1
这是一个很好的观点。这就是我考虑我的包装类的原因。但它可能不够轻巧,这让我陷入了麻烦。测试驱动开发能够提出更好的设计...想象一下! :) - GridDragon
显示剩余3条评论
3个回答

4
大多数编程方面一样,测试需要思考。TDD是一个非常有用但肯定不充分的工具,可帮助您获得良好的测试。如果您经过深思熟虑的测试,则可以预期覆盖率在80%或90%以上。我会怀疑任何100%之类的内容-这将闻起来像是有人编写测试以使覆盖范围满意,而不考虑他们正在做什么。-- Martin Fowler, 2012 对于程序的核心部分,100%的覆盖率可能是一个可以实现的目标。毕竟,您从未引入没有需要它的失败测试的代码,而“重构”不应该引入未使用的分支。但是,与边界交互的代码...100%的覆盖变得更加昂贵,您最终会到达临界点:
我靠能正常工作的代码获得报酬,不是测试,因此我的理念是尽量少地测试以达到一定的信心水平…-- Kent Beck, 2008 如果KeyStoreException未被检查,您将不会使用此特定示例;检查异常的问题在于Java中有一些独特之处。
David Parnas 在1972年写道,给了我们一些关于如何解决这个问题的提示。您可以设计解决方案来隐藏您决定使用java.security.KeyStore的决策。换句话说,您创建一个描述您希望密钥库具有的API的接口,并编写所有代码以符合该接口。只有实现需要知道异常管理的细节;只有您的实现需要知道您决定KeyStore异常是不可恢复的。
同样的想法的另一种思考方式是:我们试图将代码分成两堆:核心包含易于测试的复杂代码;边界包含难以测试的简单代码。您边界代码的准则是Hoare:“非常简单,显然没有缺陷”。
使用适用于每种情况的测试覆盖启发式方法。
我的暂时解决方案是将配置的90%代码覆盖率限制降低到40或50%,直到我有更多的类来提高我的总体平均覆盖率。使用棘轮防止覆盖率统计数据回归是一个好主意。

0
我所了解的TDD宣传的是100%代码覆盖率,但这是一个常见的误解。当我们进行TDD时,我们的目标不是追求100%的代码覆盖率,而是追求100%的需求覆盖率。并且,100%的代码覆盖率并不意味着100%的需求覆盖率,反之亦然...不幸的是,我们无法衡量需求覆盖率。因此,唯一获得它的方法就是始终坚持使用TDD。
在这种情况下,根据文档,如果密钥库未初始化,则会抛出KeyStoreException异常。我正在进行防御性编码,并保证此时密钥库将被初始化。因此,KeyStoreException不可能被抛出。
在这种情况下,TDD如何实现神话般的100%覆盖率?
我可以模拟KeyStore,但Mockito的建议是“不要模拟您不拥有的类型”。
单元测试验证“您”的单元行为“独立于其他单元”,这意味着任何其他单元“您”的测试代码需要协作的都需要被替换为“测试替身”。有一个讨论,我们应该以哪个详细级别切割我们的“单元”,但是“您不拥有的类型”绝对不是“您”的代码的一部分。不模拟它们意味着您的测试依赖于此外部代码。如果失败,您不知道“您”的代码是否有问题或外部代码是否有问题。
因此,“不要模拟您不拥有的类型”是一个相当糟糕的建议。

0

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

除非编写代码通过一个失败的单元测试,否则不允许编写任何生产代码。

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接