1. 程式人生 > >google test 輕鬆編寫C++單元測試

google test 輕鬆編寫C++單元測試

簡介: 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() 巨集來宣告測試函式。如:


清單 1. 用 TEST() 巨集宣告測試函式
TEST(GlobalConfigurationTest, configurationDataTest) 
TEST(GlobalConfigurationTest, noConfigureFileTest)

分別針對同一程式單元 GlobalConfiguration 聲明瞭兩個不同的測試(Test)函式,以分別對配置資料進行檢查( configurationDataTest ),以及測試沒有配置檔案的特殊情況( noConfigureFileTest )。

實現單元測試


針對同一程式單元設計出不同的測試場景後(即劃分出不同的 Test 後),開發者就可以編寫單元測試分別實現這些測試場景了。

在 googletest 中實現單元測試,可通過 ASSERT_* 和 EXPECT_* 斷言來對程式執行結果進行檢查。 ASSERT_* 版本的斷言失敗時會產生致命失敗,並結束當前函式; EXPECT_* 版本的斷言失敗時產生非致命失敗,但不會中止當前函式。因此, ASSERT_* 常常被用於後續測試邏輯強制依賴的處理結果的斷言,如建立物件後檢查指標是否為空,若為空,則後續物件方法呼叫會失敗;而 EXPECT_* 則用於即使失敗也不會影響後續測試邏輯的處理結果的斷言,如某個方法返回結果的多個屬性的檢查。

googletest 中定義瞭如下的斷言:

表 1: googletest 定義的斷言( Assert )
基本斷言二進位制比較 字串比較
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>=val2ASSERT_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 字串有不同的內容,忽略大小寫


下面的例項演示了上面部分斷言的使用:

清單 2. 一個較完整的 googletest 單元測試例項
// 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 編寫的單元測試的一種比較簡單可行的方法是:


為每一個被測試的 class 分別建立一個測試檔案,並在該檔案中編寫針對這一 class 的單元測試;
編寫一個 Main.cpp 檔案,並在其中包含以下程式碼,以執行所有單元測試:


清單 3. 初始化 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(); 
 }


最後,將所有測試程式碼及 Main.cpp 編譯並連結到目標程式中。
此外,在執行可執行目標程式時,可以使用 --gtest_filter 來指定要執行的測試用例,如:


./foo_test 沒有指定 filter ,執行所有測試;
./foo_test --gtest_filter=* 指定 filter 為 * ,執行所有測試;
./foo_test --gtest_filter=FooTest.* 執行測試用例 FooTest 的所有測試;
./foo_test --gtest_filter=*Null*:*Constructor* 執行所有全名(即測試用例名 + “ . ” + 測試名,如 GlobalConfigurationTest.noConfigureFileTest )含有"Null" 或 "Constructor" 的測試;
./foo_test --gtest_filter=FooTest.*-FooTest.Bar 執行測試用例 FooTest 的所有測試,但不包括 FooTest.Bar。
這一特性在包含大量測試用例的專案中會十分有用。



斷言是也就是判斷一個條件是否為真的語句,它是構成gtest程式碼最基本的單元!

使用測試韌體可以去除一些重複的程式碼,測試韌體的作用在於管理兩個或多個測試例項都會使用到的資料

定義韌體類的方法為:

  1. 寫一個繼承自::testing::Test的類,為使該類的子類能訪問到該類的資料,使用public或protected作為訪問控制標識;
  2. 在該類中,定義測試例項將用到的資料;
  3. 使用SetUp()方法或預設建構函式作資料初始化操作,使用TearDown()方法或解構函式作資料清理操作,注意SetUp()和TearDown()的拼寫;
  4. 如有需要,還可以在該類中定義成員函式,正如初始化資料,這裡所定義的成員函式也可被測試例項重複使用

參考連線:測試韌體









應用 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 類輔助單元測試時,需要:

編寫一個 Mock Class (如 class MockTurtle ),派生自待 Mock 的抽象類(如 class Turtle );
對於原抽象類中各待 Mock 的 virtual 方法,計算出其引數個數 n ;
在 Mock Class 類中,使用 MOCK_METHODn() (對於 const 方法則需用 MOCK_CONST_METHODn() )巨集來宣告相應的 Mock 方法,其中第一個引數為待 Mock 方法的方法名,第二個引數為待 Mock 方法的型別。如下:

清單 4. 使用 MOCK_METHODn 宣告 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()); 
 };

在完成上述工作後,就可以開始編寫相應的單元測試用例了。在編寫單元測試時,可通過 ON_CALL 巨集來指定 Mock 方法被呼叫時的行為,或 EXPECT_CALL 巨集來指定 Mock 方法被呼叫的次數、被呼叫時需執行的操作等,並對執行結果進行檢查。如下:

清單 5. 使用 ON_CALL 及 EXPECT_CALL 巨集
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 避免收到這一類警告資訊。如下:

清單 6. 使用 NiceMock 模板
testing::NiceMock<MockFoo> nice_foo;

在筆者開發的應用中,被測試單元會通過初始化時傳入的上層應用的介面指標,產生大量的處理成功或者失敗的訊息給上層應用,而開發者在編寫單元測試時並不關心這些訊息的內容,通過使用 NiceMock 可以避免為不關心的方法編寫 Mock 程式碼(注意:這些方法仍需在 Mock 類中宣告,否則 Mock 類會被當作 abstract class 而無法例項化)。

與 googletest 一樣,在編寫完單元測試後,也需要編寫一個如下的入口函式來執行所有的測試:

清單 7. 初始化 googlemock 並執行所有測試
#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 實現。

清單 8. 待測試的程式邏輯
// 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); 
 }

清單 9. 相應的單元測試
// 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++ 單元測試的全新組合將變得越來越成熟、越來越強大,也越來越易用。