C++ 單元測試框架 gmock 深度剖析
隨著微服務和CI的流行,在目前的軟體工程領域中單元測試可以說是必不可少的一個環節,在TDD中,單元測試更是被提高到了一個新的高度。但是很多公司由於很多不同的原因,沒有能持續維護,或者乾脆就從來沒有寫過單元測試,確實,單元測試在初期和程式碼維護期會需要花一些投入,但是,如果一個專案是需要長期維護和更新的,那麼單元測試的作用,相對於投入來說就根本不算什麼。見過很多人寫的單元測試,雖然也可以執行,也有覆蓋率,但是稍微分析一下就會看出來,那根本就不是單元測試,而已經是整合測試,比如有人竟然要在單元測試中訪問網路,寫檔案,甚至讀寫資料庫。。
那麼什麼樣的資料庫是好的單元測試呢,根據筆者的經驗,以下幾點可能是必須的:
1. 執行速度快,對於一個有幾百個單元測試用例的測試來說,我期待1-2分鐘內可以執行完成,應為如果我在重構程式碼,這可以讓我在很快的時間內得到反饋。
2. 不要依賴外部因素,單元測試只針對單一函式功能測試
3. 一個用例只測試一個函式
對於其中的第二點,可能是比較麻煩的,因為,如果一個函式是型別的成員函式,那麼很可能會依賴很多內部的成員變數,這種情況就是mock出場的時候了,因為使用mock才能讓我們專注於自己函式一業務邏輯的測試,而將依賴隔離開。筆者使用過很多種語言的mock庫,用的最順手的還是Java的mokito, 當然c++ 語言也有很多類似的產品,比如gmock, fake it, 但是其侷限性確實比較多,如果不在程式碼開始階段瞭解,並且做好計劃,後期想加入單元測試,並且使用gmock的時候可能就會追悔莫及,大動干戈,下面我們來分場景分析一下這些侷限性。
場景1:
class TurtleReal {
public:
void PenUp()
{
}
void PenDown()
{
}
};
class MockTurtleReal : public TurtleReal {
public:
MOCK_METHOD0(PenUp, void());
MOCK_METHOD0(PenDown, void());
};
class PainterdReal
{
TurtleReal* turtle;
public:
PainterdReal(TurtleReal* turtle)
: turtle(turtle) {}
bool DrawCircle(int, int, int) {
turtle->PenDown();
return true;
}
};
TEST(PainterTest, ChildRealCanDrawSomething) {
MockTurtleReal turtle;
EXPECT_CALL(turtle, PenDown())
.Times(AtLeast(1));
PainterdReal painter(&turtle);
EXPECT_TRUE(painter.DrawCircle(0, 0, 10));
}
結果1:
結論一:
為什麼用例會失敗呢,gmock 依賴C++多型機制進行工作,只有虛擬函式才能被mock, 非虛擬函式不能被mock, 這一點告訴我們,如果想要在程式碼中使用gmock類的設計中,最好採用介面隔離,對於c++來說也就是採用純虛型別,因為c++本身沒有介面型別。
場景2:
class Turtle {
public:
virtual ~Turtle() {}
virtual void PenUp() = 0;
virtual void PenDown() = 0;
};
class MockTurtle : public Turtle {
public:
MOCK_METHOD0(PenUp, void());
MOCK_METHOD0(PenDown, void());
};
class Painter
{
Turtle* turtle;
public:
Painter(Turtle* turtle)
: turtle(turtle) {}
bool DrawCircle(int, int, int) {
turtle->PenDown();
return true;
}
};
TEST(PainterTest, CanDrawSomething) {
MockTurtle turtle;
EXPECT_CALL(turtle, PenDown())
.Times(AtLeast(1));
Painter painter(&turtle);
EXPECT_TRUE(painter.DrawCircle(0, 0, 10));
}
結果2:
結論二:
將函式改為虛擬函式,測試用例通過
場景3:
class TurtleChild: Turtle {
public:
void PenUp()
{
int a = 0;
};
void PenDown()
{
int b = 0;
};
};
class MockTurtleChild : public TurtleChild {
public:
MOCK_METHOD0(PenUp, void());
MOCK_METHOD0(PenDown, void());
};
class PainterChildRef
{
TurtleChild turtle;
public:
PainterChildRef(TurtleChild& turtle)
: turtle(turtle) {}
bool DrawCircle(int, int, int) {
turtle.PenDown();
return true;
}
};
TEST(PainterTest, ChildCanDrawSomething) {
MockTurtleChild turtle;
EXPECT_CALL(turtle, PenDown())
.Times(AtLeast(1));
PainterChild painter(&turtle);
EXPECT_TRUE(painter.DrawCircle(0, 0, 10));
}
結果3:
結論三:
測試用例通過,派生類中的同名函式仍然是虛擬函式,同樣支援多型,支援gomck
場景4:
class Turtle {
public:
virtual ~Turtle() {}
virtual void PenUp() = 0;
virtual void PenDown() = 0;
};
class TurtleChild: Turtle {
public:
void PenUp()
{
int a = 0;
};
void PenDown()
{
int b = 0;
};
};
class MockTurtleChild : public TurtleChild {
public:
MOCK_METHOD0(PenUp, void());
MOCK_METHOD0(PenDown, void());
};
class PainterChildRef
{
TurtleChild turtle;
public:
PainterChildRef(TurtleChild& turtle)
: turtle(turtle) {}
bool DrawCircle(int, int, int) {
turtle.PenDown();
return true;
}
};
TEST(PainterTest, ChildRefCanDrawSomething) {
MockTurtleChild turtle;
EXPECT_CALL(turtle, PenDown())
.Times(AtLeast(1));
PainterChildRef painter(turtle);
EXPECT_TRUE(painter.DrawCircle(0, 0, 10));
}
結果4:
結論四:
測試用例失敗,以引用型別傳入的成員變數本身不具備多型特性,因此gmock不支援
結論
本文通過四個場景,層層遞進,深入的剖析了gmock的使用,希望大家在寫程式碼之前早做打算,避免大動干戈,返工重來。但是從另一個方面來說,介面隔離, p-impl 慣用法等技術,應該是一個c++老鳥的必備法寶,可見好多東西都是有其道理的,前期不瞭解,後期只能花更多的精力取彌補,要麼推翻重構,要麼直接放棄,無知者無畏,no zuo, no die..