1. 程式人生 > >leveldb學習:測試程式碼

leveldb學習:測試程式碼

leveldb中帶有了大量的測試程式,凡是檔案命名中帶有_test的檔案都是測試程式碼。就我的理解,閱讀測試程式碼非常要,因為作為一個庫,leveldb並不帶有main函式,這也不利於我們理解leveldb的結構和流程,而測試程式碼相當程度上彌補了一點。並且,作為一個在校學生,我們在平時的專案中也極少考慮過寫測試程式碼,今天我就拋磚引玉,以我的水平來剖析下leveldb的測試部分。
leveldb的測試程式碼還是比較容易讓人摸不著頭腦的,至少我第一次看是這樣。下面我選用db_test.cc檔案來舉例分析。
測試程式碼必然有一個主函式入口,在db_test.cc的最下面有main函式:

int main(int argc, char** argv) {
  if
(argc > 1 && std::string(argv[1]) == "--benchmark") { leveldb::BM_LogAndApply(1000, 1); leveldb::BM_LogAndApply(1000, 100); leveldb::BM_LogAndApply(1000, 10000); leveldb::BM_LogAndApply(100, 100000); return 0; } return leveldb::test::RunAllTests(); }

如果你看過其他的leveldb測試程式,你就會發現這個leveldb::test::RunAllTests()函式很關鍵。
這個test是何物?是巢狀在名稱空間leveldb下的子名稱空間還是一個類,我們利用sublime編輯器搜尋test關鍵字。在檔案testharness下發現:

namespace leveldb {
namespace test {

繼續鎖定testharness檔案,有:

extern int RunAllTests();

事實上testharness.h和.cc檔案是理解leveldb測試程式的關鍵,搜尋關鍵字RunAllTests:

int RunAllTests() {
  const char* matcher = getenv("LEVELDB_TESTS");

  int num = 0;
  if (tests != NULL) {
    for (size_t i = 0; i < tests->size(); i++) {
      const
Test& t = (*tests)[i]; if (matcher != NULL) { std::string name = t.base; name.push_back('.'); name.append(t.name); if (strstr(name.c_str(), matcher) == NULL) { continue; } } fprintf(stderr, "==== Test %s.%s\n", t.base, t.name); (*t.func)(); ++num; } } fprintf(stderr, "==== PASSED %d tests\n", num); return 0; }

以及:

namespace {
struct Test {
  const char* base;
  const char* name;
  void (*func)();
};
std::vector<Test>* tests;
}

所謂runalltest就是把容器tests裡的函式指標func指向的函式都跑一遍,tests容器裡的元素是Test結構,內有函式指標func,字元base、name,會用來做一個匹配判斷,先不管,注意:test、tests、Test的區別。

回到db_test.cc,定義的兩個類class SpecialEnv、class DBTest先不管,我反正是被

TEST(DBTest, Empty) {
  do {
    ASSERT_TRUE(db_ != NULL);
    ASSERT_EQ("NOT_FOUND", Get("foo"));
  } while (ChangeOptions());
}

這種扎眼的結構吸引了,像函式但又不是,在這裡TEST是一個巨集定義。在testharness.h下找到了

#define TEST(base,name)                                                 
class TCONCAT(_Test_,name) : public base {                              
 public:                                                            
  void _Run();                                                          
  static void _RunIt() {                                                
    TCONCAT(_Test_,name) t;                                             
    t._Run();                                                           
  }                                                           
};                                                                      
bool TCONCAT(_Test_ignored_,name) =                                     
  ::leveldb::test::RegisterTest(#base, #name, &TCONCAT(_Test_,name)::_RunIt); 
void TCONCAT(_Test_,name)::_Run()

注意,原始碼裡是有“\”續行符的,csdn編輯器有點頭疼,所以為了顯示效果,就被我刪了。
讓我們看看這個巨集幹了什麼,定義了一個TCONCAT(Test,name)類,繼承自類base,base是傳入的字元。

#define TCONCAT(a,b) TCONCAT1(a,b)
#define TCONCAT1(a,b) a##b

知TCONCAT(Test,name)等於_Test_name,##為字元連線符。
繼續,將兩段程式碼拼上:

#define TEST(base,name)                                                 
class TCONCAT(_Test_,name) : public base {                              
 public:                                                            
  void _Run();                                                          
  static void _RunIt() {                                                
    TCONCAT(_Test_,name) t;                                             
    t._Run();                                                           
  }                                                           
};                                                                      
bool TCONCAT(_Test_ignored_,name) =                                     
  ::leveldb::test::RegisterTest(#base, #name, &TCONCAT(_Test_,name)::_RunIt); 
void TCONCAT(_Test_,name)::_Run()
{
  do {
    ASSERT_TRUE(db_ != NULL);
    ASSERT_EQ("NOT_FOUND", Get("foo"));
  } while (ChangeOptions());
}

這樣利用巨集,讓我想起了MFC。MFC為了實現動態建立物件,定義了一個執行型錄,用一個連結串列儲存所有類的RuntimeClass資訊,而RuntimeClass是一個類,成員變數有MFC類的資訊,包括名字,建立物件函式的指標以及連結串列指標,並用巨集的形式完成了RuntimeClass物件的構造和複製。
接著,全域性bool變數TCONCAT(Test_ignored,name)放置了函式RegisterTest(#base, #name, &TCONCAT(Test,name)::_RunIt)的返回值,找到RegisterTest函式

bool RegisterTest(const char* base, const char* name, void (*func)()) {
  if (tests == NULL) {
    tests = new std::vector<Test>;
  }
  Test t;
  t.base = base;
  t.name = name;
  t.func = func;
  tests->push_back(t);
  return true;
}

如果你還記得前面的全域性容器tests,你就明白這是在幹什麼了。
總結下:
TEST巨集利用傳入的字元引數,定義了一個類Test##name,並寫入了成員函式Run()的操作,然後利用RegisterTest登記_Test##name,將其類名類名、基類名以及靜態成員函式_RunIt()指標放入全域性容器tests中,等待測試程式main函式呼叫。

未完待續!