最近由於科研需求,一直在研究Google的開源RE2庫(正則表達式識別庫),庫源碼體積龐大,用C++寫的,對於我這個之前專供Java的人來講真的是一件很痛苦的事,天天只能啃一點點。今天研究了下里面用到的測試方法,感受挺好的,拿來跟你們分享下!(哈~C++大牛勿噴)正則表達式
對於我這個C++菜鳥中的菜鳥而言,平時寫幾個函數想要測試通常都是在main中一個一個的測試,由於沒用C++寫過項目,沒有N多方法因此在main中一個個測試也不費勁。可是對於一個項目而言,或多或少都有N多方法,若是在main中一個個測試的話,不只效率低並且還容易出錯遺漏什麼的。那麼該怎麼進行測試呢?貌似如今有不少C++自動化測試的工具,反正我是一個沒用過,也無法評價。我就說下Google在RE2庫裏是怎麼測試的吧。數組
先用一個超級簡單的例子來作講解:測試兩個方法getAsciiNum()和getNonAsciiNum(),分別求flow中ASCII碼字符的數目和非ASCII碼字符的數目。函數
// test.h
#define TEST(x, y) \ void x##y(void); \ TestRegisterer r##x##y(x##y, # x "." # y); \ void x##y(void) void RegisterTest(void (*)(void), const char*); class TestRegisterer { public: TestRegisterer(void (*fn)(void), const char *s) { RegisterTest(fn, s); } };
解析:首先看定義的類TestRegisterer,有個構造方法,兩個參數:工具
1. 一個函數指針:void (*fn)(void),指向咱們具體要編寫的測試方法名;測試
2. 一個字符串:const char *s,屬於該測試方法的描述信息。spa
這個構造函數調用了另外一個函數RegisterTest(),具體實現見下面。指針
而後看最上面定義的宏TEST(x, y),主要將其替換爲TestRegisterer r##x##y(x##y, # x "." # y);其中x##y做爲方法名,# x "." # y做爲描述信息。這裏可能有些和我同樣入門級別的人沒怎麼看懂這個宏,由於不知道先後加void x##y(void);這個是幹嗎用的?一開始我也沒想明白,由於不加的話就會報錯,後來經過gcc的-E選項激活宏編譯,看了下編譯期間展開成啥模樣了。這裏以一個簡單的例子做爲說明:假設x爲test,y爲flow,若是不加先後那個,那麼展開後爲TestRegisterer rtestflow(testflow, "test.flow"); 這明顯是個函數聲明,有兩個參數,第二個是字符串,那麼第一個是什麼?編譯器會認爲是個函數名(實際上也是的),但這個函數前面明顯未定義,就會報找不到此函數聲明的錯誤,因此就須要在以前加上void x##y(void);聲明函數,固然光聲明不實如今連接時一樣報錯,因此就須要在以後加上void x##y(void)進行具體實現了,注意這裏沒有逗號,也沒有具體實現的{},由於這只是宏,Google的全部測試函數是這樣寫的:code
TEST(x, y) { .... // 具體實現 }
那麼上面例子TEST(test, flow){ ... // 具體實現 },總體展開後就是這樣:blog
void testflow(void); TestRegisterer rtestflow(testflow, "test.flow"); void testflow(void) { .... // 具體實現 }
#include <string> #include <vector> #include "test.h" #define arraysize(array) (sizeof(array)/sizeof((array)[0])) #define CHECK_EQ(x, y) if((x) != (y)) { printf("test failed!\n"); system("pause"); exit(0); } struct TestFlow { const char* flow; const int num; }; static struct TestFlow tests1[] = { {"\x02\x97\xa4\xe6\xfe\x0c", 2}, {"\x05\x97\x35\xe6\xfe\xac\x04", 3}, {"\xb2\x97\xa5\xe6\x9c\x1c\x58\xaa\x97\x03", 3}, {"\x32\x97\xa5\x05\x9c\xac\xe8\xaa\x57", 3}, {"\x42\x01\xa5\x86\x0c\x56\xe8\xaa\x97\x03", 5}, }; static struct TestFlow tests2[] = { {"\x02\x97\xa4\xe6\xfe\x0c", 4}, {"\x05\x97\x35\xe6\xfe\xac\x04", 4}, {"\xb2\x97\xa5\xe6\x9c\x1c\x58\xaa\x97\x03", 7}, {"\x32\x97\xa5\x05\x9c\xac\xe8\xaa\x57", 6}, {"\x42\x01\xa5\x86\x0c\x56\xe8\xaa\x97\x03", 5}, }; int getAsciiNum(const char*); int getNonAsciiNum(const char*); TEST(TestAsciiNum, Simple) { int failed = 0; for (int i = 0; i < arraysize(tests1); i++) { const TestFlow& t = tests1[i]; int num = getAsciiNum(t.flow); if (num != t.num) { failed++; } } CHECK_EQ(failed, 0); } TEST(TestNonAsciiNum, Simple) { int failed = 0; for (int i = 0; i < arraysize(tests2); i++) { const TestFlow& t = tests2[i]; int num = getNonAsciiNum(t.flow); if (num != t.num) { failed++; } } CHECK_EQ(failed, 0); } int getAsciiNum(const char* flow) { // we assume that there's no \x00 in flow otherwise we cannot use strlen() int num = 0, i; for(i = 0; i < strlen(flow); i++) { // ASCII: 0 ~ 127 if(flow[i] >= 0 && flow[i] < 128) num++; } return num; } int getNonAsciiNum(const char* flow) { // we assume that there's no \x00 in flow otherwise we cannot use strlen() int num = 0, i; for(i = 0; i < strlen(flow); i++) { // ASCII: 0 ~ 127 if(flow[i] < 0 || flow[i] >= 128) num++; } return num; }
看上去一目瞭然,TEST(TestAsciiNum, Simple)和TEST(TestNonAsciiNum, Simple)就是兩個具體的測試實現了,這個例子很簡單,僅僅是爲了說明問題。ci
// test.cpp #include <stdio.h> #include <stdlib.h> #include "test.h" struct Test { void (*fn)(void); const char *name; }; static Test tests[10000]; static int ntests; void RegisterTest(void (*fn)(void), const char *name) { tests[ntests].fn = fn; tests[ntests++].name = name; } int main(int argc, char **argv) { for (int i = 0; i < ntests; i++) { printf("%s\n", tests[i].name); tests[i].fn(); } printf("PASS\n"); system("pause"); return 0; }
解析:
1. 結構體Test存儲具體的測試實現,定義最多能有10000個不一樣的方法測試,也就是能同時測試10000個方法。
2. ntests表明實際所測試的方法數,我這裏就是2了。
3. RegisterTest()具體的實現也比較簡單,就是將實際所要測試的方法名和描述信息存儲到Test結構體數組tests中。
4. 最後就是在main中進行統一測試了,首先輸出測試方法描述信息,以便知道當前測試了哪些方法及若是有測試失敗時能及時進行排查。而後就是具體的執行測試函數了。
本例的測試結果以下:
你們可能以爲main寫的太簡潔,一開始什麼都沒調用,直接來個for循環,ntests的值初始不是0嗎?在main一開始也沒顯式的調用RegisterTest()將測試方法加進去啊,怎麼一進入main,ntests就變成2了?
你們要記住:全部的測試具體實現都是在TEST這個宏裏面,而宏是在編譯期間就開始展開了。以 TEST(TestAsciiNum, Simple){ ... }爲例,具體的執行過程以下:
編譯期間:
TEST(TestAsciiNum, Simple)展開爲:
void TestAsciiNumSimple(void); TestRegisterer rTestAsciiNumSimple(TestAsciiNumSimple, "TestAsciiNum.Simple"); void TestAsciiNumSimple(void) { int failed = 0; for (int i = 0; i < arraysize(tests1); i++) { const TestFlow& t = tests1[i]; int num = getAsciiNum(t.flow); if (num != t.num) { failed++; } } CHECK_EQ(failed, 0); }
而後就觸發調用了TestRegisterer的構造方法從而開始執行RegisterTest(TestAsciiNumSimple, "TestAsciiNum.Simple")方法,將TestAsciiNumSimple方法名和描述信息"TestAsciiNum.Simple"加入到結構體數組tests中,這時ntests增爲1,同理另外一個宏TEST(TestNonAsciiNum, Simple)展開後也將TestAsciiNonNumSimple方法名和描述信息"TestNonAsciiNum.Simple"加入到結構體數組tests中,這時ntests增爲2,這是編譯期間作的事。
運行期間:
從main開始,執行for循環,前後執行了具體的測試實現方法TestAsciiNumSimple()和TestAsciiNonNumSimple()從而完成測試。
用一個圖來講明更加清晰(圖畫的不太好,望見諒~~~)