C語言[工程專案應用]gtest測試框架編寫以及自定義測試框架

Leopiglet發表於2020-11-07

1 gtest測試框架:

下載地址: git clone https://github.com/google/googletest
安裝部署:

cd googletest
cmake CmakeLists.txt #生成makefile
make #執行makefile

目錄結構:

googletest
----lib
--------libgtest.a
----CMakeList.txt
----makefile
----googletest
--------include
------------gtest
----------------gtest.h

1.1 測試樣例

專案目錄構成:

project
----include
--------gtest
------------gtest.h
----lib
--------libgtest.a
----main.cpp

main.cpp:

#include <stdio.h>
#include <gtest/gtest>

int add(int a, int b) {
	return a + b;
}

// gtest.h 的 內部巨集
TEST(func, add) {
	EXPECT_EQ(add(3, 4), 7);
	EXPECT_EQ(add(3, 1), 5);
	ASSERT_EQ(add(3, 1), 5);
}

int main(int args, char *argv[]) {
	// gtest的內部名稱空間
	testing::InitGoogleTest(&argc, argv);
	// RUN_ALL_TESTS 也應為 gtest.h 的內部巨集
	return RUN_ALL_TESTS();
}

該案例用於檢測 add(3, 4)是否為7, add(3,1)是否為5, add(3,1) = 5是否出錯

編譯與執行:
g++ -std=c++11 -I./include main.cpp -L./lib -lgtest -lpthread

-std=c++11 : 指定編譯標準為C++11
-I./include : 指定標頭檔案地址
-L./lib : 指定靜態庫地址

-lgtest : 呼叫靜態庫 libgtest.a
-lpthread : 呼叫靜態庫 libpthread.a

結果 :

