如何检测测试是通过还是失败

484 阅读7分钟

如何检测测试是通过还是失败

我们将创建的测试与创建测试有很大不同,因此它们应该有自己的文件。

在编写自己的测试时,您还需要将它们组织到多个文件中。

让我们创建一个名为 Conf irm 的新文件。cpp 并将其放在测试文件夹中。使用新文件,项目结构将如下所示:

MereTDD project root folder

	Test.h

	tests folder 

		main.cpp

		Confirm.cpp

		Creation.cpp

然后,将单个测试添加到新文件,使其如下所示:

#include"../Test.h"

TEST("Test will pass without any confirms") {

} 

这两个测试之间唯一真正的区别是名称。我们真的需要另一个完全相同但名字不同的测试吗?关于添加具有相同功能但名称不同的代码的争论,我可以站在任何一方。有些人可能会看到这一点,并认为这是纯代码复制。

对我来说,区别在于意图。是的,现在两个测试都是一样的。但谁知道以后是否会对其中一个或两个进行修改呢?如果这种情况真的发生了,我们是否能够记住,一个测试服务于多种目的?

我强烈建议您编写每个测试,就好像它是您的代码和测试旨在防止的 bug 之间的唯一屏障一样。或者,测试正在执行特定的用法,以确保在设计更改期间没有任何损坏。有两个相同的测试是可以的,只要他们测试的是不同的东西。这个目标应该是独一无二的。

在这种情况下,原始测试只是确保可以以最基本的形式创建测试。新的测试明确地确保一个空测试能够通过。这是两个不同的测试,只是碰巧需要相同的测试方法体来完成它们的目标。

现在我们在项目中有了一个带有新测试的新文件,让我们构建并确保一切按预期工作。失败了。生成失败的原因如下:

1d: 5 duplicate symbols for architecture x86_64

一切编译正常,但项目未能链接到最终可运行的可执行文件。我们有五个链接错误。其中一个链接错误显示如下内容

duplicate symbol 'Test3::run()'

我只列出了一个链接器错误,因为它们都是相似的。问题是我们现在有两个 Test3的声明。每个文件创建一个声明。Cpp 和确认。这是因为 TEST 宏根据 TEST 宏出现在源文件中的行号用唯一的编号声明 TEST 类。这两个文件碰巧都在第3行使用了 TEST 宏,因此它们都声明了一个名为 Test3的类。

解决方案是在宏中声明类时使用未命名的命名空间。这仍然会创建两个类,比如 Test3,但是每个类都在一个名称空间中,该名称空间不会扩展到声明它的.cpp 文件之外。这意味着测试类可以继续基于行号,行号保证在每个行中是唯一的。Cpp 文件,现在将不再与任何其他测试冲突,这些测试碰巧在不同的Cpp 文件。

我们需要做的就是修改 TEST 和TEST_EX宏,在每个宏内部的类声明周围添加一个未命名的命名空间。我们不需要将命名空间扩展到宏的末尾,因为宏会继续声明 run 方法的开头。幸运的是,run 方法声明不需要在命名空间内。否则,我们将不得不弄清楚如何在完全定义 run 方法后用右大括号结束命名空间。实际上,我们可以在类声明之后结束命名空间。TEST 宏如下所示:

#define TEST( testName )\

namespace{\

class MERETDD_CLASS : public MereTDD::TestBase\ {\

public:\

MERETDD_CLASS (std::string_view name)\ :TestBase(name)\

{\

MereTDD::getTests().push_back(this);\ }\

void run () override;\ };\

} /* end of unnamed namespace*/\

MERETDD_CLASS MERETDD_INSTANCE(testName);\

 void MERETDD_CLASS::run ()

TEST EX宏需要一个类似的未命名名称空间,如下所示:

#define TEST_EX( testName,exceptionType)\ namespace{\

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 &)\ {\

return;\ } \

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

void run () override;\ };\

} /* end of unnamed namespace*/\

MERETDD_CLASS MERETDD_INSTANCE(testName);\ 

void MERETDD_CLASS::run ()

现在项目再次生成,运行它将显示新的测试。根据链接器生成最终可执行文件的顺序,你可能会发现新的测试在前面的测试之前或之后运行。以下是我运行测试项目时的部分结果:

image.png

没有显示其他五个测试和摘要。以六个测试结束,我们只是增加了一个测试,使总共七个测试。重要的部分是新测试运行并通过。现在我们可以想想确认会是什么样子。确认一些事情意味着什么?

运行测试时,不仅要验证测试是否完成,还要验证测试是否正确完成。它还有助于检查整个过程,以确保一切按照预期运行。可以通过比较从测试的代码中获得的值来做到这一点,以确保它们与预期的值相匹配。

假设您有一个函数,它将两个数相加并返回一个结果。您可以使用已知的值调用此函数,并将返回的和与您自己计算的预期和进行比较。您确认计算的和与预期的和相匹配。如果值匹配,则确认传递。但是如果值不匹配,那么确认就会失败,这也会导致测试失败。一个测试可以有多个确认,并且每个都将被检查以确保它们通过。一旦确认失败,就没有继续测试的必要了,因为它已经失败了。一些 TDD 纯粹主义者会声称一个测试应该只有一个确认。我认为,在只有一个确认和编写试图验证所有内容的大型测试之间,有一个很好的折衷方案。

使用多重确认编写测试的一种流行风格是,即使确认失败,也让测试继续,以此来跟踪有多少确认通过。这种风格有一个好处,因为开发人员有时可以通过一次运行测试来修复多个问题。我们没有采用这种方法,因为我认为这种好处在实践中很少实现。有些人可能会争论这一点,但听我说完。一旦某件事情被证明不能满足你的期望,最有可能的结果就是进一步失败的连锁反应。我很少看到一个设计良好的测试失败一个确认,然后以某种方式恢复通过不相关的确认。如果测试的行为是这样的,那么它通常是测试不相关的问题,并且应该被分解为多个测试。我们要遵循的实践是: 当确认失败时,测试本身也失败了。其他测试可能进行得很顺利。但是没有通过确认的测试已经失败了,没有必要继续观察测试的某些部分是否仍然可行。

在编写测试时,就像编写普通代码一样,最好避免重复。换句话说,如果您发现自己通过检查已经在其他测试中检查过的值来测试相同的内容,那么是时候考虑每个测试的目标了。编写一个包含一些基本功能的测试,这些功能将被多次使用。然后,在使用相同功能的其他测试中,您可以假定它已经被测试并且可以工作,因此不需要通过额外的确认再次验证它。有些代码可能会使这一切更加清晰。首先,让我们考虑如何在不进行确认的情况下验证预期的结果。这是一个我们不能仅仅编写确认代码的时代,因为我们还不知道我们想要它做什么。


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