程式扎記: [Google test] Google C++ Testing Framework 入門


2011年7月21日 星期四

[Google test] Google C++ Testing Framework 入門

轉載自 這裡 
Preface : 
最近因為專案需求, 需要做 White box (source code) 層面的 Unit/Functional testing. 因而就去 Study 如何 leverage google test 來幫助我們進行 Unit/Functional testing. 因而有了這個 Post. 有興趣的可以直接去讀 google test 上的 wiki. 這裡只是將一些重點摘錄與翻成中文而已. ><" 

Introduction: Why Google C++ Testing Framework? 
google test 是基於 xUnit 的架構下開發的C/C++測試 Framework. 支援包含 Windows/Linux 等平台. 如果你原本就已經在使用 JUnit, PyUnit. 那恭喜你原本的測試概念也可以帶過來. 一開始我們先來看看為什們 google test 能幫助我們寫 C/C++ 的 tests. 並且還有它提供的 features 有哪些 : 

1. Tests should be independent and repeatable. It's a pain to debug a test that succeeds or fails as a result of other tests. Google C++ Testing Framework isolates the tests by running each of them on a different object. When a test fails, Google C++ Testing Framework allows you to run it in isolation for quick debugging.
2. Tests should be well organized and reflect the structure of the tested code. Google C++ Testing Framework groups related tests into test cases that can share data and subroutines. This common pattern is easy to recognize and makes tests easy to maintain. Such consistency is especially helpful when people switch projects and start to work on a new code base.
3. Tests should be portable and reusable. The open-source community has a lot of code that is platform-neutral, its tests should also be platform-neutral. Google C++ Testing Framework works on different OSes, with different compilers (gcc, MSVC, and others), with or without exceptions, so Google C++ Testing Framework tests can easily work with a variety of configurations. (Note that the current release only contains build scripts for Linux - we are actively working on scripts for other platforms.)
4. When tests fail, they should provide as much information about the problem as possibleGoogle C++ Testing Framework doesn't stop at the first test failure. Instead, it only stops the current test and continues with the next. You can also set up tests that report non-fatal failures after which the current test continues. Thus, you can detect and fix multiple bugs in a single run-edit-compile cycle.
5. The testing framework should liberate test writers from housekeeping chores and let them focus on the test contentGoogle C++ Testing Framework automatically keeps track of all tests defined, and doesn't require the user to enumerate them in order to run them.
6. Tests should be fast. With Google C++ Testing Framework, you can reuse shared resources across tests and pay for the set-up/tear-down only once, without making tests depend on each other.

看完了上述的說明, 有沒有被 google test 吸引? 說的總是比做的容易, 接著我們就來看看它能做些什麼, 並最後用一個範例來結束這個 Post. 

Setting up a New Test Project : 
在開始利用 google test 來撰寫我們的測試之前, 必須先去取的 google test 的 project (目前是 v1.6 下載). 下載下來後根據適當的平台進行編譯 : 

msvc/ for Visual Studio, xcode/ for Mac Xcode, make/ for GNU make, codegear/ for Borland C++ Builder, and the autotools script (deprecated) and CMakeLists.txt for CMake (recommended) in the Google Test root directory

編譯完後會得到 gtest.lib, 而我們的測試 Project 必須 link 到 它並且加入對應的 include path (能找到 "gtest/gtest.h") 到專案裡, 以 MS-VS 2005 來說, 請在你要寫測試的專案中: 
- 加入 include path : (在下載下來的 google test 專案目錄中, 找到目錄 "include", 並將之複製到你測試專案可以參考的路徑中) 

- 加入 gtest.lib 的 link : 
(加入 dependencies 的目錄) 

(加入 dependencies gtest.lib 的 link) 

(設定與 gtest 相同的 runtime library) 

測試專案的環設如果設置好, 接著我們就來看看一些Unit 測試使用的一些觀念與技巧吧. 

