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

Will的大食堂發表於2020-11-14

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

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

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

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

安裝和配置

我們可以從github獲取Google Test的原始碼,如果大家沒有github賬號也可以從網盤下載。

github下載地址: https://github.com/google/googletest
網盤下載:關注公眾號【Will的大食堂】回覆【gTest下載】即可獲取下載地址。

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

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

原始碼工程目錄

開啟命令列工具cmd,進入原始碼的工程目錄,新建一個build目錄用來存放構建檔案,然後,進入build目錄執行cmake命令生成Makefile檔案。

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

執行cmake
生成makefile

Makefile檔案生成後,再執行下面的命令mingw32-make編譯庫檔案。編譯成功後就會發現有libgtest.a 和libgtest_main.a兩個靜態庫生成。這裡注意,Windows下mingw安裝的make工具名稱是mingw32-make而不是make。

mingw32-make

執行mingw32-make命令

接下來我們在VS Code寫一個測試用例,使用生成的gTest靜態庫測試下。按下快捷鍵【Ctrl+Shift+p】,在彈出的搜尋框中搜尋【C/C++:Edit Configurations】,可以建立c_cpp_properties.json配置檔案。

C/C++:Edit Configurations

在c_cpp_properties.json配置檔案新增gTest的標頭檔案目錄。

新增gTest標頭檔案目錄

在task.json配置檔案中新增gTest標頭檔案目錄和庫檔案,task.json配置檔案可以通過選單欄中Terminal選項下的【Configure Default Build Task】選項建立,可以參照之前的文章。

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是一個巨集,用來建立測試用例,它有test_case_name和test_name兩個引數。分別是測試用例名和測試名,在後面的文章中我們會對其有更深刻的理解,這裡就不細說了。RUN_ALL_TESTS也是一個巨集,它是測試用例的入口。EXPECT_EQ這個是一個斷言相關的巨集,用來檢測兩個數值是否相等。

執行結果

斷言

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

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

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

#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斷言檢查出被測函式add不滿足條件,所以程式就沒有繼續執行下去;而expect雖然檢查出被測試函式add不滿足條件,但是程式還是繼續去測試sub函式。

assert 和 expect

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

布林檢查

布林檢查主要用來檢查布林型別資料,檢查其條件是真還是假。

AssertExpectDescription
ASSERT_TRUE(condition)EXPECT_TRUE(condition)檢查條件是否為真
ASSERT_FALSE(condition)EXPECT_TRUE(condition)檢查條件是否為假

數值比較檢查

數值比較檢查主要用來比較兩個數值之間的大小關係,這裡有兩個引數。

AssertExpectDescription
ASSERT_EQ(val1, val2)EXPECT_EQ(val1, val2)檢查val1和val2是否相等
ASSERT_NE(val1, val2)EXPECT_NE(val1, val2)檢查val1和val2是否不相等
ASSERT_LT(val1, val2)EXPECT_LT(val1, val2)檢查val1是否小於val2
ASSERT_LE(val1, val2)EXPECT_LE(val1, val2)檢查val1是否小於等於val2
ASSERT_GT(val1, val2)EXPECT_GT(val1, val2)檢查val1是否大於val2
ASSERT_GE(val1, val2)EXPECT_GE(val1, val2)檢查val1是否大於val2

字串檢查

字串檢查主要用來比較字串的內容。

AssertExpectDescription
ASSERT_STREQ(str1, str2)EXPECT_STREQ(str1, str2)檢查str1和str2是否相等
ASSERT_STRNE(str1, str2)EXPECT_STRNE(str1, str2)檢查str1和str2是否不相等
ASSERT_STRCASEEQ(str1, str2)EXPECT_STRCASEEQ(str1, str2)檢查str1和str2是否相等(忽略大小寫)
ASSERT_STRCASENE(str1, str2)EXPECT_STRCASENE(str1, str2)檢查str1和str2是否不相等(忽略大小寫)

浮點數檢查

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

AssertExpectDescription
ASSERT_FLOAT_EQ(val1, val2)EXPECT_FLOAT_EQ(val1, val2)檢查兩個單精度的值是否相等
ASSERT_DOUBLE_EQ(val1, val2)EXPECT_DOUBLE_EQ(val1, val2)檢查兩個雙精度的值是否相等
ASSERT_NEAR(val1, val2, abs_error)EXPECT_NEAR(val1, val2, abs_error)檢查兩個浮點數絕對差不超過abs_error

