测试测试库能够捕获这些失败的能力

535 阅读6分钟

测试测试库能够捕获这些失败的能力

如果预期异常的测试看不到异常,会发生什么情况?这应该是一个失败,我们接下来会处理它。这种情况有点不同,因为下一次通过真的会失败。

当您编写测试并按照指导进行操作时,首先执行最少的操作以获得第一次通过的结果,然后增强测试以获得另一次通过,您将专注于通过。这很好,因为我们希望所有测试最终都能通过。

任何失败几乎总是失败。在测试中出现预期失败通常没有意义。我们在这里要做的事情有点不寻常,这是因为我们仍在开发测试库本身。我们需要确保预期但未发生的缺失异常能够作为失败的测试捕获。然后,我们希望将该失败的测试视为通过,因为我们正在测试测试库能够捕获这些失败的能力。

现在,我们在测试库中有一个漏洞,因为添加第三个期望抛出 int 但从未实际抛出 int 的测试被视为通过测试。换句话说,此集合中的测试都通过:

#include"../Test.h"

TEST("Test can be created") {

}

TEST_EX("Test with throw can be created", int) }

throw 1; {

TEST_EX("Test that never throws can be created", int) }

}

构建它工作正常并运行它表明所有三个测试都通过:

image.png

这不是我们想要的。第三个测试应该失败,因为它期望抛出一个 int,但这没有发生。但这也违背了所有测试都应该通过的目标。没有办法出现预期的失败。

当然,我们也许可以将这个概念添加到测试库中,但这会增加额外的复杂性。如果我们要添加测试失败但仍被视为通过的功能,那么如果测试由于某种意外原因失败会发生什么?

编写由于多种原因而失败但实际上被报告为通过的不良测试很容易,因为失败是预期的。

如何验证测试库本身是否可以正确检测缺失的预期异常?

我们需要关闭第三次测试暴露的洞。这个难题没有好的答案。所以,我要做的是让这个新的测试失败,然后添加将失败视为成功的能力。我不喜欢替代方案,即将测试保留在代码中,但将其注释掉,使其实际上不会运行,或者完全删除第三个测试。

最终说服我添加对成功失败测试的支持的是应该测试所有内容的想法,尤其是大事情,例如确保始终抛出预期异常的能力。您可能不需要使用将测试标记为预期失败的功能,但如果这样做,那么您将能够做同样的事情。我们处于一个独特的情况,因为我们需要测试一些关于测试库本身的东西。

好吧,让我们让新测试失败。执行此操作所需的最小代码量是在捕获预期异常时返回。如果没有捕获异常,那么我们扔其他东西。要更新的代码是 runEx 方法的TEST_EX宏重写,如下所示:

void runEx () override\ {\

try\ {\

	run();\ }\

catch (exceptionType const &)\ {\

	return;\ }\

throw 1;\ }\


宏的其余部分保持不变,因此此处仅显示 runEx 覆盖。当捕获预期的异常时,我们返回,这将导致测试通过。在 try/catch 块之后,我们抛出其他会导致测试失败的东西。

如果你觉得看到一个简单的 int 值被抛出很奇怪,请记住,我们的目标是在这一点上做所需的绝对最小值。你永远不会想留下抛出类似内容的代码,我们接下来会修复它。

这很有效,很棒,因为它是做我们想做的事情所需的最低金额,但结果看起来很奇怪且具有误导性。下面是测试结果输出:

image.png

您可以看到我们遇到了失败,但消息显示引发意外异常。这个信息几乎与我们想要的完全相反。我们希望它说没有抛出预期的异常。让我们先解决此问题,然后再继续将其转换为预期的故障。

首先,我们需要某种方式让 runTest s 函数检测意外异常和缺失异常之间的差异。 现在,它只是捕获所有内容,并将任何异常视为意外。如果我们要扔一些特别的东西并首先抓住它,那么这可能是缺少异常的信号。其他任何被抓住的东西都是意想不到的。

这个特殊的投掷应该是什么?最好的东西将是测试库专门为此定义的东西。我们可以为此定义一个新类。

我们称之为 MissingExcept ion,并在 MereTDD 命名空间中定义它,如下所示:

class MissingException {

public:

    MissingException (std::string_view exType) :mExType(exType)

    { }

    std::string_view exType () const {

        return mExType; }

private:

    std::string mExType; };

此类不仅会发出未引发预期异常的信号,而且还会跟踪应该引发的异常类型。在C++编译器理解类型的意义上,该类型将不是真正的类型。它将是该类型的文本表示形式。这实际上非常适合设计,因为这是TEST_EX宏接受的,这是在宏展开时在代码中替换为实际类型的一段文本。

在 runEx 方法的TEST_EX宏实现中,我们可以将其更改为如下所示:

void runEx () override\

{\

try\ {\

    run();\ } \

catch (exceptionType const &)\ { \

    return;\ }\

    throw MereTDD::MissingException(#exceptionType);\ }\


代码现在不再像以前那样抛出一个 int,而是抛出一个 MissingExcept ion。请注意它如何使用宏的另一个功能,即能够使用 # 运算符将宏参数转换为字符串文本。通过将 # 放在 exceptionType 之前,它将把TEST_EX宏用法中提供的 int 转换为“int”字符串文字,可用于使用预期的异常类型名称初始化 MissingException。Wè现在抛出一个可以识别缺失异常的特殊类型,所以剩下的唯一部分就是捕获这个异常类型并处理它。

这发生在 runTests 函数中,如下所示:

try

{

    test->runEx(); }

catch (MissingException const & ex) {

    std::string message = "Expected exception type ";
    
    message += ex.exType();

    message +=" was not thrown.";
    
    test->setFailed(message);

}

catch(...) {

    test->setFailed("Unexpected exception thrown."); 
    
    }

顺序很重要。我们需要先尝试捕捉Mi ssingExcept ion,然后再捕捉其他所有内容。如果我们确实捕获了 MissingException,则代码会更改显示的消息,让我们知道预期但未抛出的异常类型。

现在运行项目会显示更适用于失败的消息,如下所示:

image.png


开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 N 天,点击查看活动详情