theme: channing-cyan
*以下內容為本人的學習筆記,如需要轉載,請宣告原文鏈接 微信公眾號「ENG八戒」https://mp.weixin.qq.com/s/RIztusI3uKRnoHVf0sloeg
開發者雖然主要負責工程里的開發任務,但是每個開發完畢的功能都是需要開發者自測通過的,所以經常會聽到開發者提起單元測驗的話題,那么今天我就帶大伙一起來看看大名鼎鼎的谷歌 C++ 測驗框架 GoogleTest,
本文上接《C++ 測驗框架 GoogleTest 初學者入門篇 乙》,歡迎關注公眾號【ENG八戒】查看更多精彩內容,
斷言
什么是斷言?斷言是用來對運算式執行比較的代碼塊,呼叫時類似函式,當運算式一致時,斷言回傳成功,否則失敗,
googletest 的斷言是一組宏定義,分為 ASSERT_* 和 EXPECT_* 兩種,
比如
ASSERT_EQ(1, 2);
EXPECT_EQ(1, 2);
上面用到的兩個斷言都是比較輸入的資料是否相等,主要區別是,ASSERT_* 在失敗時終止程式運行,EXPECT_* 在失敗時不會終止程式運行,但是都會回傳錯誤資訊,因而測驗使用 EXPECT_* 可以發現更多的問題而不會打斷測驗流程,
那么 ASSERT_* 斷言失敗時,跟在其后的陳述句會被忽略執行,如果其中包含對資源的釋放,那么就有會出現資源泄漏的問題,斷言失敗報錯資訊會附帶有堆檢查錯誤,這時出現的資源泄漏問題,真的有必要修復碼?看具體情況而定,
另外,googletest 在斷言失敗后除了可以回傳標準錯誤資訊,還可以附帶回傳自定義錯誤資訊,使用運算子 << 添加自定義錯誤資訊,
ASSERT_EQ(1, 2) << "1 is not equal to 2";
EXPECT_EQ(1, 2) << "1 is not equal to 2";
任何可以傳遞給 ostream 的資料都可以作為自定義錯誤資訊傳遞給斷言,比如 C 字串、string物件,
那么,測驗的基本手段就是利用斷言,除了判斷型的斷言之外,googletest 還提供了其它型別的斷言用于協助測驗,比如顯式成功或失敗、布爾型別斷言、字串比較斷言等,詳情可以前往官網查看手冊,
https://google.github.io/googletest/reference/assertions.html
如何測驗
前面提到在 googletest 中,測驗的范圍分為測驗套件和單個測驗,測驗程式可以包含多個測驗套件,一個測驗套件可以包含多個測驗,
簡單的測驗一般推薦使用 TEST 宏來定義單個測驗,
一般的使用方式如下
TEST(test_suite_name, test_name) {
// test body
}
test_suite_name 是測驗套件名,test_name 是單個測驗的名稱,書寫時都應該符合 C++ 的識別符號規范,而且不能包含有下劃線_,更詳細的命名規范可以查看下面的鏈接
https://google.github.io/styleguide/cppguide.html#Function_Names
那么 TEST 宏到底代表著什么?一起來看看 TEST 宏定義的源代碼
#define GTEST_STRINGIFY_HELPER_(name, ...) #name
#define GTEST_STRINGIFY_(...) GTEST_STRINGIFY_HELPER_(__VA_ARGS__, )
#define GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
test_suite_name##_##test_name##_Test
#define GTEST_TEST_(test_suite_name, test_name, parent_class, parent_id) \
static_assert(sizeof(GTEST_STRINGIFY_(test_suite_name)) > 1, \
"test_suite_name must not be empty"); \
static_assert(sizeof(GTEST_STRINGIFY_(test_name)) > 1, \
"test_name must not be empty"); \
class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
: public parent_class { \
public: \
GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() = default; \
~GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() override = default; \
GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
(const GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &) = delete; \
GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=( \
const GTEST_TEST_CLASS_NAME_(test_suite_name, \
test_name) &) = delete; /* NOLINT */ \
GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
(GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &&) noexcept = delete; \
GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=( \
GTEST_TEST_CLASS_NAME_(test_suite_name, \
test_name) &&) noexcept = delete; /* NOLINT */ \
\
private: \
void TestBody() override; \
static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_; \
}; \
\
::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_suite_name, \
test_name)::test_info_ = \
::testing::internal::MakeAndRegisterTestInfo( \
#test_suite_name, #test_name, nullptr, nullptr, \
::testing::internal::CodeLocation(__FILE__, __LINE__), (parent_id), \
::testing::internal::SuiteApiResolver< \
parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__), \
::testing::internal::SuiteApiResolver< \
parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__), \
new ::testing::internal::TestFactoryImpl<GTEST_TEST_CLASS_NAME_( \
test_suite_name, test_name)>); \
void GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)::TestBody()
#define GTEST_TEST(test_suite_name, test_name) \
GTEST_TEST_(test_suite_name, test_name, ::testing::Test, \
::testing::internal::GetTestTypeId())
#define TEST(test_suite_name, test_name) GTEST_TEST(test_suite_name, test_name)
這么多預定義處理,不妨嘗試代入上面的一般使用方式,然后展開一下,展開如下
static_assert(sizeof("test_suite_name") > 1,
"test_suite_name must not be empty");
static_assert(sizeof("test_name") > 1,
"test_name must not be empty");
class test_suite_name_test_name_Test : public ::testing::Test {
public:
test_suite_name_test_name_Test() = default;
~test_suite_name_test_name_Test() override = default;
test_suite_name_test_name_Test(const test_suite_name_test_name_Test &) = delete;
test_suite_name_test_name_Test & operator=(
const test_suite_name_test_name_Test &) = delete; /* NOLINT */
test_suite_name_test_name_Test
(test_suite_name_test_name_Test &&) noexcept = delete;
test_suite_name_test_name_Test & operator=(
test_suite_name_test_name_Test &&) noexcept = delete; /* NOLINT */
private:
void TestBody() override;
static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;
};
::testing::TestInfo* const test_suite_name_test_name_Test::test_info_ =
::testing::internal::MakeAndRegisterTestInfo(
"test_suite_name", "test_name", nullptr, nullptr,
::testing::internal::CodeLocation(__FILE__, __LINE__),
::testing::internal::GetTestTypeId(),
::testing::internal::SuiteApiResolver<
parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__),
::testing::internal::SuiteApiResolver<
parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__),
new ::testing::internal::TestFactoryImpl<test_suite_name_test_name_Test>);
void test_suite_name_test_name_Test::TestBody() {
// test body
}
從展開后的代碼,可以看到有一堆代碼,最開始有兩個斷言 static_assert 用來判斷輸入的測驗套件名和測驗名長度是否大于1,所以要求 TEST 宏定義輸入的測驗套件名和測驗名都不能為空,
然后基于 ::testing::Test 派生了一個類,類名是測驗套件名和測驗名串接后再在末尾加上 _Test,類內宣告重寫 TestBody() 方法,
TEST 宏定義后面的 {} 用于定義派生類的成員方法 TestBody() 的函式體,內部填寫標準 C++ 的有效陳述句作為測驗主體,當然也包含呼叫 googletest 提供的模塊內容,注意這個代碼塊是沒有回傳值的,代碼塊執行的斷言失敗時,或者代碼崩潰,則測驗 test_name 失敗,否則成功,
再來看個例子
int square(const int a)
{
// ...
}
TEST(SquareTest, PositiveNos) {
ASSERT_EQ(0, square(0));
ASSERT_EQ(36, square(6));
ASSERT_EQ(324, square(18));
}
TEST(SquareTest, NegativeNos) {
ASSERT_EQ(1, square(-1));
ASSERT_EQ(100, square(-10));
}
上面定義了兩個測驗 PositiveNos 和 NegativeNos,都屬于測驗套件 SquareTest,
googletest 在設計時就指定通過測驗套件來匯總測驗結果,所以驗證同一個邏輯功能的測驗應該定義在同一個測驗套件內,
測驗夾具
在 googletest 里什么是測驗夾具?
測驗夾具這個概念是為了解決當你的同一個邏輯功能測驗里,有多個測驗共用測驗資料或者配置的問題,
需要用到測驗夾具的測驗一般推薦使用 TEST_F 宏來定義單個測驗,
一般的使用方式如下
TEST_F(FixtureTest, test_name) {
// test body
}
不過,TEST_F 宏的第一個輸入引數不僅僅是測驗套件名稱,同時也是測驗夾具類名,這個測驗夾具類需要自己基于類 ::testing::Test 派生實作,
class FixtureTest : public testing::Test {
protected:
void SetUp() override { ... }
void TearDown() override { ... }
// custom data
};
共用的測驗資料或者配置就在這個派生類里添加即可,SetUp() 用于初始化資料和配置,TearDown() 用于卸載配置,
那么 TEST_F 宏到底代表著什么,和 TEST 宏的區別在哪?一起來看看 TEST_F 宏定義的源代碼
#define GTEST_STRINGIFY_HELPER_(name, ...) #name
#define GTEST_STRINGIFY_(...) GTEST_STRINGIFY_HELPER_(__VA_ARGS__, )
#define GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
test_suite_name##_##test_name##_Test
#define GTEST_TEST_(test_suite_name, test_name, parent_class, parent_id) \
static_assert(sizeof(GTEST_STRINGIFY_(test_suite_name)) > 1, \
"test_suite_name must not be empty"); \
static_assert(sizeof(GTEST_STRINGIFY_(test_name)) > 1, \
"test_name must not be empty"); \
class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
: public parent_class { \
public: \
GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() = default; \
~GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() override = default; \
GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
(const GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &) = delete; \
GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=( \
const GTEST_TEST_CLASS_NAME_(test_suite_name, \
test_name) &) = delete; /* NOLINT */ \
GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
(GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &&) noexcept = delete; \
GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=( \
GTEST_TEST_CLASS_NAME_(test_suite_name, \
test_name) &&) noexcept = delete; /* NOLINT */ \
\
private: \
void TestBody() override; \
static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_; \
}; \
\
::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_suite_name, \
test_name)::test_info_ = \
::testing::internal::MakeAndRegisterTestInfo( \
#test_suite_name, #test_name, nullptr, nullptr, \
::testing::internal::CodeLocation(__FILE__, __LINE__), (parent_id), \
::testing::internal::SuiteApiResolver< \
parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__), \
::testing::internal::SuiteApiResolver< \
parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__), \
new ::testing::internal::TestFactoryImpl<GTEST_TEST_CLASS_NAME_( \
test_suite_name, test_name)>); \
void GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)::TestBody()
#define GTEST_TEST_F(test_fixture, test_name) \
GTEST_TEST_(test_fixture, test_name, test_fixture, \
::testing::internal::GetTypeId<test_fixture>())
#define TEST_F(test_fixture, test_name) GTEST_TEST_F(test_fixture, test_name)
這么多預定義處理,手癢代入一般的使用方式然后展開一下,展開如下
static_assert(sizeof("FixtureTest") > 1,
"FixtureTest must not be empty");
static_assert(sizeof("test_name") > 1,
"test_name must not be empty");
class FixtureTest_test_name_Test : public FixtureTest {
public:
FixtureTest_test_name_Test() = default;
~FixtureTest_test_name_Test() override = default;
FixtureTest_test_name_Test(const FixtureTest_test_name_Test &) = delete;
FixtureTest_test_name_Test & operator=(
const FixtureTest_test_name_Test &) = delete; /* NOLINT */
FixtureTest_test_name_Test
(FixtureTest_test_name_Test &&) noexcept = delete;
FixtureTest_test_name_Test & operator=(
FixtureTest_test_name_Test &&) noexcept = delete; /* NOLINT */
private:
void TestBody() override;
static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;
};
::testing::TestInfo* const FixtureTest_test_name_Test::test_info_ =
::testing::internal::MakeAndRegisterTestInfo(
#FixtureTest, #test_name, nullptr, nullptr,
::testing::internal::CodeLocation(__FILE__, __LINE__),
::testing::internal::GetTypeId<FixtureTest>(),
::testing::internal::SuiteApiResolver<
FixtureTest>::GetSetUpCaseOrSuite(__FILE__, __LINE__),
::testing::internal::SuiteApiResolver<
FixtureTest>::GetTearDownCaseOrSuite(__FILE__, __LINE__),
new ::testing::internal::TestFactoryImpl<FixtureTest_test_name_Test>);
void FixtureTest_test_name_Test::TestBody() {
// test body
}
從展開后的代碼來看,TEST_F 和 TEST 實作基本類似,那么使用時要遵循的規則也是一樣的,除了需要傳入自定義的基于 ::testing::Test 派生類,并且測驗套件名就是測驗夾具類名,
舉個例子,有個模板類 Queue 的邏輯功能需要測驗,它實作了 FIFO 的資料佇列管理,
template <typename E> // E 是元素型別
class Queue {
public:
Queue();
void Enqueue(const E& element); // 資料入隊
E* Dequeue(); // 資料出隊,如果佇列為空則回傳 NULL
size_t size() const; // 佇列資料長度
...
};
然后需要基于 ::testing::Test 派生一個測驗夾具類 QueueTest
class QueueTest : public ::testing::Test {
protected:
void SetUp() override {
q1_.Enqueue(1);
q2_.Enqueue(2);
q2_.Enqueue(3);
}
// void TearDown() override {}
Queue<int> q0_;
Queue<int> q1_;
Queue<int> q2_;
};
夾具類 QueueTest 內定義了三個佇列資料物件,SetUp() 內對資料物件初始化,q0_ 保持為空,q1_ 入隊一個資料,q2_ 入隊兩個資料,
為什么不實作 TearDown() 呢?TearDown() 本來的設計意圖是卸載配置,不是剛好可以用來清理資料嗎?是的,的確可以,不過這里有個更好的選擇,就是使用類解構式來對佇列清空,這里有個建議就是,能用解構式處理的,盡量用解構式替代 TearDown(),因為用解構式可以確保被呼叫而且呼叫的順序不會亂,但不是說所有情況都建議用解構式替代 TearDown(),這里不展開了,
接著呼叫 TEST_F 定義兩個測驗,基于測驗夾具類 QueueTest,測驗套件名也是 QueueTest,兩個測驗名分別為 IsEmptyInitially 和 DequeueWorks,
TEST_F(QueueTest, IsEmptyInitially) {
EXPECT_EQ(q0_.size(), 0);
}
TEST_F(QueueTest, DequeueWorks) {
int* n = q0_.Dequeue();
EXPECT_EQ(n, nullptr);
n = q1_.Dequeue();
ASSERT_NE(n, nullptr);
EXPECT_EQ(*n, 1);
EXPECT_EQ(q1_.size(), 0);
delete n;
n = q2_.Dequeue();
ASSERT_NE(n, nullptr);
EXPECT_EQ(*n, 2);
EXPECT_EQ(q2_.size(), 1);
delete n;
}
上面的這兩個測驗定義,都會創建 QueueTest 類物件,分別創建而且不共用,所以資料不會相互影響,
第一個測驗 IsEmptyInitially,googletest 框架會先創建 QueueTest 類物件 obj,呼叫 SetUp() 初始化資料和配置,執行測驗,這里只執行了一個 EXPECT_EQ 斷言,EXPECT_* 型別的斷言失敗后會回傳失敗資訊,不會終止測驗程式,繼續下一步測驗,然后呼叫 TearDown() 清理,最后執行物件 obj 的解構式釋放資源并退出當前測驗,
第二個測驗 DequeueWorks,執行流程與上一個類似,其中測驗內容包含有 ASSERT_* 類別的斷言,這種斷言在失敗后除了會回傳失敗資訊,還會終止測驗程式,如果斷言失敗之后的測驗已沒有意義,那么適合使用 ASSERT_* 類別的斷言,
測驗呼叫程序
其它 C++ 測驗框架在測驗開始前,需要你把測驗排列出來,但是 googletest 不需要這么麻煩, 在 googletest 框架中,定義好測驗后,只需要在 main 部分執行如下代碼即可,
int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
InitGoogleTest() 可以對程式的輸入命令執行決議,基于這點可以通過命令列的方式控制測驗框架的運行,
繼續以上面的代碼為例,大致流程如下
- InitGoogleTest() 初始化測驗框架,
- RUN_ALL_TESTS() 啟動測驗,
- 查找測驗套件內的測驗,
- 保存配置標志,
- 創建 QueueTest 實體,
- 呼叫 QueueTest 實體的 SetUp() 初始化資料配置,
- 執行測驗,
- 呼叫 QueueTest 實體的 TearDown() 卸載資料配置,
- 恢復配置標志,
- 重復第 3 步,直到所有測驗執行完畢,
RUN_ALL_TESTS() 回傳 0 表示成功,否則失敗,只能在主執行緒里呼叫 RUN_ALL_TESTS(),
在一般的測驗里,如果在測驗運行之前不需要做一些自定義的事情,而且這些事情無法在測驗夾具和測驗套件的框架中表達時,main 函式這部分其實都一樣,那么 googletest 就在庫 gtest_main 里提供了一個很方便的入口點,也就是幫你提前寫好了 main 函式,你可以省去這部分,編譯的時候記得鏈接庫 gtest_main 即可,
好了,這個系列的文章就寫到這里啦,
學習可以等,時間不等人!
關注我,帶你學習編程領域更多核心技能!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/550189.html
標籤:其他
上一篇:人工智能中的頂會
下一篇:一天吃透作業系統八股文