從Google Test 轉到 Catch

前言

如果你見過我,你可能會知道我是自動化測試的忠實信徒。即使對於小型項目,我也傾向於在早期實施一些測試,對於大型項目,我認爲測試是絕對必要的。我可以花很長時間來講爲什麼測試很重要,而你應該這樣做,但這不是今天的主題。相反,我將介紹爲什麼我將所有單元測試從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非常易於集成,測試簡潔易讀,編譯時間和運行時性能都不會成爲我的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章