googletest 與 googlemock 是 Google 公司於 2008 年發布的兩套用於單元測試的應用框架,本文將向讀者介紹如何應用這兩套應用框架輕鬆編寫 C++ 單元測試代碼。以下討論基於 gtest-1.2.1 及 gmock-1.0.0 。
單元測試概述
測試並不只是測試工程師的責任,對於開發工程師,為了保證發布給測試環節的代碼具有足夠好的質量( Quality ),為所編寫的功能代碼編寫適量的單元測試是十分必要的。
單元測試( Unit Test ,模塊測試)是開發者編寫的一小段代碼,用於檢驗被測代碼的一個很小的、很明確的功能是否正確,通過編寫單元測試可以在編碼階段發現程序編碼錯誤,甚至是程序設計錯誤。
單元測試不但可以增加開發者對於所完成代碼的自信,同時,好的單元測試用例往往可以在回歸測試的過程中,很好地保證之前所發生的修改沒有破壞已有的程序邏輯。因此,單元測試不但不會成為開發者的負擔,反而可以在保證開發質量的情況下,加速迭代開發的過程。
對於單元測試框架,目前最為大家所熟知的是 JUnit 及其針對各語言的衍生產品, C++ 語言所對應的 JUnit 系單元測試框架就是 CppUnit 。但是由於 CppUnit 的設計嚴格繼承自 JUnit ,而沒有充分考慮 C++ 與 Java 固有的差異(主要是由於 C++ 沒有反射機制,而這是 JUnit 設計的基礎),在 C++ 中使用 CppUnit 進行單元測試顯得十分繁瑣,這一定程度上制約了 CppUnit 的普及。筆者在這裡要跟大家介紹的是一套由 google 發布的開源單元測試框架( Testing Framework ): googletest 。
應用 googletest 編寫單元測試代碼
googletest 是由 Google 公司發布,且遵循 New BSD License (可用作商業用途)的開源項目,並且 googletest 可以支持絕大多數大家所熟知的平台。與 CppUnit 不同的是: googletest 可以自動記錄下所有定義好的測試,不需要用戶通過列舉來指明哪些測試需要運行。
定義單元測試
在應用 googletest 編寫單元測試時,使用 TEST() 宏來聲明測試函數。如:
TEST(GlobalConfigurationTest, configurationDataTest) TEST(GlobalConfigurationTest, noConfigureFileTest) |
分別針對同一程序單元 GlobalConfiguration 聲明了兩個不同的測試(Test)函數,以分別對配置數據進行檢查( configurationDataTest ),以及測試沒有配置文件的特殊情況( noConfigureFileTest )。
實現單元測試
針對同一程序單元設計出不同的測試場景后(即劃分出不同的 Test 后),開發者就可以編寫單元測試分別實現這些測試場景了。
在 googletest 中實現單元測試,可通過 ASSERT_* 和 EXPECT_* 斷言來對程序運行結果進行檢查。 ASSERT_* 版本的斷言失敗時會產生致命失敗,並結束當前函數; EXPECT_* 版本的斷言失敗時產生非致命失敗,但不會中止當前函數。因此, ASSERT_* 常常被用於後續測試邏輯強制依賴的處理結果的斷言,如創建對象后檢查指針是否為空,若為空,則後續對象方法調用會失敗;而 EXPECT_* 則用於即使失敗也不會影響後續測試邏輯的處理結果的斷言,如某個方法返回結果的多個屬性的檢查。
googletest 中定義了如下的斷言:
基本斷言 | 二進位比較 | 字元串比較 |
ASSERT_TRUE(condition); EXPECT_TRUE(condition); condition為真 ASSERT_FALSE(condition); EXPECT_FALSE(condition); condition為假 | ASSERT_EQ(expected,actual); EXPECT_EQ(expected,actual); expected==actual 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_STREQ(expected_str,actual_str); EXPECT_STREQ(expected_str,actual_str); 兩個 C 字元串有相同的內容 ASSERT_STRNE(str1,str2); EXPECT_STRNE(str1,str2); 兩個 C 字元串有不同的內容 ASSERT_STRCASEEQ(expected_str,actual_str); EXPECT_STRCASEEQ(expected_str,actual_str); 兩個 C 字元串有相同的內容,忽略大小寫 ASSERT_STRCASENE(str1,str2); EXPECT_STRCASENE(str1,str2); 兩個 C 字元串有不同的內容,忽略大小寫 |
下面的實例演示了上面部分斷言的使用:
// Configure.h #pragma once #include <string> #include <vector> class Configure { private: std::vector<std::string> vItems; public: int addItem(std::string str); std::string getItem(int index); int getSize(); }; // Configure.cpp #include "Configure.h" #include <algorithm> /** * @brief Add an item to configuration store. Duplicate item will be ignored * @param str item to be stored * @return the index of added configuration item */ int Configure::addItem(std::string str) { std::vector<std::string>::const_iterator vi=std::find(vItems.begin(), vItems.end(), str); if (vi != vItems.end()) return vi - vItems.begin(); vItems.push_back(str); return vItems.size() - 1; } /** * @brief Return the configure item at specified index. * If the index is out of range, "" will be returned * @param index the index of item * @return the item at specified index */ std::string Configure::getItem(int index) { if (index >= vItems.size()) return ""; else return vItems.at(index); } /// Retrieve the information about how many configuration items we have had int Configure::getSize() { return vItems.size(); } // ConfigureTest.cpp #include <gtest/gtest.h> #include "Configure.h" TEST(ConfigureTest, addItem) { // do some initialization Configure* pc = new Configure(); // validate the pointer is not null ASSERT_TRUE(pc != NULL); // call the method we want to test pc->addItem("A"); pc->addItem("B"); pc->addItem("A"); // validate the result after operation EXPECT_EQ(pc->getSize(), 2); EXPECT_STREQ(pc->getItem(0).c_str(), "A"); EXPECT_STREQ(pc->getItem(1).c_str(), "B"); EXPECT_STREQ(pc->getItem(10).c_str(), ""); delete pc; } |
運行單元測試
在實現完單元測試的測試邏輯后,可以通過 RUN_ALL_TESTS() 來運行它們,如果所有測試成功,該函數返回 0,否則會返回 1 。 RUN_ALL_TESTS() 會運行你鏈接到的所有測試??它們可以來自不同的測試案例,甚至是來自不同的文件。
因此,運行 googletest 編寫的單元測試的一種比較簡單可行的方法是:
#include <gtest/gtest.h> int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); // Runs all tests using Google Test. return RUN_ALL_TESTS(); } |
此外,在運行可執行目標程序時,可以使用 --gtest_filter 來指定要執行的測試用例,如:
這一特性在包含大量測試用例的項目中會十分有用。
應用 googlemock 編寫 Mock Objects
很多 C++ 程序員對於 Mock Objects (模擬對象)可能比較陌生,模擬對象主要用於模擬整個應用程序的一部分。在單元測試用例編寫過程中,常常需要編寫模擬對象來隔離被測試單元的“下游”或“上游”程序邏輯或環境,從而達到對需要測試的部分進行隔離測試的目的。
例如,要對一個使用資料庫的對象進行單元測試,安裝、配置、啟動資料庫、運行測試,然後再卸裝資料庫的方式,不但很麻煩,過於耗時,而且容易由於環境因素造成測試失敗,達不到單元測試的目的。模仿對象提供了解決這一問題的方法:模仿對象符合實際對象的介面,但只包含用來“欺騙”測試對象並跟蹤其行為的必要代碼。因此,其實現往往比實際實現類簡單很多。
為了配合單元測試中對 Mocking Framework 的需要, Google 開發並於 2008 年底開放了: googlemock 。與 googletest 一樣, googlemock 也是遵循 New BSD License (可用作商業用途)的開源項目,並且 googlemock 也可以支持絕大多數大家所熟知的平台。
注 1:在 Windows 平台上編譯 googlemock
對於 Linux 平台開發者而言,編譯 googlemock 可能不會遇到什麼麻煩;但是對於 Windows 平台的開發者,由於 Visual Studio 還沒有提供 tuple ( C++0x TR1 中新增的數據類型)的實現,編譯 googlemock 需要為其指定一個 tuple 類型的實現。著名的開源 C++ 程序庫 boost 已經提供了 tr1 的實現,因此,在 Windows 平台下可以使用 boost 來編譯 googlemock 。為此,需要修改 %GMOCK_DIR%/msvc/gmock_config.vsprops ,設定其中 BoostDir 到 boost 所在的目錄,如:
<UserMacro Name="BoostDir" Value="$(BOOST_DIR)" /> |
其中 BOOST_DIR 是一個環境變數,其值為 boost 庫解壓后所在目錄。
對於不希望在自己的開發環境上解包 boost 庫的開發者,在 googlemock 的網站上還提供了一個從 boost 庫中單獨提取出來的 tr1 的實現,可將其下載后將解壓目錄下的 boost 目錄拷貝到 %GMOCK_DIR% 下(這種情況下,請勿修改上面的配置項;建議對 boost 不甚了解的開發者採用後面這種方式)。
在應用 googlemock 來編寫 Mock 類輔助單元測試時,需要:
#include <gmock/gmock.h> // Brings in Google Mock. class MockTurtle : public Turtle { MOCK_METHOD0(PenUp, void()); MOCK_METHOD0(PenDown, void()); MOCK_METHOD1(Forward, void(int distance)); MOCK_METHOD1(Turn, void(int degrees)); MOCK_METHOD2(GoTo, void(int x, int y)); MOCK_CONST_METHOD0(GetX, int()); MOCK_CONST_METHOD0(GetY, int()); }; |
using testing::Return; // #1,必要的聲明 TEST(BarTest, DoesThis) { MockFoo foo; // #2,創建 Mock 對象 ON_CALL(foo, GetSize()) // #3,設定 Mock 對象默認的行為(可選) .WillByDefault(Return(1)); // ... other default actions ... EXPECT_CALL(foo, Describe(5)) // #4,設定期望對象被訪問的方式及其響應 .Times(3) .WillRepeatedly(Return("Category 5")); // ... other expectations ... EXPECT_EQ("good", MyProductionFunction(&foo)); // #5,操作 Mock 對象並使用 googletest 提供的斷言驗證處理結果 } // #6,當 Mock 對象被析構時, googlemock 會對結果進行驗證以判斷其行為是否與所有設定的預期一致 |
其中, WillByDefault 用於指定 Mock 方法被調用時的默認行為; Return 用於指定方法被調用時的返回值; Times 用於指定方法被調用的次數; WillRepeatedly 用於指定方法被調用時重複的行為。
對於未通過 EXPECT_CALL 聲明而被調用的方法,或不滿足 EXPECT_CALL 設定條件的 Mock 方法調用, googlemock 會輸出警告信息。對於前一種情況下的警告信息,如果開發者並不關心這些信息,可以使用 Adapter 類模板 NiceMock 避免收到這一類警告信息。如下:
testing::NiceMock<MockFoo> nice_foo; |
在筆者開發的應用中,被測試單元會通過初始化時傳入的上層應用的介面指針,產生大量的處理成功或者失敗的消息給上層應用,而開發者在編寫單元測試時並不關心這些消息的內容,通過使用 NiceMock 可以避免為不關心的方法編寫 Mock 代碼(注意:這些方法仍需在 Mock 類中聲明,否則 Mock 類會被當作 abstract class 而無法實例化)。
與 googletest 一樣,在編寫完單元測試后,也需要編寫一個如下的入口函數來執行所有的測試:
#include <gtest/gtest.h> #include <gmock/gmock.h> int main(int argc, char** argv) { testing::InitGoogleMock(&argc, argv); // Runs all tests using Google Test. return RUN_ALL_TESTS(); } |
下面的代碼演示了如何使用 googlemock 來創建 Mock Objects 並設定其行為,從而達到對核心類 AccountService 的 transfer (轉賬)方法進行單元測試的目的。由於 AccountManager 類的具體實現涉及資料庫等複雜的外部環境,不便直接使用,因此,在編寫單元測試時,我們用 MockAccountManager 替換了具體的 AccountManager 實現。
// Account.h // basic application data class #pragma once #include <string> class Account { private: std::string accountId; long balance; public: Account(); Account(const std::string& accountId, long initialBalance); void debit(long amount); void credit(long amount); long getBalance() const; std::string getAccountId() const; }; // Account.cpp #include "Account.h" Account::Account() { } Account::Account(const std::string& accountId, long initialBalance) { this->accountId = accountId; this->balance = initialBalance; } void Account::debit(long amount) { this->balance -= amount; } void Account::credit(long amount) { this->balance += amount; } long Account::getBalance() const { return this->balance; } std::string Account::getAccountId() const { return accountId; } // AccountManager.h // the interface of external services which should be mocked #pragma once #include <string> #include "Account.h" class AccountManager { public: virtual Account findAccountForUser(const std::string& userId) = 0; virtual void updateAccount(const Account& account) = 0; }; // AccountService.h // the class to be tested #pragma once #include <string> #include "Account.h" #include "AccountManager.h" class AccountService { private: AccountManager* pAccountManager; public: AccountService(); void setAccountManager(AccountManager* pManager); void transfer(const std::string& senderId, const std::string& beneficiaryId, long amount); }; // AccountService.cpp #include "AccountService.h" AccountService::AccountService() { this->pAccountManager = NULL; } void AccountService::setAccountManager(AccountManager* pManager) { this->pAccountManager = pManager; } void AccountService::transfer(const std::string& senderId, const std::string& beneficiaryId, long amount) { Account sender = this->pAccountManager->findAccountForUser(senderId); Account beneficiary = this->pAccountManager->findAccountForUser(beneficiaryId); sender.debit(amount); beneficiary.credit(amount); this->pAccountManager->updateAccount(sender); this->pAccountManager->updateAccount(beneficiary); } |
// AccountServiceTest.cpp // code to test AccountService #include <map> #include <string> #include <gtest/gtest.h> #include <gmock/gmock.h> #include "../Account.h" #include "../AccountService.h" #include "../AccountManager.h" // MockAccountManager, mock AccountManager with googlemock class MockAccountManager : public AccountManager { public: MOCK_METHOD1(findAccountForUser, Account(const std::string&)); MOCK_METHOD1(updateAccount, void(const Account&)); }; // A facility class acts as an external DB class AccountHelper { private: std::map<std::string, Account> mAccount; // an internal map to store all Accounts for test public: AccountHelper(std::map<std::string, Account>& mAccount); void updateAccount(const Account& account); Account findAccountForUser(const std::string& userId); }; AccountHelper::AccountHelper(std::map<std::string, Account>& mAccount) { this->mAccount = mAccount; } void AccountHelper::updateAccount(const Account& account) { this->mAccount[account.getAccountId()] = account; } Account AccountHelper::findAccountForUser(const std::string& userId) { if (this->mAccount.find(userId) != this->mAccount.end()) return this->mAccount[userId]; else return Account(); } // Test case to test AccountService TEST(AccountServiceTest, transferTest) { std::map<std::string, Account> mAccount; mAccount["A"] = Account("A", 3000); mAccount["B"] = Account("B", 2000); AccountHelper helper(mAccount); MockAccountManager* pManager = new MockAccountManager(); // specify the behavior of MockAccountManager // always invoke AccountHelper::findAccountForUser // when AccountManager::findAccountForUser is invoked EXPECT_CALL(*pManager, findAccountForUser(testing::_)).WillRepeatedly( testing::Invoke(&helper, &AccountHelper::findAccountForUser)); // always invoke AccountHelper::updateAccount //when AccountManager::updateAccount is invoked EXPECT_CALL(*pManager, updateAccount(testing::_)).WillRepeatedly( testing::Invoke(&helper, &AccountHelper::updateAccount)); AccountService as; // inject the MockAccountManager object into AccountService as.setAccountManager(pManager); // operate AccountService as.transfer("A", "B", 1005); // check the balance of Account("A") and Account("B") to //verify that AccountService has done the right job EXPECT_EQ(1995, helper.findAccountForUser("A").getBalance()); EXPECT_EQ(3005, helper.findAccountForUser("B").getBalance()); delete pManager; } // Main.cpp #include <gtest/gtest.h> #include <gmock/gmock.h> int main(int argc, char** argv) { testing::InitGoogleMock(&argc, argv); // Runs all tests using Google Test. return RUN_ALL_TESTS(); } |
注 2:上述範例工程詳見附件。要編譯該工程,請讀者自行添加環境變數 GTEST_DIR 、 GMOCK_DIR ,分別指向 googletest 、 googlemock 解壓后所在目錄;對於 Windows 開發者,還需要將 %GMOCK_DIR%/msvc/gmock_config.vsprops 通過 View->Property Manager 添加到工程中,並將 gmock.lib 拷貝到工程目錄下。
通過上面的實例可以看出, googlemock 為開發者設定 Mock 類行為,跟蹤程序運行過程及結果,提供了豐富的支持。但與此同時,應用程序也應該盡量降低應用代碼間的耦合度,使得單元測試可以很容易對被測試單元進行隔離(如上例中, AccountService 必須提供了相應的方法以支持 AccountManager 的替換)。關於如何通過應用設計模式來降低應用代碼間的耦合度,從而編寫出易於單元測試的代碼,請參考本人的另一篇文章《應用設計模式編寫易於單元測試的代碼》( developerWorks , 2008 年 7 月)。
注 3:此外,開發者也可以直接通過繼承被測試類,修改與外圍環境相關的方法的實現,達到對其核心方法進行單元測試的目的。但由於這種方法直接改變了被測試類的行為,同時,對被測試類自身的結構有一些要求,因此,適用範圍比較小,筆者也並不推薦採用這種原始的 Mock 方式來進行單元測試。
總結
Googletest 與 googlemock 的組合,很大程度上簡化了開發者進行 C++ 應用程序單元測試的編碼工作,使得單元測試對於 C++ 開發者也可以變得十分輕鬆;同時, googletest 及 googlemock 目前仍在不斷改進中,相信隨著其不斷發展,這一 C++ 單元測試的全新組合將變得越來越成熟、越來越強大,也越來越易用。(責任編輯:A6)
[火星人 ] 輕鬆編寫 C++ 單元測試已經有4188次圍觀