單元測(cè)試成神之路——C++篇 28 Sep 2020 Reading time ~8 minutes 前言 在上一篇單元測(cè)試成神之路————GoLang篇 中, 首先介紹了單元測(cè)試的意義和編寫(xiě)單元測(cè)試的一般方法。作為同系列出品,本篇文章則主要介紹如何在C++中寫(xiě)單元測(cè)試。 本文直接從常用的C++單元測(cè)試框架出發(fā),分別對(duì)幾種框架進(jìn)行了簡(jiǎn)單的介紹和小結(jié),然后介紹了Mock的框架, 并以具體代碼示例進(jìn)行說(shuō)明,最后列舉了一些常見(jiàn)問(wèn)題。 一、常用C++單測(cè)框架 常用的C++單測(cè)對(duì)比如下: Google Test | Catch 2 | CppUTest | | 特點(diǎn) | <ul><li>成熟、兼容性好</li><li>簡(jiǎn)潔、有效率</li><li>常用、學(xué)習(xí)資源多</li></ul> | <ul><li>框架只有一個(gè)catch.hpp 、集成輕松</li><li>有Given-When-Then分區(qū),適合BDD行為驅(qū)動(dòng)開(kāi)發(fā)</li><li>無(wú)自帶Mock框架</li></ul> | <ul><li>可以檢測(cè)內(nèi)存泄露</li><li>輸出更簡(jiǎn)潔</li><li>適合在嵌入式系統(tǒng)項(xiàng)目中使用</li></ul> | Mock框架 | Google Mock | 無(wú)自帶Mock框架 | CppUMock | 推薦指數(shù) | ★★★★★ | ★★★☆☆ | ★★☆☆☆ | 一般情況下,我們推薦使用Google Test搭配Google Mock。如果項(xiàng)目有特殊需求或更適合其他框架,也可以考慮。 根據(jù)實(shí)際使用頻率,在以下部分,Google Test和Google Mock的介紹更為詳細(xì);對(duì)于其他框架,這里介紹它們的主要特點(diǎn), 具體使用方法,可以查閱各自文檔。 二. Google Test Google Test是目前比較成熟而且最常用的C++單元測(cè)試框架之一。 1. 基本概念 斷言(Assertions) 是檢查條件是否為真的語(yǔ)句。斷言的結(jié)果可能是成功或者失敗, 而失敗又分為非致命失敗或致命失敗。如果發(fā)生致命失敗,測(cè)試進(jìn)程將中止當(dāng)前運(yùn)行,否則它將繼續(xù)運(yùn)行。 測(cè)試(Test) 使用斷言來(lái)驗(yàn)證被測(cè)試代碼的行為。如果測(cè)試崩潰或斷言失敗,則測(cè)試失敗;否則測(cè)試成功。 測(cè)試套件(Test Suite) 包含一個(gè)或多個(gè)測(cè)試(Test)。當(dāng)測(cè)試套件中的多個(gè)測(cè)試需要共享通用對(duì)象和子例程時(shí), 可以將它們放入測(cè)試夾具(Test Fixture)。 測(cè)試程序(Test Program) 可以包含多個(gè)測(cè)試套件。 2. 斷言 Google Test中,斷言(Assertions) 是類(lèi)似函數(shù)調(diào)用的宏。斷言失敗時(shí),googletest會(huì)輸出斷言的源文件和 行號(hào)位置以及失敗消息;我們還可以提供自定義失敗消息,該消息將附加到googletest消息中。 斷言成對(duì)出現(xiàn)(ASSERT_* 和EXPECT_* ),它們測(cè)試的對(duì)象相同,但對(duì)當(dāng)前運(yùn)行有不同的影響。ASSERT_* 版本失敗時(shí) 會(huì)產(chǎn)生致命故障,并中止當(dāng)前函數(shù)(不一定是整個(gè)TEST)運(yùn)行。EXPECT_* 版本會(huì)產(chǎn)生非致命故障,不會(huì)停止當(dāng)前函數(shù)運(yùn)行。 通常EXPECT_* 是首選,因?yàn)榭梢栽跍y(cè)試中報(bào)告多個(gè)故障。但是如果在斷言失敗時(shí)繼續(xù)執(zhí)行沒(méi)有意義,則應(yīng)使用ASSERT_* 。 要提供自定義失敗消息,只需使用<< 運(yùn)算符或此類(lèi)運(yùn)算符的序列將其流式傳輸?shù)胶曛屑纯?。一個(gè)例子: ASSERT_EQ(x.size(), y.size()) << "x和y長(zhǎng)度不同";
for (int i = 0; i < x.size(); ++i) {
EXPECT_EQ(x[i], y[i]) << "x和y元素存在不同:" << i;
}
以下是一些最常用的斷言,如果需要查閱其他斷言,可以前往googletest的官方文檔。 2.1. 基本斷言 condition 是返回true /false 的變量、布爾表達(dá)式、函數(shù)調(diào)用等,以下斷言對(duì)其進(jìn)行驗(yàn)證。 失敗時(shí)中斷執(zhí)行的斷言 | 失敗時(shí)不中斷執(zhí)行的斷言 | 斷言成功情況 | ASSERT_TRUE(condition); | EXPECT_TRUE(condition); | condition為真 | ASSERT_FALSE(condition); | EXPECT_FALSE(condition); | condition為假 | 例如:在ASSERT_TRUE(condition) 中,當(dāng)condition 為true 時(shí),符合斷言,不影響執(zhí)行;當(dāng)condition 為false 時(shí),不符合斷言,且由于是ASSERT ,當(dāng)前執(zhí)行中斷。 2.2. 普通比較型斷言 val1 和val2 是兩個(gè)可用== 、!= 、> 、< 等運(yùn)算符進(jìn)行比較的值,以下斷言對(duì)其進(jìn)行比較。 失敗時(shí)中斷執(zhí)行的斷言 | 失敗時(shí)不中斷執(zhí)行的斷言 | 斷言成功情況 | ASSERT_EQ(val1, val2); | EXPECT_EQ(val1, val2); | val1 == val2 | ASSERT_NE(val1, val2); | EXPECT_NE(val1, val2); | val1 != val2 | ASSERT_LT(val1, val2); | EXPECT_LT(val1, val2); | val1 < val2 | ASSERT_LE(val1, val2); | EXPECT_LE(val1, val2); | val1 <= val2 | ASSERT_GT(val1, val2); | EXPECT_GT(val1, val2); | val1 > val2 | ASSERT_GE(val1, val2); | EXPECT_GE(val1, val2); | val1 >= val2 | 例如:在ASSERT_GT(val1, val2) 中,只有當(dāng)val1 > val2 時(shí),符合斷言,不影響執(zhí)行;當(dāng)val1 <= val2 時(shí), 不符合斷言,且由于是ASSERT ,當(dāng)前執(zhí)行中斷。 2.3. C字符串比較型斷言 str1 和str2 是兩個(gè)C字符串,以下斷言對(duì)它們的值進(jìn)行比較;如果要比較兩個(gè)std::string 對(duì)象,直接用之前 提到的EXPECT_NE ,EXPECT_NE 等。 失敗時(shí)中斷執(zhí)行的斷言 | 失敗時(shí)不中斷執(zhí)行的斷言 | 斷言成功情況 | ASSERT_STREQ(str1,str2); | EXPECT_STREQ(str1,str2); | 這兩個(gè)C字符串具有相同的內(nèi)容 | ASSERT_STRNE(str1,str2); | EXPECT_STRNE(str1,str2); | 兩個(gè)C字符串的內(nèi)容不同 | ASSERT_STRCASEEQ(str1,str2); | EXPECT_STRCASEEQ(str1,str2); | 忽略大小寫(xiě),兩個(gè)C字符串的內(nèi)容相同 | ASSERT_STRCASENE(str1,str2); | EXPECT_STRCASENE(str1,str2) | 忽略大小寫(xiě),兩個(gè)C字符串的內(nèi)容不同 | 例如:char *str1 = "ABC"; char *str2 = "ABC"; ,EXPECT_STREQ(str1, str2); 斷言通過(guò), 因?yàn)樗鼈兊膬?nèi)容一樣;而EXPECT_EQ(str1, str2); 斷言失敗,因?yàn)樗鼈兊牡刂凡灰粯印?/p> 注意:一個(gè)NULL 指針和一個(gè)空字符串"" 是不同的。 2.4. 浮點(diǎn)數(shù)比較型斷言 val1 和val2 是兩個(gè)浮點(diǎn)數(shù),以下斷言對(duì)其進(jìn)行比較。 失敗時(shí)中斷執(zhí)行的斷言 | 失敗時(shí)不中斷執(zhí)行的斷言 | 斷言成功情況 | ASSERT_FLOAT_EQ(val1, val2); | EXPECT_FLOAT_EQ(val1, val2); | 這兩個(gè)float值幾乎相等 | ASSERT_DOUBLE_EQ(val1, val2); | EXPECT_DOUBLE_EQ(val1, val2); | 這兩個(gè)double值幾乎相等 | 以下斷言可以選擇可接受的誤差范圍: 失敗時(shí)中斷執(zhí)行的斷言 | 失敗時(shí)不中斷執(zhí)行的斷言 | 斷言成功情況 | ASSERT_NEAR(val1, val2, abs_error); | EXPECT_NEAR(val1, val2, abs_error); | val1和val2的差的絕對(duì)值不超過(guò)abs_error | 2.5. 明確的成功和失敗 - 明確生成成功:
SUCCEED(); 生成一個(gè)成功,但這不代表整個(gè)測(cè)試就成功了。 - 明確生成失敗:
FAIL(); 生成致命錯(cuò)誤 ADD_FAILURE(); 生成非致命錯(cuò)誤。 ADD_FAILURE_AT("file_path",line_number); 生成非致命錯(cuò)誤,輸出文件名和行號(hào)。 例如: if(condition) {
SUCCEED();
} else{
FAIL();
}
效果上等同于 只是ASSERT_TRUE 失敗時(shí)可以輸出condition 的具體值。當(dāng)?shù)覀冃枰?yàn)證的condition 很復(fù)雜時(shí), 或者需要很多個(gè)if..else... 分支來(lái)驗(yàn)證彼此互斥的情況以保證覆蓋到每一種可能性時(shí),SUCCEED() 、FAIL() 等 明確的成功/失敗可能是更好的選擇。 2.6. 異常斷言 這些斷言驗(yàn)證一段代碼(statement )是否拋出(或不拋出)給定類(lèi)型的異常: 失敗時(shí)中斷執(zhí)行的斷言 | 失敗時(shí)不中斷執(zhí)行的斷言 | 斷言成功情況 | ASSERT_THROW(statement, exception_type); | EXPECT_THROW(statement, exception_type); | statement拋出給定類(lèi)型的異常 | ASSERT_ANY_THROW(statement); | EXPECT_ANY_THROW(statement); | statement拋出任何類(lèi)型的異常 | ASSERT_NO_THROW(statement); | EXPECT_NO_THROW(statement); | statement不拋出任何異常 | 2.7. 使用已有布爾函數(shù) 當(dāng)predN 是一個(gè)有N 個(gè)參數(shù),返回布爾值的函數(shù)時(shí),以下斷言可以獲取更好的錯(cuò)誤信息。 失敗時(shí)中斷執(zhí)行的斷言 | 失敗時(shí)不中斷執(zhí)行的斷言 | 斷言成功情況 | ASSERT_PRED1(pred1, val1); | EXPECT_PRED1(pred1, val1); | pred1(val1)為真 | ASSERT_PRED2(pred2, val1, val2); | EXPECT_PRED2(pred2, val1, val2); | pred2(val1, val2)為真 | … | … | … | 例如:isComparable(Object o1, Object o2) 是一個(gè)返回布爾值的函數(shù)。我們可以有以下選擇, 都能達(dá)到驗(yàn)證函數(shù)調(diào)用結(jié)果的目的: ASSERT_TRUE(isComparable(obj1, obj2)); ASSERT_PRED2(isComparable, obj1, obj2); 區(qū)別在于:當(dāng)斷言失敗時(shí),ASSERT_TRUE 只會(huì)告知函數(shù)最后的返回值是false ;而ASSERT_TRUE 同時(shí) 會(huì)輸出val1 、val2 的值。 3. 測(cè)試 創(chuàng)建一個(gè)測(cè)試的步驟: - 使用
TEST() 宏定義和命名測(cè)試功能。 - 在
TEST() 宏內(nèi),構(gòu)造出達(dá)到測(cè)試狀態(tài)的函數(shù)、變量 - 使用斷言指定函數(shù)、變量期望的返回值、值。
TEST(MessageTestSuite, BodyLengthNegative) {
... 構(gòu)造 ...
... 斷言 ...
}
TEST() 第一個(gè)參數(shù)是Test Suite的名稱(chēng),第二個(gè)參數(shù)是Test Suite內(nèi)的Test名稱(chēng)。這兩個(gè)名稱(chēng)都必須是有效的 C++標(biāo)識(shí)符,并且它們不應(yīng)包含任何下劃線(xiàn)(_ )。測(cè)試的全名包括Test Suite名和Test名。來(lái)自不同Test Suite的 測(cè)試可以具有相同的Test名。它們都不是變量,也不是字符串。 在上面的例子中,這個(gè)測(cè)試的名稱(chēng)是BodyLengthNegative ,Test Suite的名稱(chēng)是MessageTestSuite 。 4. 測(cè)試夾具:多個(gè)測(cè)試有共有的數(shù)據(jù)配置 如果多個(gè)測(cè)試有共有的數(shù)據(jù)配置,可以使用測(cè)試夾具(Test Fixture)將共用部分提取出來(lái)重復(fù)利用。 要?jiǎng)?chuàng)建一個(gè)測(cè)試夾具: - 創(chuàng)建一個(gè)繼承
::testing::Test 的類(lèi)。從protected 開(kāi)始這個(gè)類(lèi),因?yàn)槲覀円獜淖宇?lèi)訪(fǎng)問(wèn)夾具成員。 - 在類(lèi)內(nèi)部,聲明計(jì)劃使用的任何對(duì)象。
- 如有必要,編寫(xiě)默認(rèn)的
constructor 或SetUp() 函數(shù)為每個(gè)測(cè)試準(zhǔn)備對(duì)象。 - 如有必要,編寫(xiě)一個(gè)
destructor 或TearDown() 函數(shù)以釋放在SetUp() 中分配的任何資源。 - 如有必要,定義一些共享的類(lèi)函數(shù)
當(dāng)使用測(cè)試夾具是,需要使用TEST_F() 而不是TEST() class TestFixtureName : public ::testing::Test {
protected:
virtual void SetUp() {
...
}
virtual void TearDown() {
...
}
virtual int SomeFunction() {
...
}
SomeObject object;
};
TEST_F(TestFixtureName, TestName1) {
... 構(gòu)造 ...
... 斷言 ...
}
TEST_F(TestFixtureName, TestName2) {
... 構(gòu)造 ...
... 斷言 ...
}
那上面這個(gè)例子來(lái)說(shuō),對(duì)于每個(gè)TEST_F() 測(cè)試,googletest將在運(yùn)行時(shí) - 創(chuàng)建一個(gè)新的測(cè)試夾具(Test Fixture)對(duì)象
- 通過(guò)
SetUp() 對(duì)其進(jìn)行初始化 - 運(yùn)行該
TEST_F() 測(cè)試 - 通過(guò)調(diào)用進(jìn)行清理
TearDown() - 然后刪除該測(cè)試夾具(Test Fixture)對(duì)象
所以,雖然多個(gè)TEST_F 共用同一部分代碼,但共同代碼會(huì)每個(gè)TEST_F 都獨(dú)立執(zhí)行一次。同一測(cè)試套件中的不同測(cè)試具有不同的測(cè)試夾具對(duì)象。一個(gè)測(cè)試對(duì)測(cè)試夾具所做的任何更改均不會(huì)影響其他測(cè)試。 三、Catch 2 Catch2 僅有頭部文件(header only),所以它的第一個(gè)優(yōu)點(diǎn)是可以輕易地放入任何項(xiàng)目中進(jìn)行使用。只需要 #include "catch.hpp" 就可以在當(dāng)前文件使用 Catch 1. REQUIRE Catch的基礎(chǔ)使用方法也很簡(jiǎn)單。 #define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one cpp file
#include "catch.hpp"
unsigned int Factorial( unsigned int number ) {
return number <= 1 ? number : Factorial(number-1)*number;
}
TEST_CASE( "Factorials are computed", "[factorial]" ) {
REQUIRE( Factorial(0) == 1 );
REQUIRE( Factorial(1) == 1 );
REQUIRE( Factorial(2) == 2 );
REQUIRE( Factorial(3) == 6 );
REQUIRE( Factorial(10) == 3628800 );
}
2. SECTIONS Catch的SECTION相當(dāng)于GTEST里夾具(fixture)的功能。對(duì)每一個(gè)SECTION,TEST_CASE 都從頭開(kāi)始執(zhí)行。 TEST_CASE( "vectors can be sized and resized", "[vector]" ) {
std::vector<int> v( 5 );
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
SECTION( "resizing bigger changes size and capacity" )
v.resize( 10 );
REQUIRE( v.size() == 10 );
REQUIRE( v.capacity() >= 10 );
}
SECTION( "resizing smaller changes size not capacity" ){
v.resize( 0 );
REQUIRE( v.size() == 0 );
REQUIRE( v.capacity() >= 5 );
}
SECTION( "reserving bigger changes capacity not size" ) {
v.reserve( 10 );
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 10 );
}
SECTION( "reserving smaller does not change size" ) {
v.reserve( 0 );
REQUIRE( v.size() == 5 );
REQUIRE( v.capacity() >= 5 );
}
}
3. 標(biāo)簽 Catch提供標(biāo)簽特性。 TEST_CASE( "A", "[widget]" ) { /* ... */ }
TEST_CASE( "B", "[widget]" ) { /* ... */ }
TEST_CASE( "C", "[gadget]" ) { /* ... */ }
TEST_CASE( "D", "[widget][gadget]" ) { /* ... */ }
"[widget]" 選取 A、B、D. "[gadget]" 選取 C、D. "[widget][gadget]" 只選取 D "[widget],[gadget]" 所有A、B、C、D. - 還有一些特殊標(biāo)簽指定特殊行為
4. 特點(diǎn)總結(jié) - 框架只有一個(gè)
catch.hpp 、集成輕松 - 有Given-When-Then分區(qū),適合BDD行為驅(qū)動(dòng)開(kāi)發(fā)
- 無(wú)自帶Mock框架
四、CppUTest 1. main和test main.cpp: #include "CppUTest/CommandLineTestRunner.h"
int main(int ac, char** av){
return CommandLineTestRunner::RunAllTests(ac, av);
}
test.cpp: #include "CppUTest/TestHarness.h"
TEST_GROUP(FirstTestGroup){
void setup(){
// Init stuff
}
void teardown(){
// Uninit stuff
}
};
TEST(FirstTestGroup, FirstTest){
FAIL("Fail me!");
}
TEST(FirstTestGroup, SecondTest){
STRCMP_EQUAL("hello", "world");
}
2. 斷言 CHECK(boolean condition) 檢查任何布爾結(jié)果。 CHECK_TEXT(boolean condition, text) 檢查任何布爾結(jié)果,并在失敗時(shí)輸出文本。 CHECK_FALSE(condition) 檢查任何布爾結(jié)果 CHECK_EQUAL(expected, actual) 使用== 檢查實(shí)體之間的相等性。因此,如果有一個(gè)支持operator==() 的類(lèi),則可以使用此宏比較兩個(gè)實(shí)例。 CHECK_COMPARE(first, relop, second) 檢查在兩個(gè)實(shí)體之間是否存在關(guān)系運(yùn)算符。失敗時(shí),打印兩個(gè)操作數(shù)求和的結(jié)果。 CHECK_THROWS(expected_exception, expression) 檢查表達(dá)式是否拋出expected_exception (例如std::exception )。CHECK_THROWS 僅在使用標(biāo)準(zhǔn)C ++庫(kù)(默認(rèn))構(gòu)建CppUTest時(shí)可用。 STRCMP_EQUAL(expected, actual) 使用strcmp() 檢查const char * 字符串是否相等。 STRNCMP_EQUAL(expected, actual, length) 使用strncmp() 檢查const char * 字符串是否相等。 STRCMP_NOCASE_EQUAL(expected, actual) 不考慮大小寫(xiě),檢查const char * 字符串是否相等。 3. 特點(diǎn) - 可以檢測(cè)內(nèi)存泄露
- 輸出更簡(jiǎn)潔
- 使用在嵌入式系統(tǒng)項(xiàng)目中使用
五、Google Mock Google Mock一般來(lái)說(shuō)和Google Test搭配使用,但Google Test也可以和其他Mock框架一起使用。 本部分是Google Mock基礎(chǔ)常用的用法,如需要特殊用法,請(qǐng)查閱Google Mock官方文檔。 1. Fake、Mock、Stub - Fake對(duì)象有具體的實(shí)現(xiàn),但采取一些捷徑,比如用內(nèi)存替代真實(shí)的數(shù)據(jù)庫(kù)讀取。
- Stub對(duì)象沒(méi)有具體的實(shí)現(xiàn),只是返回提前準(zhǔn)備好的數(shù)據(jù)。
- Mock對(duì)象和Stub類(lèi)似,只是在測(cè)試中需要調(diào)用時(shí),針對(duì)某種輸入指定期望的行為。Mock和Stub的區(qū)別是, Mock除了返回?cái)?shù)據(jù)還可以指定期望以驗(yàn)證行為。
2. 簡(jiǎn)單例子:Mock Turtle Turtle類(lèi): class Turtle {
...
virtual ~Turtle() {};
virtual void PenUp() = 0;
virtual void PenDown() = 0;
virtual void Forward(int distance) = 0;
virtual void Turn(int degrees) = 0;
virtual void GoTo(int x, int y) = 0;
virtual int GetX() const = 0;
virtual int GetY() const = 0;
};
MockTurtle類(lèi): #include "gmock/gmock.h"
class MockTurtle : public Turtle {
public:
...
MOCK_METHOD(void, PenUp, (), (override));
MOCK_METHOD(void, PenDown, (), (override));
MOCK_METHOD(void, Forward, (int distance), (override));
MOCK_METHOD(void, Turn, (int degrees), (override));
MOCK_METHOD(void, GoTo, (int x, int y), (override));
MOCK_METHOD(int, GetX, (), (const, override));
MOCK_METHOD(int, GetY, (), (const, override));
};
創(chuàng)建Mock類(lèi)的步驟: MockTurtle 繼承Turtle - 找到
Turtle 的一個(gè)虛函數(shù) - 在
public: 的部分中,寫(xiě)一個(gè)MOCK_METHOD(); -
將虛函數(shù)函數(shù)簽名復(fù)制進(jìn)MOCK_METHOD(); 中,加兩個(gè)逗號(hào):一個(gè)在返回類(lèi)型和函數(shù)名之間另一個(gè)在函數(shù)名和參數(shù)列表之間 例如:void PenDown() 有三部分:void 、PenDown 和() ,這三部分就是MOCK_METHOD 的前三個(gè)參數(shù) - 如果要模擬
const 方法,添加一個(gè)包含(const) 的第4個(gè)參數(shù)(必須帶括號(hào))。 - 建議添加
override 關(guān)鍵字。所以對(duì)于const 方法,第四個(gè)參數(shù)變?yōu)?code>(const, override),對(duì)于非const 方法,第四個(gè)參數(shù)變?yōu)?code>(override)。這不是強(qiáng)制性的。 - 重復(fù)步驟直至完成要模擬的所有虛擬函數(shù)。
3. 在測(cè)試中使用Mock 在測(cè)試中使用Mock的步驟: - 從
testing 名稱(chēng)空間導(dǎo)入gmock.h 的函數(shù)名(每個(gè)文件只需要執(zhí)行一次)。 - 創(chuàng)建一些Mock對(duì)象。
- 指定對(duì)它們的期望(方法將被調(diào)用多少次?帶有什么參數(shù)?每次應(yīng)該做什么(對(duì)參數(shù)做什么、返回什么值)?等等)。
- 使用Mock對(duì)象;可以使用googletest斷言檢查結(jié)果。如果mock函數(shù)的調(diào)用超出預(yù)期或參數(shù)錯(cuò)誤,將會(huì)立即收到錯(cuò)誤消息。
- 當(dāng)Mock對(duì)象被銷(xiāo)毀時(shí),gMock自動(dòng)檢查對(duì)模擬的所有期望是否得到滿(mǎn)足。
#include "path/to/mock-turtle.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
using ::testing::AtLeast; // #1
TEST(PainterTest, CanDrawSomething) {
MockTurtle turtle; // #2
EXPECT_CALL(turtle, PenDown()) // #3
.Times(AtLeast(1));
Painter painter(&turtle); // #4
EXPECT_TRUE(painter.DrawCircle(0, 0, 10)); // #5
}
在這個(gè)例子中,我們期望turtle 的PenDown() 至少被調(diào)用一次。如果在turtle 對(duì)象被銷(xiāo)毀時(shí),PenDown() 還沒(méi)有被調(diào)用或者調(diào)用兩次或以上,測(cè)試會(huì)失敗。 4. 指定期望 EXPECT_CALL (指定期望)是使用Google Mock的核心。EXPECT_CALL 的作用是兩方面的: -
告訴這個(gè)Mock(假)方法如何模仿原始方法: 我們?cè)?code>EXPECT_CALL中告訴Google Mock,某個(gè)對(duì)象的某個(gè)方法被第一次調(diào)用時(shí),會(huì)修改某個(gè)參數(shù),會(huì)返回某個(gè)值;第二次調(diào)用時(shí),會(huì)修改某個(gè)參數(shù),會(huì)返回某個(gè)值……. -
驗(yàn)證被調(diào)用的情況 我們?cè)?code>EXPECT_CALL中告訴Google Mock,某個(gè)對(duì)象的某個(gè)方法總共會(huì)被調(diào)用N次(或大于N次、小于N次)。如果最終次數(shù)不符合預(yù)期,會(huì)導(dǎo)致測(cè)試失敗。 4.1. 基本語(yǔ)法 EXPECT_CALL(mock_object, method(matchers))
.Times(cardinality)
.WillOnce(action)
.WillRepeatedly(action);
mock_object 是對(duì)象 method(matchers) 用于匹配相應(yīng)的函數(shù)調(diào)用 cardinality 指定基數(shù)(被調(diào)用次數(shù)情況) action 指定被調(diào)用時(shí)的行為 例子: using ::testing::Return;
...
EXPECT_CALL(turtle, GetX())
.Times(5)
.WillOnce(Return(100))
.WillOnce(Return(150))
.WillRepeatedly(Return(200));
這個(gè)EXPECT_CALL() 指定的期望是:在turtle 這個(gè)Mock對(duì)象銷(xiāo)毀之前,turtle 的getX() 函數(shù)會(huì)被調(diào)用五次。第一次返回100 ,第二次返回150 ,第三次及以后都返回200 。指定期望后,5次對(duì)getX 的調(diào)用會(huì)有這些行為。但如果最終調(diào)用次數(shù)不為5次,則測(cè)試失敗。 4.2. 參數(shù)匹配:哪次調(diào)用 using ::testing::_;
using ::testing::Ge;
// 只與Forward(100)匹配
EXPECT_CALL(turtle, Forward(100));
// 與GoTo(x,y)匹配, 只要x>=50
EXPECT_CALL(turtle, GoTo(Ge(50), _));
_ 相當(dāng)于“任何”。 100 相當(dāng)于Eq(100) 。 Ge(50) 指參數(shù)大于或等于50。 - 如果不關(guān)心參數(shù),只寫(xiě)函數(shù)名就可以。比如
EXPECT_CALL(turtle, GoTo); 。 4.3. 基數(shù):被調(diào)用幾次 用Times(m) ,Times(AtLeast(n)) 等來(lái)指定期待的調(diào)用次數(shù)。 Times 可以被省略。比如整個(gè)EXPECT_CALL 只有一個(gè)WillOnce(action) 相當(dāng)于也說(shuō)明了調(diào)用次數(shù)只能為1。 4.4. 行為:該做什么 常用模式:如果需要指定前幾次調(diào)用的特殊情況,并且之后的調(diào)用情況相同。使用一系列WillOnce() 之后有WillRepeatedly() 除了用來(lái)指定調(diào)用返回值的Return() ,Google Mock中常用行為中還有:SetArgPointee<N>(value) , SetArgPointee 將第N 個(gè)指針參數(shù)(從0開(kāi)始)指向的變量賦值為value 。 比如void getObject(Object* response){...} 的EXPECT_CALL : Object* a = new Object;
EXPECT_CALL(object, request)
.WillOnce(SetArgPointee<1>(*a));
就修改了傳入的指針response ,使其指向了一個(gè)我們新創(chuàng)建的對(duì)象。 如果有多個(gè)行為,應(yīng)該使用DoAll(a1, a2, ..., an) 。DoAll 執(zhí)行所有n 個(gè)action并返回an 的結(jié)果。 4.5. 使用多個(gè)預(yù)期 例子: using ::testing::_;
...
EXPECT_CALL(turtle, Forward(_)) // #1
.Times(3);
EXPECT_CALL(turtle, Forward(10)) // #2
.Times(2);
...mock對(duì)象函數(shù)被調(diào)用...
//Forward(10); // 與#2匹配
//Forward(20); // 與#1匹配
正常情況下,Google Mock以倒序搜索預(yù)期:如果和多個(gè)EXPECT_CALL 都可以匹配,只有之前的, 距離調(diào)用最近的一個(gè)EXPECT_CALL() 會(huì)被匹配。例如: - 連續(xù)三次調(diào)用
Forward(10) 會(huì)生錯(cuò)誤因?yàn)樗?2匹配。 - 連續(xù)三次調(diào)用
Forward(20) 不會(huì)有錯(cuò)誤因?yàn)樗?1匹配。 一旦匹配,該預(yù)期會(huì)被一直綁定,即使執(zhí)行次數(shù)達(dá)到上限之后,還是是生效的,這就是為什么三次調(diào)用 Forward(10) 超過(guò)了2號(hào)EXPECT_CALL 的上限時(shí),不會(huì)去試圖綁定1號(hào)EXPECT_CALL 而是報(bào)錯(cuò)的原因。 為了明確地讓某一個(gè)EXPECT_CALL “退休”,可以加上RetiresOnSaturation() ,例子: using ::testing::Return;
EXPECT_CALL(turtle, GetX()) // #1
.WillOnce(Return(10))
.RetiresOnSaturation();
EXPECT_CALL(turtle, GetX()) // #2
.WillOnce(Return(20))
.RetiresOnSaturation();
turtle.GetX() // 與#2匹配,返回20,然后#2“退休”
turtle.GetX() // 與#1匹配,返回10
在這個(gè)例子中,第一次GetX() 調(diào)用和#2匹配,返回20 ,然后這個(gè)EXPECT_CALL 就“退休”了; 第二次GetX() 調(diào)用和#1匹配,返回10 4.6. Sequence 可以用sequence來(lái)指定期望匹配的順序。 using ::testing::Return;
using ::testing::Sequence;
Sequence s1, s2;
...
EXPECT_CALL(foo, Reset())
.InSequence(s1, s2)
.WillOnce(Return(true));
EXPECT_CALL(foo, GetSize())
.InSequence(s1)
.WillOnce(Return(1));
EXPECT_CALL(foo, Describe(A<const char*>()))
.InSequence(s2)
.WillOnce(Return("dummy"));
在上面的例子中,創(chuàng)建了兩個(gè)Sequence s1 和s2 ,屬于s1 的有Reset() 和GetSize() , 所以Reset() 必須在GetSize() 之前執(zhí)行。屬于s2 的有Reset() 和Describe(A<const char*>()) , 所以Reset() 必須在Describe(A<const char*>()) 之前執(zhí)行。所以,Reset() 必須在GetSize() 和Describe() 之前執(zhí)行。而GetSize() 和Describe() 這兩者之間沒(méi)有順序約束。 如果需要指定很多期望的順序,有另一種用法: using ::testing::InSequence;
{
InSequence seq;
EXPECT_CALL(...)...;
EXPECT_CALL(...)...;
...
EXPECT_CALL(...)...;
}
在這種用法中,scope中(大括號(hào)中)的期望必須遵守嚴(yán)格的順序。 5. 更多 六、情景示例 在這部分,我們用一個(gè)示例項(xiàng)目來(lái)演示,如何在不同情景中使用 Google Test和Google Mock寫(xiě)單元測(cè)試用例。 1. 項(xiàng)目結(jié)構(gòu) 示例項(xiàng)目是一個(gè)C++命令行聊天室軟件,包含服務(wù)器和客戶(hù)端。 .
├── CMakeLists.txt
├── README.md
├── client_main.cpp
├── server_main.cpp
├── include
│ ├── chat_client.hpp
│ ├── chat_message.hpp
│ ├── chat_participant.hpp
│ ├── chat_room.hpp
│ ├── chat_server.hpp
│ ├── chat_session.hpp
│ ├── http_request.hpp
│ ├── http_request_impl.hpp
│ ├── message_dao.hpp
│ └── message_dao_impl.hpp
├── src
│ ├── chat_client.cpp
│ ├── chat_message.cpp
│ ├── chat_room.cpp
│ ├── chat_server.cpp
│ ├── chat_session.cpp
│ ├── http_request_impl.cpp
│ └── message_dao_impl.cpp
└── tests
├── chat_message_unittest.cpp
└── chat_room_unittest.cpp
2. 普通測(cè)試 如果被測(cè)試的函數(shù)不包含外部依賴(lài),用Google Test基礎(chǔ)的用法就可以完成用例編寫(xiě)。 原函數(shù): void chat_message::body_length(std::size_t new_length) {
body_length_ = new_length;
if (body_length_ > 512)
body_length_ = 512;
}
這個(gè)函數(shù)很簡(jiǎn)單。就是給body_length_ 賦值但是有最大值限制。測(cè)試用例可以這樣寫(xiě): TEST(ChatMessageTest, BodyLengthNegative) {
chat_message c;
c.body_length(-50);
EXPECT_EQ(512, c.body_length());
}
TEST(ChatMessageTest, BodyLength0) {
chat_message c;
c.body_length(0);
EXPECT_EQ(0, c.body_length());
}
TEST(ChatMessageTest, BodyLength100) {
chat_message c;
c.body_length(100);
EXPECT_EQ(100, c.body_length());
}
TEST(ChatMessageTest, BodyLength512) {
chat_message c;
c.body_length(512);
EXPECT_EQ(512, c.body_length());
}
TEST(ChatMessageTest, BodyLength513) {
chat_message c;
c.body_length(513);
EXPECT_EQ(512, c.body_length());
}
我們可以看到,對(duì)于這類(lèi)函數(shù),用例編寫(xiě)很直接簡(jiǎn)單,步驟都是構(gòu)造變量,再用合適的Google Test的 宏來(lái)驗(yàn)證變量值或者函數(shù)調(diào)用返回值。 3. 簡(jiǎn)單 Mock 原函數(shù) void chat_room::leave(chat_participant_ptr participant) {
participants_.erase(participant);
}
participants_ 的類(lèi)型是 std::set<chat_participant_ptr> 。這個(gè)函數(shù)的目的很明顯,將一個(gè)participant 從set 中移除。 真實(shí)地創(chuàng)建一個(gè)聊天參與者participant 對(duì)象可以條件比較苛刻或者成本比較高。為了有效率地驗(yàn)證這個(gè)函數(shù),我們可以新建一些Mock的chat_participant_ptr 而不用嚴(yán)格地去創(chuàng)建真實(shí)的participant 對(duì)象。 chat_participant 對(duì)象: class chat_participant {
public:
virtual ~chat_participant() {}
virtual void deliver(const chat_message &msg) = 0;
};
Mock對(duì)象: class mock_chat_participant : public chat_participant {
public:
MOCK_METHOD(void, deliver, (const chat_message &msg), (override));
};
測(cè)試用例: TEST(ChatRoomTest, leave) {
auto p1 = std::make_shared<mock_chat_participant>(); //新建第一個(gè)Mock指針
auto p2 = std::make_shared<mock_chat_participant>(); //新建第二個(gè)Mock指針
auto p3 = std::make_shared<mock_chat_participant>(); //新建第三個(gè)Mock指針
auto p4 = std::make_shared<mock_chat_participant>(); //新建第四個(gè)Mock指針
chat_room cr; //新建待測(cè)試對(duì)象chat_room
cr.join(p1);
cr.join(p2);
cr.join(p3);
cr.join(p4);
EXPECT_EQ(cr.participants().size(), 4);
cr.leave(p4);
EXPECT_EQ(cr.participants().size(), 3);
cr.leave(p4);
EXPECT_EQ(cr.participants().size(), 3);
cr.leave(p2);
EXPECT_EQ(cr.participants().size(), 2);
}
4. Web請(qǐng)求 chat_room 中有一個(gè)log() ,依賴(lài)網(wǎng)絡(luò)請(qǐng)求。原函數(shù): std::string chat_room::log() {
std::string* response;
this->requester->execute("request",response); // web訪(fǎng)問(wèn),結(jié)果存在response指針中
return *response;
}
在單元測(cè)試中,我們只關(guān)心被測(cè)試部分的邏輯。為了測(cè)試這個(gè)函數(shù),我們不應(yīng)該創(chuàng)建真實(shí)的requester ,應(yīng)該使用mock。 http_request 對(duì)象: class http_request {
public:
virtual ~http_request(){}
virtual bool execute(std::string request, std::string* response)=0;
};
Mock對(duì)象: class mock_http_request : public http_request {
public:
MOCK_METHOD(bool, execute, (std::string request, std::string * response), (override));
};
測(cè)試用例: TEST(ChatRoomTest, log) {
testing::NiceMock<mock_message_dao> mock_dao; //在下一部分會(huì)提到mock_message_dao
mock_http_request mock_requester; //Mock對(duì)象
std::string response = "response"; //期待調(diào)用函數(shù)的第二個(gè)參數(shù)將指向這個(gè)string對(duì)象
EXPECT_CALL(mock_requester, execute)
.WillRepeatedly( //每次調(diào)用都會(huì)(WillRepeatedly)執(zhí)行
testing::DoAll( //每次執(zhí)行包含多個(gè)行為
testing::SetArgPointee<1>(response),//將傳入?yún)?shù)指針變量response指向response
testing::Return(true))); //返回值為true
chat_room cr
= chat_room(&mock_dao, &mock_requester); //將mock對(duì)象通過(guò)chat_room的constructor注入
EXPECT_EQ(cr.log(),"response"); //調(diào)用和Google Test斷言
}
5. 數(shù)據(jù)庫(kù)訪(fǎng)問(wèn) chat_room 對(duì)象會(huì)將聊天者發(fā)送的消息存儲(chǔ)在redis數(shù)據(jù)庫(kù)中。當(dāng)新用戶(hù)加入時(shí),chat_room 對(duì)象從數(shù)據(jù)庫(kù) 獲取所有歷史消息發(fā)送給該新用戶(hù)。 join() 函數(shù): void chat_room::join(chat_participant_ptr participant) {
participants_.insert(participant);
std::vector<std::string> recent_msg_strs =
this->dao->get_messages(); //從數(shù)據(jù)庫(kù)中獲取歷史消息
for (std::string recent_msg_str: recent_msg_strs) {
//將每一個(gè)消息發(fā)送給該聊天參與者
auto msg = chat_message();
msg.set_body_string(recent_msg_str);
participant->deliver(msg);
}
}
message_dao 對(duì)象: class message_dao {
public:
virtual ~message_dao(){}
virtual bool add_message(std::string m)=0;
virtual std::vector<std::string> get_messages()=0;
};
Mock對(duì)象: class mock_message_dao : public message_dao {
public:
MOCK_METHOD(bool, add_message, (std::string m), (override));
MOCK_METHOD(std::vector<std::string>, get_messages, (), (override));
};
測(cè)試用例: TEST(ChatRoomTest, join) {
mock_message_dao mock_dao; //創(chuàng)建mock對(duì)象(需要注入chat_room)
http_request_impl requester; //創(chuàng)建web訪(fǎng)問(wèn)對(duì)象(也需要注入chat_room)
auto mock_p1 = std::make_shared<mock_chat_participant>();
//創(chuàng)建participant的mock指針
EXPECT_CALL(mock_dao, get_messages)
.WillOnce(testing::Return(std::vector<std::string>{"test_msg_body_1", "test_msg_body_2", "test_msg_body_3"}));
//指定get_messages調(diào)用的返回值
EXPECT_CALL(*mock_p1, deliver).Times(3);
//指定deliver調(diào)用的次數(shù)
chat_room cr = chat_room(&mock_dao, &requester);
//創(chuàng)建chat_room對(duì)象,注入dao和requester
cr.join(mock_p1); //調(diào)用
}
先創(chuàng)建mock對(duì)象,再指定函數(shù)調(diào)用的預(yù)期,最后指向被測(cè)試函數(shù)。我們可以看到,mock_dao 指定了get_messages 的 返回值時(shí)一個(gè)長(zhǎng)度為3的vector,所以有3條消息會(huì)被deliver。 七、FAQ 1. 單元測(cè)試源文件應(yīng)該放在項(xiàng)目的什么位置? 一般來(lái)說(shuō),我們會(huì)在根目錄創(chuàng)建一個(gè)tests 文件夾,里面放單元測(cè)試部分的源代碼,從而不會(huì)和被測(cè)試代碼混在一起。 如果需要和其他測(cè)試(如接口測(cè)試、壓力測(cè)試)等區(qū)分開(kāi)來(lái),可以 - 把
tests 改成unittests 、utests 等,或者 - 在
tests 創(chuàng)建不同子文件夾存放不同類(lèi)型的測(cè)試代碼。 2. Google Mock只能Mock虛函數(shù),如果我想Mock非虛函數(shù)怎么辦? 由于Google Mock(及其他大部分Mock框架)通過(guò)繼承來(lái)動(dòng)態(tài)重載機(jī)制的限制,一般來(lái)說(shuō)Google Mock只能Mock虛函數(shù)。如果要mock非虛函數(shù),官方文檔提供這幾種思路: - Mock類(lèi)和原類(lèi)沒(méi)有繼承關(guān)系,在測(cè)試對(duì)象使用函數(shù)模板。在測(cè)試中,測(cè)試對(duì)象接受Mock類(lèi)。
- 創(chuàng)建一個(gè)接口(抽象類(lèi)),原類(lèi)繼承自這個(gè)接口(抽象類(lèi))。在測(cè)試中Mock這個(gè)接口(抽象類(lèi))。
這兩種方法,都需要對(duì)代碼進(jìn)行一定的修改或重構(gòu)。如果不想修改被測(cè)試代碼??梢钥紤]使用hook技術(shù)替換被mock的部分從而mock一般函數(shù)。 使用TMock 對(duì)非虛函數(shù)mock的例子: mock函數(shù) # include "tmock.h"
class MockClass
{
public:
//注冊(cè)mock類(lèi)
TMOCK_CLASS(MockClass);
//聲明mock類(lèi)函數(shù),TMOCK_METHOD{n}第一個(gè)參數(shù)與attach_func_lib第一個(gè)參數(shù)相同,其余參考與MOCK_METHOD{n}一致。
TMOCK_METHOD1("original", original, uint32_t(const char * str_file_md5) )
};
單測(cè)中應(yīng)用tmock 的方法和Google Mock基本一致。但在結(jié)束的時(shí)候需要使用TMOCK_CLEAR 清除exception, detach hook的函數(shù),防止干擾其他單元測(cè)試。 3. Google Test官方文檔中說(shuō)測(cè)測(cè)試套件名稱(chēng)、測(cè)試夾具名稱(chēng)、測(cè)試名稱(chēng)中不應(yīng)該出現(xiàn)下劃線(xiàn)_ 。為什么? TEST(TestSuiteName, TestName) 生成名為TestSuiteName_TestName_Test 的類(lèi)。 下劃線(xiàn)_ 是特殊的,因?yàn)镃 ++保留以下內(nèi)容供編譯器和標(biāo)準(zhǔn)庫(kù)使用。所以開(kāi)頭和結(jié)尾有下劃線(xiàn)很容易讓生成的類(lèi)的標(biāo)識(shí)符不合法。 另一方面,下劃線(xiàn)可能讓不同測(cè)試生成相同的類(lèi)。比如TEST(Time,F(xiàn)lies_Like_An_Arrow){...} 和TEST(Time_Flies,Like_An_Arrow){...} 都生成名為Time_Flies_Like_An_Arrow_Test 的類(lèi)。 4. 測(cè)試輸出里有很多Uninteresting mock function call 警告怎么辦? 創(chuàng)建的Mock的對(duì)象的某些調(diào)用如果沒(méi)有相應(yīng)匹配的EXPECT_CALL ,Google Mock會(huì)生成這個(gè)警告。 為了去除這個(gè)警告,可以使用NiceMock 。比如如果原本使用MockFoo nice_foo; 新建mock對(duì)象的話(huà),可以改成NiceMock<MockFoo> nice_foo; 。NiceMock<MockFoo> 是MockFoo 的子類(lèi)。 八、結(jié)語(yǔ) 1. 實(shí)踐小結(jié) 和GoLang單元測(cè)試框架有些區(qū)別的是,GoLang自生就提供了自帶的測(cè)試框架,也有第三方框架進(jìn)行選擇。 而C/C++/php等語(yǔ)言的單元測(cè)試框架則需要第三方提供和安裝。 框架的使用,無(wú)非是一些語(yǔ)法糖的差異和使用的難易程度。不管使用什么語(yǔ)言,什么框架,最關(guān)鍵的是利用單元測(cè)試的思路, 寫(xiě)出解耦的、可測(cè)試的、易于維護(hù)的代碼,保證代碼的質(zhì)量。 單元測(cè)試是一種手段,能夠一定程度的改善生產(chǎn)力。凡事有度過(guò)猶不及,如果一味的盲目的追求測(cè)試覆蓋率, 忽視了測(cè)試代碼本身的質(zhì)量,那么各種無(wú)效的單元測(cè)試反而帶來(lái)了沉重的維護(hù)負(fù)擔(dān)。因此單測(cè)的代碼,本身也是代碼, 也是和項(xiàng)目本身的代碼一樣,需要重構(gòu)、維護(hù)的(好好寫(xiě)代碼)。 2. 特別鳴謝 感謝實(shí)習(xí)生鐘梓軒,在暑假實(shí)習(xí)期間,主導(dǎo)整理了C++單測(cè)的代碼示例和部分文章內(nèi)容。 3. 相關(guān)閱讀
|