加入星計劃,您可以享受以下權益:

  • 創(chuàng)作內(nèi)容快速變現(xiàn)
  • 行業(yè)影響力擴散
  • 作品版權保護
  • 300W+ 專業(yè)用戶
  • 1.5W+ 優(yōu)質(zhì)創(chuàng)作者
  • 5000+ 長期合作伙伴
立即加入
  • 正文
    • 安裝和配置
    • 斷言
    • 測試事件
    • TestSuite測試事件
    • 參數(shù)化
  • 推薦器件
  • 相關推薦
  • 電子產(chǎn)業(yè)圖譜
申請入駐 產(chǎn)業(yè)圖譜

一文掌握Google開源單元測試框架

05/03 08:13
3110
閱讀需 34 分鐘
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

我們在開發(fā)的過程中需要做一些驗證測試,來保證我們的代碼是按照設計要求工作的,這就需要單元測試了。單元測試(Unit Test),我們稱為“UT測試”。對于一個復雜的系統(tǒng)來說,需要編寫大量的單元測試用例,有人會覺得這么多的測試代碼,將會花費大量的時間,影響開發(fā)的進度,會得不償失。真的是這樣嗎?其實,對于越是復雜的系統(tǒng)就越是需要單元測試來保證我們的代碼的開發(fā)質(zhì)量,及時測試出代碼的問題,在開發(fā)階段發(fā)現(xiàn)問題總比在系統(tǒng)發(fā)布之后發(fā)現(xiàn)問題能夠較少的節(jié)省資源或成本。

對于單元測試應該是每個開發(fā)工程師必備的技能,尤其是高階的開發(fā)工程師會更加注重UT的重要性。同時,我們在開發(fā)功能模塊之前會考慮到測試用例的實現(xiàn),這樣自然的就會考慮到功能模塊的模塊化便于UT的編寫,從這一方面來說也能提高開發(fā)人員開發(fā)的代碼質(zhì)量。另外,單元測試用例還可以作為示例供開發(fā)人員參考,從而能夠更輕松的掌握模塊的使用。

今天就和大家一起學習一個開源的C++的單元測試框架Google test,大家看名字就知道它是由牛逼的Google公司出品。Google Test可以在多種平臺上使用,它可以支持:

Linux、Mac OS X、Windows、Cygwin、MinGW、Windows Mobile、Symbian、PlatformIO等。

安裝和配置

我們可以從github獲取Google Test的源碼,如果大家沒有github賬號也可以從網(wǎng)盤下載。

github下載地址: https://github.com/google/googletest

網(wǎng)盤下載:關注公眾號【W(wǎng)ill的大食堂】回復【gTest下載】即可獲取下載地址。

因為我們下載到的gTest是源代碼,還需要將其編譯成庫文件再進行使用。下面將和大家一起學習如何在windows環(huán)境下生成gTest的庫文件。在這之前我們需要安裝CMake和MinGW,大家可以參考下面這兩個文章進行安裝。

  • 《VS Code 編譯和調(diào)試C/C++程序也可以這么爽》
  • 《VS Code中如何安裝和使用CMake工具?》

將下載的gTest的源碼進行解壓,源碼目錄如下圖所示。

源碼工程目錄

打開命令行工具cmd,進入源碼的工程目錄,新建一個build目錄用來存放構建文件,然后,進入build目錄執(zhí)行cmake命令生成Makefile文件。

mkdir build
cd build
cmake -G "MinGW Makefiles" ..

Makefile文件生成后,再執(zhí)行下面的命令mingw32-make編譯庫文件。編譯成功后就會發(fā)現(xiàn)有l(wèi)ibgtest.a 和libgtest_main.a兩個靜態(tài)庫生成。這里注意,Windows下mingw安裝的make工具名稱是mingw32-make而不是make。

mingw32-make


執(zhí)行mingw32-make命令

接下來我們在VS Code寫一個測試用例,使用生成的gTest靜態(tài)庫測試下。按下快捷鍵【Ctrl+Shift+p】,在彈出的搜索框中搜索【C/C++:Edit Configurations】,可以創(chuàng)建c_cpp_properties.json配置文件。


C/C++:Edit Configurations

