只做需要通过的事情
在编写代码时,很容易想到如何使用方法的所有可能性,并编写代码来立即处理每种可能性。随着经验的积累,这变得更加容易,并且通常被视为编写健壮代码的好方法,而不会忘记处理不同的用例或错误条件。
我敦促你减少你一次写下所有这些的渴望。相反,只做通过测试所需的事情。然后,当您考虑其他用例时,请为每个用例编写一个测试,然后再扩展代码来处理它们。这同样适用于错误情况。当您考虑应该添加的一些新错误处理时,请编写一个测试,该测试将导致在代码中处理该错误条件之前出现。
为了了解如何完成此操作,让我们扩展测试库以允许预期的异常。我们现在有两个测试用例:
#include"../Test.h"
TEST("Test can be created") {
{
TEST ("Test with throw can be created")
{
throw 1;
}
第一个确保可以创建测试。它什么都不做就过去了。第二个测试引发异常。它实际上只是抛出一个简单的 int 值 1。这会导致测试失败。看到一个或多个测试失败似乎令人沮丧。但请记住,我们刚刚进行了测试,这是您应该感觉良好的成就。
目标是确保可以添加多个测试。抛出 int 以确保任何异常都将被视为失败。我们还没有准备好完全处理抛出的异常。这就是现在要做的。
我们将采用抛出异常的现有代码并将其转换为预期的异常,但我们将遵循此处给出的建议,并做绝对最少的工作。这意味着我们不会直接跳入尝试抛出多个不同异常的解决方案,并且我们还没有处理我们认为应该抛出异常但没有抛出的情况。
因为我们在编写测试库本身,所以我们有时关注的是测试本身。在许多方面,测试变得类似于您将使用的任何特定于项目的代码。因此,虽然现在我们需要注意不要一次添加一堆测试,但稍后您需要小心不要添加一堆尚未立即完成所有测试的额外代码。一旦我们将测试库升级到功能更完整的版本,然后开始使用它来创建日志记录库,您就会看到这种转变。此时,该指南将应用于日志记录库,我们希望避免添加额外的逻辑来处理不同的日志记录方案,而无需先添加测试来执行这些方案。
从最终用法开始,我们需要考虑当出现预期异常时,TEST 宏用法应该是什么样子。我们需要传达的主要内容是我们期望抛出的异常类型。
只需要一种类型的异常。即使某些受测代码引发多个异常类型,我们也不希望每个测试列出多个异常类型。这是因为,虽然代码可以检查不同的错误条件并为每个错误引发不同的异常类型,但每个测试本身都应该编写为仅测试其中一个错误条件。
如果您的方法有时会引发不同的异常,那么您应该对导致每个异常的每个条件进行测试。每个测试都应该是特定的,并且始终导致单个异常或根本不导致异常。如果测试期望抛出异常,则应始终抛出该异常,以便将测试视为通过。
目前,我们只想做需要做的事情。以下是新用法的外观
TEST_EX("可以创建带有throw的测试", int)
{
throw 1;
}
您会注意到的第一件事是,我们需要一个新的宏来传递预期引发的异常类型。我称之为TEST_EX,代表测试异常。紧跟在测试名称后面的是预期异常类型的新宏参数。在本例中,它是一个 int,因为代码抛出 1。
为什么我们需要一个新的宏?
因为宏并不是真正的函数。它们只是使用简单的文本替换。我们希望能够区分不希望引发任何异常的测试与期望引发异常的测试之间的区别。宏不能像方法或函数那样重载,每个不同的版本都使用不同的参数声明。在编写宏时需要牢记特定数量的参数。
当测试不希望引发任何异常时,为异常类型传递一些占位符值没有任何意义。最好有一个宏只采用名称并表示不需要异常,另一个宏采用名称和异常类型。这是设计需要妥协的真实例子。理想情况下,不需要新的宏。我们在这里用语言给我们的东西做得最好。宏是一种古老的技术,有自己的规则。
现在尝试构建没有真正的意义。这时我们将采用捷径并跳过实际构建。事实上,在我的编辑器中,int 类型已经突出显示为错误。
它提示我们错误地使用了关键字,对您来说也可能看起来很奇怪。不能只将类型(无论它们是否为关键字)作为方法参数传递。请记住,宏并不是真正的方法。一旦宏完全展开,编译器将永远不会看到 int 的这种奇怪用法。可以将类型作为模板参数传递。但宏也不支持模板参数。
现在我们有了预期的用法,下一步是考虑启用这种用法的解决方案。我们不希望测试作者必须为预期的异常编写 try/catch 块。这就是测试库应该做的。这意味着我们需要在 Test 类中有一个具有 try/catch 块的新方法。此方法可以捕获预期的异常并暂时忽略它。我们忽略它,因为我们期待异常,这意味着如果我们捕获它,那么测试应该通过。如果我们让预期的异常在测试之外继续,那么 runTests 函数将捕获它并报告由于意外异常而导致的失败。
我们希望将捕获全部保留在 runTests 中,因为这是我们检测意外异常的方式。对于意外的异常,我们不知道要捕获哪种类型,因为我们希望准备好捕获任何内容。
在这里,我们确实知道预期什么类型的异常,因为它是在TEST_EX宏中提供的。Wle 可以让 Test 类中的新方法捕获预期的异常。我们把这个新方法称为runEx。runEx 方法需要做的就是查找预期的异常并忽略它。如果测试抛出其他东西,那么 runEx 不会捕获它。但是 runTests 函数肯定会抓住它。
让我们看一些代码来更好地理解。下面是 Test.h 中TEST_EX宏:
#define TEST_EX(testName, exceptionType)\
class MERETDD_CLASS: public MereTDD::TestBase\
{\
public:\
MERETDD_CLASS (std::string_view name)\
:TestBase(name)\
{\
MereTDD::getTests().push_back(this);\ }\
void runEx () override\ {\
try\ {\
run();\ }\
catch (exceptionType const &)\ {\
}\
}\
void run () override;\
};\
您可以看到,runEx 所做的只是在 try/catch 块中调用原始运行方法,该块捕获指定的 except ionType。在我们的特定情况下,我们将捕获一个 int 并忽略它。所有这些工作都是使用 try/catch 块包装 run 方法,这样测试作者就不必这样做了。runEx 方法也是虚拟重写。这是因为 runTests 函数需要调用 runEx 而不是直接调用 run。只有这样,才会捕获预期的异常。我们不希望 runTests 有时为具有预期异常的测试调用 runEx,并为没有预期异常的测试调用 run。如果 runTests 总是调用 runEx,那就更好了。
这意味着我们需要有一个 runEx 的默认实现,它只是调用运行而没有 try/ catch 块。我们可以在测试基类中执行此操作,无论如何都需要声明虚拟 runEx 方法。run 和 runEx 方法在 TestBase 中如下所示:
virtual void runEx ()
{
run(); }
virtual void run () = 0;
预期异常的TEST_EX宏将覆盖 runEx 以捕获异常,而不希望出现异常的 TEST 宏将使用基本 runEx 类实现,该实现仅直接调用 run。
现在,我们需要修改 runTests 函数来调用 runEx 而不是 run,像这样:
inline int runTests (std::ostream & output)
{
output << "Running"
<< getTests().size()
<<" tests\n";
int numPassed = 0;
int numFailed =0;
for (auto * test: getTests())
{
output <<"-----------\n"
<< test->name()
<< std::endl;
try
{
test->runEx(); }
catch (...)
{
test->setFailed("Unexpected exception thrown.");
}
此处仅显示 runTests 函数的前半部分。函数的其余部分保持不变。实际上,它只是try块中现在调用runEx的单行代码需要更新。现在,我们可以构建项目并运行它以查看测试的执行情况。输出如下所示:
第二个测试曾经失败,但现在它通过了,因为异常是预期的。
只需要 wvhat 即可通过。
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 N 天,点击查看活动详情”