Basic Concepts : 
當撰寫 Google Test 時, 我們透過一些 assertions (ex. ASSERT_EQ(expect, actual)) 與 statements 來確認函式的呼叫結果是不是如我們所預期. 如函式的返回結果可能是 true, false. 如果返回結果與預期結果不符, 代表測試 failure. 這時這個測試就會結束. 但是有時一個測試過程可能會包含多個測試步驟, 如果你使用 assertion 則只要一個測試步驟 failure, 則接下來的測試步驟便不會往下跑 (也就是跑不到). 若你希望就算這個測試步驟 failure, 接下來的測試步驟能夠繼續的話, 那就要改用 expect 的語法 (ex. EXPECT_EQ(expect, actual)). 這兩者的差別就在於你使用 assertions 時, 代表如果這個步驟 failure 了, 接下來的步驟再跑下去也 make no senses. 但是使用 expect 的話代表每個步驟可能互相獨立, 即使其中一個 failure, 接下來的步驟也應該跑下去, 這樣便能多抓些bugs. 

接著要談的是在 google test 中, 一個 test case 是可以包含多個 tests. 譬如如果一個函式接收一個整數的參數與一個字串, 那麼也許會有一個專門測試整數參數的 test case, 而這個 test case 的 tests 可能包含 正數, 負數與 0值等. 因此你可以將這些 tests 群組到一個 "測數字參數" 的 test case 裡. 接著如果你希望能在多個 test cases 間 share 一些 common 的 objects 或 subroutines 時, 你便需要借助 test fixture class 了(之後會再介紹). 有了這些概念, 接著我們要介紹一些常用的 assertions 與 expect 語法. 

Assertions : 
Google Test 的 assertions 是由巨集寫成的 function calls. 你透過這些 assertions 來檢視/測試 目的函式或類別. 當一個 assertion fails, Google Test 將會列印出該 assertion 的出現行數與對應的原始檔案位置. 

你也可以透過一些方法讓 assertion 的 failure message 含你 customized 過後的 log 訊息 : 

  1. ASSERT_EQ(x.size(), y.size()) << "Vectors x and y are of unequal length";  
  3. for (int i = 0; i < x.size(); ++i) {  
  4.   EXPECT_EQ(x[i], y[i]) << "Vectors x and y differ at index " << i;  
  5. }  
如果你的訊息包含 wchar_t 等寬字元的內容, 則底下是 google test 的處理 : 

