从Google Test 转到 Catch

本文讲述了作者从使用Google Test转向Catch测试框架的原因和过程。主要原因是Catch的简单设置、无Fixture热水和更具表现力的匹配器。在移植数千个测试的过程中,虽然遇到一些挑战,但整体代码量减少,可读性提高。简洁的报告者也让测试输出更加清晰。
摘要由CSDN通过智能技术生成

前言

如果你见过我,你可能会知道我是自动化测试的忠实信徒。即使对于小型项目,我也倾向于在早期实施一些测试,对于大型项目,我认为测试是绝对必要的。我可以花很长时间来讲为什么测试很重要,而你应该这样做,但这不是今天的主题。相反,我将介绍为什么我将所有单元测试从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?对我来说,主要有这几个原因:

  1. 简单的设置 - 它总是只需要一个头文件,不需要手动组合。
  2. 没有Fix图热水!
  3. 更具表现力的匹配者。

第一个原因应该是显而易见的,但让我详细说明第二个原因。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。现在我需要一些东西,但是:

  1. 移植上几千个测试,包括从Google Test到Catch的数万个测试宏。
  2. 为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非常易于集成,测试简洁易读,编译时间和运行时性能都不会成为我的问题。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值