前言
如果你见过我,你可能会知道我是自动化测试的忠实信徒。即使对于小型项目,我也倾向于在早期实施一些测试,对于大型项目,我认为测试是绝对必要的。我可以花很长时间来讲为什么测试很重要,而你应该这样做,但这不是今天的主题。相反,我将介绍为什么我将所有单元测试从Google Test(我之前使用的测试框架)移至Catch,并阐明了我如何做到这一点。在我们开始讨论之前,让我们回顾一下我是如何进入Google Test以及为什么我想首先改变一些东西。
一个简短的历史
许多月前这篇博文让我对单元测试很感兴趣。鉴于我没有任何经验,并且由于UnitTest ++看起来和任何其他框架一样好,我使用它编写了我的初始测试。这是在2008年左右的某个时候。在2010年,我对UnitTest ++感到有点沮丧,因为开发并不是那么强大,我希望有更多的测试宏用于字符串比较等等。长话短说,我最终将所有测试移植到Google Test。
在当世,Google Test是在 Google Code,上开发的,确实经常版本发布,但不是经常。将Google Test捆绑到单个文件中需要运行一个单独的工具(而且它仍然这样。)。我最终使用Google Test进行了所有测试 - 其中大约有3000个测试,其中包含大量Fixtures。在开发时,我在每个构建上运行单元测试,所以我还写了一个自定义报告者,所以我的控制台输出如下所示:
SUCCESS (11 tests, 0 ms)
SUCCESS (1 tests, 0 ms)
SUCCESS (23 tests 1 ms)
您可能想知道为什么还会记录时间:鉴于每次编译都运行测试,它们运行得更快,所以我总是关注测试时间,如果事情开始变慢,我可以移动它进入一个单独的测试套件。
多年来,这对我很有帮助,但 我对Google Test仍有一些抱怨。首先,很明显这个项目是由谷歌开发的,所以他们的方向 - 死亡测试等 - 并没有让我的生活更简单。与此同时,我的视野里出现了一个新的框架:Catch。
接触Catch
你可能会问为什么要使用Catch?对我来说,主要有这几个原因:
- 简单的设置 - 它总是只需要一个头文件,不需要手动组合。
- 没有Fix图热水!
- 更具表现力的匹配者。
第一个原因应该是显而易见的,但让我详细说明第二个原因。Catch解决“Fixture问题”的方法是在代码中包含包含测试代码的部分,然后每个部分执行一次。
下面是一个小开胃菜:
TEST_CASE("DateTime", "[core]")
{
const DateTime dt (1969, 7, 20, 20, 17, 40, 42, DateTimeReference::Utc);
SECTION("GetYear")
{
CHECK (dt.GetYear () == 1969);
}
SECTION("GetMonth")
{
CHECK (dt.GetMonth () == 7);
}
// 下面或许是更多的内容
}
以上,到配上更好的匹配器(没有更多的ASSERT_EQ宏),你可以使用正常的比较。这足以让我相信Catch。现在我需要一些东西,但是:
- 移植上几千个测试,包括从Google Test到Catch的数万个测试宏。
- 为Catch实现自定义报告器。
移植
由于我是一个相当懒惰的人,并且因为测试格式非常统一,所以我决定半自动化从Google Test到Catch的转换。很有可能可以制作一个完美的自动化工具,至少对于断言,通过在Clang上构建它并进行重构。但我想如果我自动完成80%左右应该仍然没问题。
在你问为什么我没有移植到像Catch这样应该更快的其他框架之前:在我的测试中,Catch足够快,以至于测试开销无关紧要。我可以在不到10毫秒的时间内轻松执行20000个断言,因此“更快”在这一点上并不是真正的论据。
有趣的是,通过转移到Catch,代码行显着减少,其中大部分是因为Fixtures已经消失,而更多的代码现在使用了SECTION宏,我可以合并公共代码。以前,我经常会复制一些小的设置,因为它比写一个Fixture打字更少。使用 Catch这很简单,我最终自愿清理我的测试。为了给你一些想法,这是核心库的提交:114个文件已更改,6717个插入(+), 6885个删除( - )(或-3%)。对于我的几何库,它有更多的设置代码,相对减少相当高:36个文件更改,2342个插入(+), 2478删除( - ) - 5%。这里和那里有几个百分点可能看起来不太重要,但由于较少的样板,它们直接转化为提高了可读性。
在一些极端情况下,Catch的行为与Google Test不同。值得注意的是,带有0 的EXPECT_FLOAT_EQ需要转换为CHECK(a == Approx (0).margin(some_eps)),因为Catch默认使用相对epsilon,当与0比较时变为0。另一个影响STREQ -在Catch中,你需要使用匹配器,它将整个测试转换为CHECK_THAT(str,Catch :: Equals(“Expected str”)); 。
Terse 报告者
最后遗漏的是简洁的报告者。Catch2再次发生了变化,这是目前的稳定版本。报告者是catch-main.cpp的一部分,我将其编译成一个静态库,然后将其链接到测试可执行文件中。简洁的报告者很简单:
namespace Catch {
class TerseReporter : public StreamingReporterBase<TerseReporter>
{
public:
TerseReporter (ReporterConfig const& _config)
: StreamingReporterBase (_config)
{
}
static std::string getDescription ()
{
return "Terse output";
}
virtual void assertionStarting (AssertionInfo const&) {}
virtual bool assertionEnded (AssertionStats const& stats) {
if (!stats.assertionResult.succeeded ()) {
const auto location = stats.assertionResult.getSourceInfo ();
std::cout << location.file << "(" << location.line << ") error\n"
<< "\t";
switch (stats.assertionResult.getResultType ()) {
case ResultWas::DidntThrowException:
std::cout << "Expected exception was not thrown";
break;
case ResultWas::ExpressionFailed:
std::cout << "Expression is not true: " << stats.assertionResult.getExpandedExpression ();
break;
case ResultWas::Exception:
std::cout << "Unexpected exception";
break;
default:
std::cout << "Test failed";
break;
}
std::cout << std::endl;
}
return true;
}
void sectionStarting (const SectionInfo& info) override
{
++sectionNesting_;
StreamingReporterBase::sectionStarting (info);
}
void sectionEnded (const SectionStats& stats) override
{
if (--sectionNesting_ == 0) {
totalDuration_ += stats.durationInSeconds;
}
StreamingReporterBase::sectionEnded (stats);
}
void testRunEnded (const TestRunStats& stats) override
{
if (stats.totals.assertions.allPassed ()) {
std::cout << "SUCCESS (" << stats.totals.testCases.total () << " tests, "
<< stats.totals.assertions.total () << " assertions, "
<< static_cast<int> (totalDuration_ * 1000) << " ms)";
} else {
std::cout << "FAILURE (" << stats.totals.assertions.failed << " out of "
<< stats.totals.assertions.total () << " failed, "
<< static_cast<int> (totalDuration_ * 1000) << " ms)";
}
std::cout << std::endl;
StreamingReporterBase::testRunEnded (stats);
}
private:
int sectionNesting_ = 0;
double totalDuration_ = 0;
};
CATCH_REGISTER_REPORTER ("terse", TerseReporter)
}
使用参数-r terse
开运行测试以选择这个报告者。这会产生想洗面这样的测试报告
SUCCESS (11 tests, 18 assertions, 0 ms)
SUCCESS (1 tests, 2 assertions, 0 ms)
SUCCESS (23 tests, 283 assertions, 1 ms)
作为额外的奖励,它还显示了执行的测试宏的数量。这有助于识别通过一些长循环运行的测试。
结论
移植值得吗?花了一些时间进行新的Catch测试,并在编写了更多的测试后,我仍然相信它是值得的。Catch非常易于集成,测试简洁易读,编译时间和运行时性能都不会成为我的问题。