異常檢查

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

AssertExpectDescription
ASSERT_THROW(statement, exception_type)EXPECT_THROW(statement, exception_type)檢查丟擲的給定的異常
ASSERT_ANY_THROW(statement)EXPECT_ANY_THROW(statement)檢查丟擲的任何異常
ASSERT_NO_THROW(statement)EXPECT_NO_THROW(statement)檢查不丟擲異常

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

顯示成功或失敗

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

  • 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(); 
} 

執行結果如下:

成功失敗斷言

死亡測試

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

AssertExpectDescription
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會優先執行。

死亡測試

測試事件

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

  • 測試程式是一個可執行程式,它有一個測試程式的入口main函式。
  • 測試用例是用來定義需要驗證的內容。
  • 測試套件是測試用例的集合,執行測試。

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

  • TestCase級別測試事件:這個級別的事件會在TestCase之前與之後執行;
  • TestSuite級別測試事件:這個級別的事件會在TestSuite中第一個TestCase之前與最後一個TestCase之後執行;
  • 全域性測試事件:這是級別的事件會在所有TestCase中第一個執行前,與最後一個之後執行。

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

TestCase測試事件

TestCase測試事件,需要實現兩個函式SetUp()和TearDown()。

  • SetUp()函式是在TestCase之前執行。
  • TearDown()函式是在TestCase之後執行。

這兩個函式是不是有點像類的建構函式和解構函式,但是切記他們並不是建構函式和解構函式,只是打個比方才這麼說而已。我們可以藉助下面的程式碼示例來加深對它的理解。這兩個函式是testing::Test的成員函式,我們在編寫測試類時需要繼承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(); 
} 

測試結果如下,兩個函式都是是在每個TestCase(test_add和test_sub)之前和之後執行。

TestCase事件

TestSuite測試事件

TestSuite測試事件,同樣的也需要實現的兩個函式SetUpTestCase()和TearDownTestCase(),而這兩個函式是靜態函式。這兩個靜態函式同樣也是testing::Test類的成員,我們直接改寫下測試類calcFunctionTest,新增兩個靜態函式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;

};

改寫好之後,我們再看一下執行結果。這兩個函式分別是在本TestSuite中的第一個TestCase之前和最後一個TestCase之後執行。

TestSuite事件

全域性測試事件

全域性測試事件,也需要繼承一個類,但是它需要繼承testing::Environment類實現SetUp()和TearDown()兩個函式。還需要在main函式中呼叫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(); 
} 

從測試結果上看,全域性事件的這兩個函式分別是在第一個TestSuite之前和最後一個TestSuite之後執行的。

全域性事件

以上三種測試事件我們可以根據需要進行靈活使用。另外,細心的同學會發現,這裡測試用例我們該用了TEST_F這個巨集,這是因為繼承了testing::Test,與之對應就需要使用TEST_F巨集。

引數化

在學習gTest引數化之前我們先看一個測試例子。


#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(); 
} 

示例執行結果:

未引數化例子

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

如何對其進行引數化呢?直接上程式碼,我們再來看下面一個例子。


#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(); 
} 

執行結果和前面的例子一樣。

引數化例子

從這個例子中,我們不難發現和之前的測試程式有一些不同。這裡繼承了::testing::TestWithParam類,引數T就是需要引數化的資料型別,這個例子裡引數化資料型別是TestParam結構體。這裡還需要使用另外一個巨集TEST_P而不是TEST_F這個巨集,它的兩個引數和TEST_F和TEST一致。另外,程式中還增加一個巨集INSTANTIATE_TEST_CASE_P用來輸入測試引數,它有三個引數(第一個引數大家可任意取名,第二個引數是test_case_name和TEST_P巨集的名稱一致,第三個引數是需要傳遞的引數)。

以上就是今天的所有內容,感謝大家耐心的閱讀,希望大家都有所收穫,願大家程式碼無bug。

感謝大家耐心的閱讀,希望都有所收穫,也歡迎大家關注微信公眾號【Will的大食堂】,一起交流學習。

在這裡插入圖片描述

相關文章