[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from func
[ RUN      ] func.add
main.cpp:10: Failure
Expected equality of these values:
  add(3, 1)
    Which is: 4
  5
main.cpp:12: Failure
Expected equality of these values:
  add(3, 1)
    Which is: 4
  6
[  FAILED  ] func.add (0 ms)
[----------] 1 test from func (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[  PASSED  ] 0 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] func.add

 1 FAILED TEST

2 自定義測試框架

要求:

  1. 設計TEST(a , b)巨集
  2. 設計RUN_ALL_TESTS()巨集
  3. 設計EXPECT_EQ(…)等系列功能
  4. 實現程式碼變色
  5. 實現動態記憶體分佈

主體架構:

main_my.c
----test.h
----test.c
makefile
bin
----a.out

2.1 主檔案main_my.c :

#include <studio.h>
#inlcude "test.h"

int add(int a, int b) {
	return a + b;
}

// 測試用例組的整體巨集替換
TEST(testfunc, add1) {
	// 測試單元的具體巨集替換
	EXPECT_EQ(add(3, 4), 7);
	EXPECT_NE(add(3, 2), 5);
	EXPECT_EQ(add(3, 3), 6);
}
TEST(testfunc, add2) {
	EXPECT_EQ(add(3, 4), 7);
	EXPECT_LT(add(3, 1), 5);
	EXPECT_EQ(add(3, 3), 6);
}
TEST(test, funcadd1) {
	EXPECT_EQ(add(3, 4), 7);
	EXPECT_NE(add(1, 1), 5);
	EXPECT_EQ(add(3, 3), 6);
}

int main(int argc, char *argv[]) {
	// 執行所有測試用例
	return RUN_ALL_TESTS();
}

2.2 標頭檔案include/test.h :

PART 1

#ifndef _TEST_H
#define _TEST_H
// 動態空間分佈引入的自定義連結串列庫
#include "linklist.h"

// TEST巨集定義
#define TEST(a, b)\
void a##_haizei_##b();\
__attribute__((constructor)) void add##_haizei_##a##_haizei_##b() {\
	add_function(a##_haizei_##b, #a"."#b);\
}\
void a##_haizei_##b()
/* 1
 * a : 變數名1, b : 變數名2
 * 2
 * __attribute__(constructor) type func(...) {...}  
 * executing func before function main
 * (add the TEST and compose the name(add_function) before main)
 * 3
 * ## : concat a and b as ab
*/

#的用法:

  1. 將多個變數名連線成一個新的變數(函式)名
    例 : a##_haizei_##b() == a_haizei_b
  2. 取變數名的字串
    例: #a"."#b == "a.b"

TEST巨集實現 :
位於 : 主函式的前面
功能 : 1. 宣告單元測試函式;2. 定義單元測試用例新增函式
技巧:

__attribute__((constructor)) func機制 : func先於main執行

int main(int argc, char * argv[]) {
    @autoreleasepool {
        printf("main function");
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
__attribute__((constructor)) static void beforeFunction()
{
    printf("beforeFunction\n");
}

// 輸出結果順序
beforeFunction
main function

__attribute__為GNU C的特色機制 : 可以設定 函式, 變數, 型別的屬性

https://www.jianshu.com/p/dd425b9dc9db

PART 2

// 輸出顏色巨集設定
#define COLOR(a, b) "\033[" #b "m" a "\033[0m"
#define COLOR_HL(a, b) "\033[1;" #b "m" a "\033[0m"

#define GREEN(a) COLOR(a, 32)
#define RED(a) COLOR(a, 31)
#define BLUE(a) COLOR(a, 34)
#define YELLOW(a) COLOR(a, 33)

#define GREEN_HL(a) COLOR_HL(a, 32)
#define RED_HL(a) COLOR_HL(a, 31)
#define BLUE_HL(a) COLOR_HL(a, 34)
#define YELLOW_HL(a) COLOR_HL(a, 33)
/* printf中設定顏色的方式, 其中A為被上色的字元
 * 正常亮度:
 * "\033[xxmA\033[0m"
 * 高亮:
 * "\033[1;xxmA\033[0m"
*/

PART 3

// 泛型巨集: _Generic的使用 (為生成的值設定顏色)
// 只能用於.c檔案中 (在.cpp & .cc檔案中使用會報錯)
// 依照變數a的型別返回對應的字串
#define TYPE(a) _Generic((a), int:"%d", double:"%lf")

// 利用泛型巨集 和 顏色函式名巨集替換 對生成值設定顏色
// a: 生成值, color: 待替換顏色函式名
#define P(a, color) {\
	char frm[1000];\
	sprintf(frm, color("%s"), TYPE(a));\
	printf(frm, a);\
}

泛型巨集_Generic的使用 :
_Generic是C11新增的一個關鍵字
_Generic((var), type1 : ..., type2 : ..., ……, default : ...)
type 表示型別,…表示對這個型別的var進行的操作,最終的default表示其他型別,也就是所定義的type中沒有的型別,就會跳到default

attribute((constructor))用法解析.

PART 4

// 實際單元測試用例的部分共有功能的巨集定義
/* EXPECT(a, b, cmp)
 * a : 單元用例的實際函式返回值; 
 * b : 單元用例的期望值; 
 * cmp : 用例的操作符
*/
#define EXPECT(a, b, cmp) {\
	printf(GREEN("[-----------] ") #a" " #cmp" "#b" ");\
	haizei_test_info.total += 1;\
	__typeof(a) _a = (a);\
	__typeof(b) _b = (b);\
	if (_a cmp _b) {\
		haizei_test_info.success += 1;\
		printf("%s\n",\
			(_a) cmp (_b) ? GREEN_HL("True") : RED_HL("False"));\
	}\
	else {\
		printf("%s\n",\
			(_a) cmp (_b) ? GREEN_HL("True") : RED_HL("False"));\
		printf("\n");\
		printf(YELLOW_HL("\t%s:%d: failure\n"), __FILE__, __LINE__);\
		printf(YELLOW_HL("\t\texpect : " #a " " #cmp " " #b "\n"));\
		printf(YELLOW_HL("\t\tactual : " ));\
		P(_a, YELLOW_HL);\
		printf("\n\n");\
	}\
}

// 測試用例的具體設定
#define EXPECT_EQ(a, b) EXPECT(a, b, ==)
#define EXPECT_NE(a, b) EXPECT(a, b, !=)
#define EXPECT_LT(a, b) EXPECT(a, b, <)
#define EXPECT_LE(a, b) EXPECT(a, b, <=)
#define EXPECT_GT(a, b) EXPECT(a, b, >)
#define EXPECT_GE(a, b) EXPECT(a, b, >=)

詳細註釋 :

EXPECT(a, b, cmp) {
	// a , b被變數替換, cmp被邏輯比較符替換
	
	printf(GREEN("[-----------] ") #a" " #cmp" "#b" ");
	// GREEN("...")將返回一個帶顏色的字串 並與(例)"5 >= 2"相連
	
	haizei_test_info.total += 1;
	// 更新測試用例計數
	
	__typeof(a) _a = (a);
	__typeof(b) _b = (b);

typeof() :
GUN C提供的一種特性,可以取得變數的型別,或者表示式的型別

在巨集定義中動態獲取相關結構體成員的型別 :

例:
1 .定義一個和變數x相同型別的臨時變數_max1,定義一個和變數y相同型別的臨時變數_max2
2 .判斷兩者型別是否一致,不一致給出一個警告,最後比較兩者。

#define max(x, y) ({                \
    typeof(x) _max1 = (x);          \
    typeof(y) _max2 = (y);          \
    (void) (&_max1 == &_max2);      \//如果呼叫者傳參時兩者型別不一致,編譯時會警告。
    _max1 > _max2 ? _max1 : _max2; })
	if (_a cmp _b) {
		
		// 表示用例通過, 更新用例通過計數, 並按要求設定列印資訊
		haizei_test_info.success += 1;
		printf("%s\n",
			(_a) cmp (_b) ? GREEN_HL("True") : RED_HL("False"));
	}
	else {
		
		// 列印用例出錯處的位置資訊與程式碼資訊, 並列印實際輸出值的資訊進行對比
		printf("%s\n",
			(_a) cmp (_b) ? GREEN_HL("True") : RED_HL("False"));
		printf("\n");
		printf(YELLOW_HL("\t%s:%d: failure\n"), __FILE__, __LINE__);
		printf(YELLOW_HL("\t\texpect : " #a " " #cmp " " #b "\n"));
		printf(YELLOW_HL("\t\tactual : " ));
		P(_a, YELLOW_HL); 
		// _a 與實際生成右值的型別相等, YELLOW_HL來替換顏色函式名
		
		printf("\n\n");
	}
}

PART 5

// 以下宣告內容的定義將位於../src/test.c檔案中

// 設定測試用例指標 : 用於計數 與 儲存
typedef void (*Testfunc)();

// 儲存測試用例函式指標, 函式字串名, 以及連結串列資訊
typedef struct Function {
	Testfunc func;
	const char* str;
	struct LinkNode p;
} Function;

// 用於記錄測試用例總數與通過次數的結構體
struct FunctionInfo {
	int total, success;
};

// 宣告外部變數,其具體定義會在後面的檔案中出現
extern struct FunctionInfo haizei_test_info;

// 宣告主函式中的方法
int RUN_ALL_TESTS();

// 宣告將測試用錄進行記錄儲存的方法
void add_function(Testfunc, const char*);

#endif

2.3 自定義連結串列標頭檔案include/linklist.h

#ifndef _LinkList_H
#define _LinkList_H

// 獲取 T 型別 name欄位的偏移量
#define offset(T, name) (long long)( &( ((T *)(0))->name ) )
// p : 當前節點欄位p的地址, T, name (可獲取接下來T型別的name欄位的偏移量)
// 因為地址的數字表示比較長, 所以需要log long來匹配
#define Head(p, T, name) (T *)( (char *)(p) - offset(T, name) )
struct LinkNode {
	struct LinkNode *next;
};
#endif

offset(T, name) :

  1. 將空地址0強轉成T *型別 : (T *)(0)
  2. T對應的name成員變數的地址 : &(((T *)(0))->name)
  3. 地址取long long型別

Head(p, T, name)需結合後續操作進行解讀

以上兩步是為了將含LinkNode型別成員的結構體進行連線

2.4 功能函式檔案src/test.c

#include "../include/test.h"
#include <string.h>
#inlcude <stdio.h>
#include <math.h>
#include <stdlib.h>

// 設定起始例項儲存節點, 並設定末尾節點進行初始化
Function func_head, *func_tail = &func_head;

// 對應 include/test.h 中的 extern struct FunctionInfo haizei_test_info;
struct FunctionInfo haizei_test_info;

// 主函式的實際操作
int RUN_ALL_TESTS() {
	// 遍歷之前在之前已經執行過的add_function所儲存的各用例所處的地址資訊(以便於呼叫)
	for (struct LinkNode *p = func_head.p.next; p; p = p->next) {
		// 獲得當用例小組的指標, 列印開始資訊 與 對應函式名
		Function *func = Head(p, Function, p);
		printf(GREEN("[====RUN====]") YELLOW_HL(" %s") "\n", func->str);
		// 初始化用例計數
		haizei_test_info.total = 0;
		haizei_test_info.success = 0;
		// 執行函式
		func->func();
		// 計算用例通過率
		double rate = 100.0 * haizei_test_info.success / haizei_test_info.total;
		printf(GREEN("[__"));
		if (fabs(rate - 100.0) < 1e-6) {
			printf(BLUE_HL("%6.2lf%%"), rate);
		}
		else {
			printf(YELLOW_HL("%6.2lf%%"), rate);
		}
		printf(GREEN("__]") "total : %d success : %d\n",
			haizei_test_info.total, haizei_test_info.success
		);
	}
	return 0;
}

// 儲存用例資訊的連結串列生成函式
void add_function(Testfunc func, const char* str) {
	struct Function *temp = (Function *)calloc(1, sizeof(Function));
	temp->func = func;
	temp->str = strdup(str);
	func_tail->p.next = &(temp->p);
	func_tail = temp;
}

strdup()strcpy()的區別:
strdup()可以把要複製的內容直接複製給沒有初始化的指標,它會自動分配空間給目的指標,但需要手動free()進行記憶體回收。
strcpy()的目的指標一定是已經分配(足夠)記憶體的指標

2.5 編譯執行檔案makefile

.PHONY:clean run

all: main_my.o src/test.o include/test.h
        gcc -I./ main_my.o src/test.o -o ./bin/my
main.o: main_my.c include/test.h
        gcc -I./ -c main_my.c
haizei/test.o: haizei/test.c haizei/test.h
        gcc -I./ -c src/test.c -o src/test.o
clean:
        rm -rf bin/my main_my.o src/test.o
run:
        ./bin/my

相關文章