Anything that can be streamed to an ostream can be streamed to an assertion macro--in particular, C strings and string objects. If a wide string(wchar_t*, TCHAR* in UNICODE mode on Windows, or std::wstringis streamed to an assertion, it will be translated to UTF-8 when printed.

Basic Assertions : 
底下是基本的 Assertions 說明 : (適用平台 : Linux, Windows, Mac.) 

Binary Comparison : (適用平台 : Linux, Windows, Mac.) 
如果有返回值與預期值的比較, 可以參考下面 : (適用平台 : Linux, Windows, Mac.) 

這裡要注意的是如果你的比較值類型是 C String且傳入為 pointer, 則上面的函式是以 pointer 的位置做為比較的基準而不是字串的內容 (這可能才是你要的!) . 因此如果你想要比較的是字串的內容, 則可以使用 ASSERT_STREQ(). 底下是原文的說明 : 

ASSERT_EQ() does pointer equality on pointers. If used on two C strings, it tests if they are in the same memory location, not if they have the same value. Therefore, if you want to compare C strings (e.g. const char*) by value, use ASSERT_STREQ() , which will be described later on. In particular, to assert that a C string is NULL, use ASSERT_STREQ(NULL, c_string) . However, to compare two string objects, you should use ASSERT_EQ.

String Comparison : 
如果你要比較的是 C String 的內容, 則可以考慮下面介紹的巨集 : (適用平台 : Linux, Windows, Mac.) 

注意如果巨集的名稱有 "CASE" 的話, 字串的大小寫會被忽略. 另外 Null pointer 與 空字串會被認為是不同的結果. 如果需要更多與字串比較相關的內容 (substring, prefix, suffix, and regular expression matching, for example) 可以參考 Advanced Google Test Guide. 

Simple Tests : 
接著我們要來寫簡單的測試, 首先參考原文說明測試如何撰寫與判斷執行的結果 : 

1. Use the TEST() macro to define and name a test function, These are ordinary C++ functions that don't return a value.
2. In this function, along with any valid C++ statements you want to include, use the various Google Test assertions to check values.
3. The test's result is determined by the assertions; if any assertion in the test fails (either fatally or non-fatally), or if the test crashes, the entire test fails. Otherwise, it succeeds.

簡單的 TEST 範例如下 : 

  1. TEST(test_case_name, test_name) {  
  2. //... test body ...  
  3. }  
在 TEST 巨集的第一個參數是 test case 的名稱, 第二個參數是 test 的說明. 注意的是這兩個參數都不能包含字符 "_" (underscore).參考原文說明如下 : 

TEST() arguments go from general to specific. The first argument is the name of the test case, and the second argument is the test's name within the test case. Both names must be valid C++ identifiers, and they should not contain underscore (_). A test's full name consists of its containing test case and its individual name. Tests from different test cases can have the same individual name.

接著我們考慮函式 Factorial(n), 並試著為它撰寫 tests : 

  1. int Factorial(int n); // Returns the factorial of n  
一個簡單的 test case 範例如下 : 

  1. // Tests factorial of 0.  
  2. TEST(FactorialTest, HandlesZeroInput) {  
  3.   EXPECT_EQ(1, Factorial(0));  
  4. }  
  6. // Tests factorial of positive numbers.  
  7. TEST(FactorialTest, HandlesPositiveInput) {  
  8.   EXPECT_EQ(1, Factorial(1));  
  9.   EXPECT_EQ(2, Factorial(2));  
  10.   EXPECT_EQ(6, Factorial(3));  
  11.   EXPECT_EQ(40320, Factorial(8));  
  12. }  
從上面的範例, 我們撰寫了兩個 tests (HandlesZeroInput 與 HandlesPositiveInput), 而該兩個 tests 都屬於 test case "FactorialTest". 執行結果如下 : 

Test Fixtures: Using the Same Data Configuration for Multiple Tests 
如果你發現有某些 test cases 依賴於相同的 data 或 configuration, 則你可以試著建立 test fixture 來在這些 test cases 中重用共同的設定或 data. 而建立 test feature 的原文說明如下 : 

1. Derive a class from ::testing::Test . Start its body with protected: or public: as we'll want to access fixture members from sub-classes.
2. Inside the class, declare any objects you plan to use.
3. If necessary, write a default constructor or SetUp() function to prepare the objects for each test.
4. If necessary, write a destructor or TearDown() function to release any resources you allocated in SetUp() . To learn when you should use the constructor/destructor and when you should use SetUp()/TearDown(), read this FAQ entry.
5. If needed, define subroutines for your tests to share.

如果你要使用 test feature 時, 請改使用 TEST_F() 巨集 否則將不能 access 在 test fixture 定義的 物件s 或 subroutines. 如原先使用的 TEST(), 第一個參數是 test case 的名稱, 但在 TEST_F() 中 test case 的名稱必須是你定義的 test fixture 類別的名字. (_F 指的就是 fixture) 如果不這麼做你將會得到 "compiler error". 針對每一個 TEST_F() , Google Test 會 : 

1. Create a fresh test fixture at runtime
2. Immediately initialize it via SetUp() ,
3. Run the test
4. Clean up by calling TearDown()
5. Delete the test fixture. Note that different tests in the same test case have different test fixture objects, and Google Test always deletes a test fixture before it creates the next one. Google Test does not reuse the same test fixture for multiple tests. Any changes one test makes to the fixture do not affect other tests.

接著我們使用一個類別 JQueue 實現佇列, 並針對這個類別撰寫 test fixture 類別 "JQueueTest". 而 JQueue 類別的介面如下 : 

- JQueue.h :
  1. #ifndef __JQUEUE_H__  
  2. #define __JQUEUE_H__  
  4. #include   
  5. #include   
  7. using namespace std;  
  8. #define MAX_QUEUE_SIZE 20 // Define size of JQueue  
  10. class JQueue{  
  11. protected:  
  12.     int rear;  
  13.     int _queue[MAX_QUEUE_SIZE];  
  15. public:  
  16.     JQueue()  
  17.     {  
  18.         rear = -1;  
  19.     }  
  21.     bool add(int val); // Add data to queue  
  22.     bool isEmpty(); // Judge if queue is empty  
  23.     bool isFull(); // Judge if queue is full  
  24.     bool get(int *val); // Fetch the value from queue  
  25.     int length(); // Current data length in queue  
  26. };  
  29. #endif  

接著我們定義 test fixture 類別 JQueueTest 的介面 : 

- JQueueTest.h :
  1. #ifndef __JQUEUETEST_H__  
  2. #define __JQUEUETEST_H__  
  3. #include "gtest/gtest.h"  
  4. #include "JQueue.h"  
  6. class JQueueTest : public ::testing::Test {  
  7. protected:  
  8.     JQueueTest() {  
  9.         // You can do set-up work for each test here.  
  10.         cout << "\t[JQueueTest] Constructor..." << endl;  
  11.     }  
  13.     virtual ~JQueueTest() {  
  14.         // You can do clean-up work that doesn't throw exception here.  
  15.         cout << "\t[JQueueTest] Deconstructor..." << endl;  
  16.     }  
  18.     // If the constructor and destructor are not enough for setting up  
  19.     // and cleaning up each test, you can define the following methods.  
  21.     // Code here will be called immediately after the constructor (right before each test)  
  22.     virtual void SetUp();  
  24.     // Code here will be called immediately after each test (right before the destructor)  
  25.     virtual void TearDown();  
  27.     // Objects declared here can be used by all tests in the test case for Foo.  
  28.     JQueue _q0;  
  29.     JQueue _q1;  
  30.     JQueue _q2;  
  31. };  
  33. #endif  

最後我們使用 TEST_F() 撰寫 tests 如下 : 

- Test case :
  1. TEST_F(JQueueTest, IsEmptyInitially)  
  2. {  
  3.     EXPECT_EQ(0, _q0.length());  
  4. }  
  6. TEST_F(JQueueTest, DequeueWorks)  
  7. {  
  8.     int val=0;  
  9.     bool bRst = _q0.get(&val);  
  10.     EXPECT_EQ(false, bRst);  
  12.     bRst = _q1.get(&val);  
  13.     ASSERT_TRUE(bRst);  
  14.     EXPECT_EQ(2, val);  
  15.     EXPECT_EQ(0, _q1.length());  
  17.     bRst = _q2.get(&val);  
  18.     ASSERT_TRUE(bRst);  
  19.     EXPECT_EQ(3, val);  
  20.     EXPECT_EQ(1, _q2.length());  
  21. }  

當開始跑 test 時, Google Test 會 : 

1. Google Test constructs a JQueueTest object (let's call it t1).
2. t1.SetUp() initializes t1 .
3. The first test ( IsEmptyInitially ) runs on t1 .
4. t1.TearDown() cleans up after the test finishes.
5. t1 is destructed.
6. The above steps are repeated on another JQueueTest object, this time running the DequeueWorks test.

接著我們來看看 JQueue 與 JQueueTest 的實作 : 

- JQueue.cpp :
  1. #include "JQueue.h"  
  3. bool JQueue::add(int val)  
  4. {  
  5.     if(!this->isFull())  
  6.     {  
  7.         this->_queue[++rear] = val;  
  8.         return true;  
  9.     }  
  10.     cout <<  "\t[JQueue] Queue is full" << endl;  
  11.     return false;  
  12. }  
  14. bool JQueue::isFull()  
  15. {  
  16.     if((rear+1)==MAX_QUEUE_SIZE) return true;  
  17.     else return false;  
  18. }  
  20. bool JQueue::isEmpty()  
  21. {  
  22.     if(rear>=0return false;  
  23.     else return true;  
  24. }  
  26. bool JQueue::get(int *val)  
  27. {  
  28.     if(!this->isEmpty())  
  29.     {  
  30.         *val = this->_queue[0];  
  31.         for(int i=0; i
  32.         {  
  33.             this->_queue[i] = this->_queue[i+1];  
  34.         }  
  35.         rear--;  
  36.         return true;  
  37.     }  
  38.     cout << "\t[JQueue] Queue is empty" << endl;  
  39.     return false;  
  40. }  
  42. int JQueue::length()  
  43. {  
  44.     return rear+1;  
  45. }  

- JQueueTest.cpp :
  1. #include "JQueueTest.h"  
  3. void JQueueTest::SetUp()  
  4. {  
  5.     cout << "\t[JQueueTest] Setup resources..." << endl;  
  6.     this->_q1.add(2);  
  7.     this->_q2.add(3);  
  8.     this->_q2.add(4);  
  9. }  
  11. void JQueueTest::TearDown()  
  12. {  
  13.     cout << "\t[JQueueTest] Teardown resources..." << endl;  
  14. }  

執行結果 : 

Invoking the Tests : 
巨集 TEST()  TEST_F() 會將自己註冊到 Google Test. 並且在定義好 tests 後, 便可以呼叫 另一個巨集 RUN_ALL_TESTS() 來跑 tests. 如果該巨集回傳 0 表示所有的 tests 都 success, 否則返回 1 代表有某個或某些 tests failed. 底下是當你呼叫巨集 RUN_ALL_TESTS() 時 Google Test 的行為 : 

1. Saves the state of all Google Test flags.
2. Creates a test fixture object for the first test.
3. Initializes it via SetUp().
4. Runs the test on the fixture object.
5. Cleans up the fixture via TearDown().
6. Deletes the fixture.
7. Restores the state of all Google Test flags.
8. Repeats the above steps for the next test, until all tests have run.

最後該巨集的返回值必須就是 main 函數的返回值並且不可以忽略, 以下是原文的說明 : 

You must not ignore the return value of RUN_ALL_TESTS(), or gcc will give you a compiler error. The rationale for this design is that the automated testing service determines whether a test has passed based on its exit code, not on its stdout/stderr output; thus your main() function must return the value of RUN_ALL_TESTS().

Writing the main() Function : 
有了以上的說明, 要寫一個 main 函數就是非常簡單的一件事了. 唯一要注意的便是你可以使用 ::testing::InitGoogleTest() 來解析命令列的參數, 並藉由命令列的參數來動態改變 test program 的行為. 而更多說明請參考原文如下 : 

The ::testing::InitGoogleTest() function parses the command line for Google Test flags, and removes all recognized flags. This allows the user to control a test program's behavior via various flags, which we'll cover in AdvancedGuide. You must call this function before calling RUN_ALL_TESTS(), or the flags won't be properly initialized.

拿剛剛的 JQueue 說明撰寫 main 函數如下 : 

- main.cpp :
  1. #include   
  2. #include   
  3. #include "gtest/gtest.h"  
  4. #include "JQueueTest.h"  
  6. //===================================//  
  7. // Test Cases  
  8. //===================================//  
  9. // Tests JQueue for empty initialization  
  10. TEST_F(JQueueTest, IsEmptyInitially)  
  11. {  
  12.     EXPECT_EQ(0, _q0.length());  
  13. }  
  15. TEST_F(JQueueTest, DequeueWorks)  
  16. {  
  17.     int val=0;  
  18.     bool bRst = _q0.get(&val);  
  19.     EXPECT_EQ(false, bRst);  
  21.     bRst = _q1.get(&val);  
  22.     ASSERT_TRUE(bRst);  
  23.     EXPECT_EQ(2, val);  
  24.     EXPECT_EQ(0, _q1.length());  
  26.     bRst = _q2.get(&val);  
  27.     ASSERT_TRUE(bRst);  
  28.     EXPECT_EQ(3, val);  
  29.     EXPECT_EQ(1, _q2.length());  
  30. }  
  32. int main(int argc, char** argv)  
  33. {  
  34.     testing::InitGoogleTest(&argc, argv);  
  35.     return RUN_ALL_TESTS();  
  36. }  

Supplement : 
* Google Test Project Home : Google C++ Testing Framework 
* Google Unit Test Framework試玩

2 則留言:

  1. JQueue.cpp 31行的
    for(int i=0; i

    using namespace std;

    1. 我猜應該是我在 paste 文章到 google blog 時因為 "<" ">" 是 HTML 關鍵字, 所以造成上述的問題.
      我把整個 demo 的 VS2010 Project 放在 :
      裡面有源代碼與相關專案設定, 但是 gtest 可以還是要自己下載並重新編譯後加到專案的設定.




Where there is a will, there is a way!