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