在c_cpp_properties.json配置文件添加gTest的頭文件目錄。


添加gTest頭文件目錄

在task.json配置文件中添加gTest頭文件目錄和庫文件,task.json配置文件可以通過菜單欄中Terminal選項下的【Configure Default Build Task】選項創(chuàng)建,可以參照之前的文章。

Configure Default Build Task

添加頭文件目錄和庫文件

上面配置好之后,我們寫個測試用例跑一下。

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

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

int sub(int a, int b)
{
return a - b;
}

TEST(testcase, test_add)
{
EXPECT_EQ(add(1,2), 3);
EXPECT_EQ(sub(1,2), -1);
}

int main(int argc, char **argv)
{
std::cout << "run google test --> " << std::endl << std::endl;
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
運行結果如下圖所示,代碼中的TEST是一個宏,用來創(chuàng)建測試用例,它有test_case_name和test_name兩個參數(shù)。分別是測試用例名和測試名,在后面的文章中我們會對其有更深刻的理解,這里就不細說了。RUN_ALL_TESTS也是一個宏,它是測試用例的入口。EXPECT_EQ這個是一個斷言相關的宏,用來檢測兩個數(shù)值是否相等。


運行結果

斷言

除了上面示例里的EXPECT_EQ,在gTest里有很多斷言相關的宏。斷言可以檢查出某些條件的真假,因此,我們可以通過它來判斷被測試的函數(shù)的成功與否。這里斷言我們主要可以分為兩類:

  • 以"ASSERT_"開頭的斷言,致命性斷言(Fatal assertion)
  • 以"EXPECT_"開頭的斷言 ,非致命性斷言(Nonfatal assertion)

上面的兩種斷言會在斷言條件不滿足時會有區(qū)別,即當不滿足條件時, "ASSERT_"斷言會在當前函數(shù)終止,而不會繼續(xù)執(zhí)行下去;而"EXPECT_"則會繼續(xù)執(zhí)行。我們可以通過下面一個例子來理解下他們的區(qū)別。

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

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

int sub(int a, int b)
{
return a - b;
}

TEST(testcase, test_expect)
{
std::cout << "------ test_expect start-----" << std::endl;

std::cout << "add function start" << std::endl;
EXPECT_EQ(add(1,2), 2);
std::cout << "add function end" << std::endl;

std::cout << "sub function start" << std::endl;
EXPECT_EQ(sub(1,2), -1);
std::cout << "sub function end" << std::endl;

std::cout << "------ test_expect end-----" << std::endl;
}

TEST(testcase, test_assert)
{

std::cout << "------ test_assert start-----" << std::endl;

std::cout << "add function start" << std::endl;
ASSERT_EQ(add(1,2), 2);
std::cout << "add function end" << std::endl;

std::cout << "sub function start" << std::endl;
ASSERT_EQ(sub(1,2), -1);
std::cout << "sub function end" << std::endl;

std::cout << "------ test_assert end-----" << std::endl;
}

int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

從下面的運行結果上看,assert斷言檢查出被測函數(shù)add不滿足條件,所以程序就沒有繼續(xù)執(zhí)行下去;而expect雖然檢查出被測試函數(shù)add不滿足條件,但是程序還是繼續(xù)去測試sub函數(shù)。


assert 和 expect

上面的示例用到的都是判斷相等條件的斷言,還有其他條件檢查的斷言。主要可以分為布爾檢查,數(shù)值比較檢查,字符串檢查,浮點數(shù)檢查,異常檢查等等。下面我們逐一認識這些斷言。

布爾檢查

布爾檢查主要用來檢查布爾類型數(shù)據(jù),檢查其條件是真還是假。

數(shù)值比較檢查

數(shù)值比較檢查主要用來比較兩個數(shù)值之間的大小關系,這里有兩個參數(shù)。

字符串檢查

字符串檢查主要用來比較字符串的內(nèi)容。

浮點數(shù)檢查

對于浮點數(shù)來說,因為其精度原因,我們無法確定其是否完全相等,實際上對于浮點數(shù)我比較兩個浮點數(shù)近似相等。

異常檢查

異常檢查可以將異常轉換成斷言的形式。

除了上面的一些類型的斷言,還有一切其他的常用斷言。

顯示成功或失敗

這一類斷言會在測試運行中標記成功或失敗。它主要有三個宏:

  • SUCCED():標記成功。
  • FAIL() : 標記失敗,類似ASSERT斷言標記致命錯誤;
  • ADD_FAILURE():標記,類似EXPECT斷言標記非致命錯誤。

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

int divison(int a, int b)
{
return a / b;
}

TEST(testCaseTest, test0)
{
std::cout << "start test 0" << std::endl;
SUCCEED();
std::cout << "test pass" << std::endl;
}

TEST(testCaseTest, test1)
{
std::cout << "start test 1" << std::endl;
FAIL();
std::cout << "test fail" << std::endl;
}

TEST(testCaseTest, test2)
{
std::cout << "start test 2" << std::endl;
ADD_FAILURE();
std::cout << "test fail" << std::endl;
}

int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

執(zhí)行結果如下:


成功失敗斷言

死亡測試

死亡測試是用來檢測測試程序是否按照預期的方式崩潰。

Assert Expect Description
ASSERT_DEATH(statement, regex) EXPECT_DEATH(statement, regex) 檢查按照代碼給定的方式崩潰

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

int divison(int a, int b)
{
return a / b;
}

TEST(testCaseDeathTest, test_div)
{
EXPECT_DEATH(divison(1, 0), "");
}
int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

上面這個例子就是死亡測試,其運行結果如下,這里需要注意的是test_case_name如果使用DeathTest為后綴,gTest會優(yōu)先運行。


死亡測試

測試事件

在學習測試事件之前,我們先來了解下三個概念,它們分別是測試程序,測試套件,測試用例。

  • 測試程序是一個可執(zhí)行程序,它有一個測試程序的入口main函數(shù)。
  • 測試用例是用來定義需要驗證的內(nèi)容。
  • 測試套件是測試用例的集合,運行測試。

我們回過來看測試事件,在GTest中有了測試事件的這個機制,就能能夠在測試之前或之后能夠做一些準備/清理的操作。根據(jù)事件執(zhí)行的位置不同,我們可將測試事件分為三種:

  • TestCase級別測試事件:這個級別的事件會在TestCase之前與之后執(zhí)行;
  • TestSuite級別測試事件:這個級別的事件會在TestSuite中第一個TestCase之前與最后一個TestCase之后執(zhí)行;
  • 全局測試事件:這是級別的事件會在所有TestCase中第一個執(zhí)行前,與最后一個之后執(zhí)行。

這些測試事件都是基于類的,所以需要在類上實現(xiàn)。下面我們依次來學習這三種測試事件。

TestCase測試事件

TestCase測試事件,需要實現(xiàn)兩個函數(shù)SetUp()和TearDown()。

  • SetUp()函數(shù)是在TestCase之前執(zhí)行。
  • TearDown()函數(shù)是在TestCase之后執(zhí)行。

這兩個函數(shù)是不是有點像類的構造函數(shù)和析構函數(shù),但是切記他們并不是構造函數(shù)和析構函數(shù),只是打個比方才這么說而已。我們可以借助下面的代碼示例來加深對它的理解。這兩個函數(shù)是testing::Test的成員函數(shù),我們在編寫測試類時需要繼承testing::Test。

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

class calcFunction
{
public:
int add(int a, int b)
{
return a + b;
}

int sub(int a, int b)
{
return a - b;
}
};

class calcFunctionTest : public testing::Test
{
protected:
virtual void SetUp()
{
std::cout << "--> " << __func__ << " <--" <<std::endl;
}
virtual void TearDown()
{
std::cout << "--> " << __func__ << " <--" <<std::endl;
}

calcFunction calc;

};

TEST_F(calcFunctionTest, test_add)
{
std::cout << "--> test_add start <--" << std::endl;
EXPECT_EQ(calc.add(1,2), 3);
std::cout << "--> test_add end <--" << std::endl;
}

TEST_F(calcFunctionTest, test_sub)
{
std::cout << "--> test_sub start <--" << std::endl;
EXPECT_EQ(calc.sub(1,2), -1);
std::cout << "--> test_sub end <--" << std::endl;
}

int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

測試結果如下,兩個函數(shù)都是是在每個TestCase(test_add和test_sub)之前和之后執(zhí)行。


TestCase事件

TestSuite測試事件

TestSuite測試事件,同樣的也需要實現(xiàn)的兩個函數(shù)SetUpTestCase()和TearDownTestCase(),而這兩個函數(shù)是靜態(tài)函數(shù)。這兩個靜態(tài)函數(shù)同樣也是testing::Test類的成員,我們直接改寫下測試類calcFunctionTest,添加兩個靜態(tài)函數(shù)SetUpTestCase()和TearDownTestCase()到測試類中即可。

class calcFunctionTest : public testing::Test
{
protected:
static void SetUpTestCase()
{
std::cout<< "--> " << __func__ << " <--" << std::endl;
}

static void TearDownTestCase()
{
std::cout<< "--> " << __func__ << " <--" << std::endl;
}

virtual void SetUp()
{
std::cout << "--> " << __func__ << " <--" <<std::endl;
}
virtual void TearDown()
{
std::cout << "--> " << __func__ << " <--" <<std::endl;
}

calcFunction calc;

};

改寫好之后,我們再看一下運行結果。這兩個函數(shù)分別是在本TestSuite中的第一個TestCase之前和最后一個TestCase之后執(zhí)行。


TestSuite事件

全局測試事件

全局測試事件,也需要繼承一個類,但是它需要繼承testing::Environment類實現(xiàn)SetUp()和TearDown()兩個函數(shù)。還需要在main函數(shù)中調(diào)用testing::AddGlobalTestEnvironment方法注冊全局事件。我們直接上代碼吧!

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

class calcFunction
{
public:
int add(int a, int b)
{
return a + b;
}

int sub(int a, int b)
{
return a - b;
}
};

class calcFunctionEnvironment : public testing::Environment
{
public:
virtual void SetUp()
{
val = 123;
std::cout << "--> Environment " << __func__ << " <--" << std::endl;
}
virtual void TearDown()
{
std::cout << "--> Environment " << __func__ << " <--" << std::endl;
}

int val;
};

calcFunctionEnvironment* calc_env;

class calcFunctionTest : public testing::Test
{
protected:
static void SetUpTestCase()
{
std::cout<< "--> " << __func__ << " <--" << std::endl;
}

static void TearDownTestCase()
{
std::cout<< "--> " << __func__ << " <--" << std::endl;
}

virtual void SetUp()
{
std::cout << "--> " << __func__ << " <--" <<std::endl;
}
virtual void TearDown()
{
std::cout << "--> " << __func__ << " <--" <<std::endl;
}

calcFunction calc;

};

TEST_F(calcFunctionTest, test_add)
{
std::cout << "--> test_add start <--" << std::endl;
EXPECT_EQ(calc.add(1,2), 3);
std::cout << "Global Environment val = " << calc_env->val << std::endl;
std::cout << "--> test_add end <--" << std::endl;
}

TEST_F(calcFunctionTest, test_sub)
{
std::cout << "--> test_sub start <--" << std::endl;
EXPECT_EQ(calc.sub(1,2), -1);
std::cout << "Global Environment val = " << calc_env->val << std::endl;
std::cout << "--> test_sub end <--" << std::endl;
}

int main(int argc, char **argv)
{
calc_env = new calcFunctionEnvironment;
testing::AddGlobalTestEnvironment(calc_env);

testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

從測試結果上看,全局事件的這兩個函數(shù)分別是在第一個TestSuite之前和最后一個TestSuite之后執(zhí)行的。

全局事件

以上三種測試事件我們可以根據(jù)需要進行靈活使用。另外,細心的同學會發(fā)現(xiàn),這里測試用例我們該用了TEST_F這個宏,這是因為繼承了testing::Test,與之對應就需要使用TEST_F宏。

參數(shù)化

在學習gTest參數(shù)化之前我們先看一個測試例子。

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

class calcFunction
{
public:
int add(int a, int b)
{
std::cout << a << " + " << b << " = " << a + b << std::endl;
return a + b;
}

int sub(int a, int b)
{
std::cout << a << " - " << b << " = " << a - b << std::endl;
return a - b;
}
};

class calcFunctionTest : public testing::Test
{
protected:
calcFunction calc;
};

TEST_F(calcFunctionTest, test_add0)
{
EXPECT_EQ(calc.add(1,2), 3);
}

TEST_F(calcFunctionTest, test_add1)
{
EXPECT_EQ(calc.add(1,3), 4);
}

TEST_F(calcFunctionTest, test_add2)
{
EXPECT_EQ(calc.add(2,4), 6);
}

TEST_F(calcFunctionTest, test_add3)
{
EXPECT_EQ(calc.add(-1,-2), -3);
}

int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

示例執(zhí)行結果:


未參數(shù)化例子

上面的測試用例中我們寫了多個測試用例,但是其參數(shù)都是同樣的,有的實際應用場景可能比這個程序寫的測試檢查還要多。寫這么多重復的代碼實在是太累了。gTest提供了一個非常友好的工具,將這些測試的值進行參數(shù)化,就不用寫那么多重復的代碼了。

如何對其進行參數(shù)化呢?直接上代碼,我們再來看下面一個例子。

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

class calcFunction
{
public:
int add(int a, int b)
{
std::cout << a << " + " << b << " = " << a + b << std::endl;
return a + b;
}

int sub(int a, int b)
{
std::cout << a << " - " << b << " = " << a - b << std::endl;
return a - b;
}
};

struct TestParam
{
int a;
int b;
int c;
};

class calcFunctionTest : public ::testing::TestWithParam<struct TestParam>
{
protected:
calcFunction calc;
TestParam param;

virtual void SetUp()
{
param.a = GetParam().a;
param.b = GetParam().b;
param.c = GetParam().c;
}

};

TEST_P(calcFunctionTest, test_add)
{
EXPECT_EQ(calc.add(param.a, param.b), param.c);
}

INSTANTIATE_TEST_CASE_P(addTest, calcFunctionTest, ::testing::Values( TestParam{1, 2 , 3},
TestParam{1, 3 , 4},
TestParam{2, 4 , 6},
TestParam{-1, -2 , -3}));

int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

執(zhí)行結果和前面的例子一樣。


參數(shù)化例子

從這個例子中,我們不難發(fā)現(xiàn)和之前的測試程序有一些不同。這里繼承了::testing::TestWithParam類,參數(shù)T就是需要參數(shù)化的數(shù)據(jù)類型,這個例子里參數(shù)化數(shù)據(jù)類型是TestParam結構體。這里還需要使用另外一個宏TEST_P而不是TEST_F這個宏,它的兩個參數(shù)和TEST_F和TEST一致。另外,程序中還增加一個宏INSTANTIATE_TEST_CASE_P用來輸入測試參數(shù),它有三個參數(shù)(第一個參數(shù)大家可任意取名,第二個參數(shù)是test_case_name和TEST_P宏的名稱一致,第三個參數(shù)是需要傳遞的參數(shù))。

以上就是今天的所有內(nèi)容,感謝大家耐心的閱讀,希望大家都有所收獲,還有更多精彩內(nèi)容敬請關注,最后愿大家代碼無bug。

推薦器件

更多器件
器件型號 數(shù)量 器件廠商 器件描述 數(shù)據(jù)手冊 ECAD模型 風險等級 參考價格 更多信息
74LVXC3245MTC 1 Texas Instruments LV/LV-A/LVX/H SERIES, 8-BIT TRANSCEIVER, TRUE OUTPUT, PDSO24, 4.40 MM, PLASTIC, TSSOP-24
$0.98 查看
CY62167EV30LL-45ZXA 1 Cypress Semiconductor Standard SRAM, 1MX16, 45ns, CMOS, PDSO48, TSOP1-48
$15.18 查看
SP000063871 1 Avago Technologies FIBER OPTIC TRANSMITTER, 630-685nm, THROUGH HOLE MOUNT, ROHS COMPLIANT, PLASTIC, PACKAGE-2

ECAD模型

下載ECAD模型
$8.74 查看

相關推薦

電子產(chǎn)業(yè)圖譜