基於gtest、gmock、mockcpp和lcov的C語言LLT工程 —— LLT構造和lcov查看覆蓋率實例


由於工作原因,之前在CI這一塊一直是依照公司流程走的,LLT這一塊都是照貓畫虎,對於整體框架自己沒有一個完整的概念,最近有時間,研究了一下整體的邏輯框架,在此記錄一下。

關於gtest,gmock和mockcpp,這里不再細講,知道gtest,gmock是google的一套用於C/C++ LLT的框架即可,要用到mockcpp是因為gmock其實只能用於對對象函數的mocker,不能對C代碼中的一般函數進行mcoker,這個在后面的代碼中可以看出來。

一、准備工作

1. googletest、googlemock

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

2. mockcpp

https://code.google.com/archive/p/mockcpp/downloads

3. 編譯mockcpp

最新的2.6版本的mockcpp只需要解壓然后進入根目錄,然后:

cmake .  //生成Makefile

make install

 

 這里需要把libmockcpp.a保存下來,后面會用到;

/usr/local/include/文件夾下面的所有文件也需要保存下來;

4. 編譯googletest

直接在下載地根目錄進行

cmke .

make

即可得到gmock, gtest編譯結果和靜態庫:

 

 保存libgtest.a和libgmock.a后面用於靜態鏈接;

二、建立測試工程

1. 創建GtestLearn工程,這里是我們需要測試的代碼,目錄結構如下:

 

 mian.c

 1 #include <stdio.h>
 2 #include "func.h"
 3 
 4 int main(int argc, char **argv) {
 5     int ret = 0;
 6     struct test_t test;
 7 
 8     ret = add(1, 2);
 9     printf("Get add result: %d\n", ret);
10 
11     test.a = 10;
12     test.b = 12;
13     ret = add_struct(&test);
14     printf("Get add struct result: %d\n", ret);
15 
16     test.p_func = NULL;
17     ret = test_struct_func(&test);
18     printf("Get test struct result: %d\n", ret);
19 
20     ret = test_stub_func();
21     printf("Get test stub func result: %d\n", ret);
22 
23     return 0;
24 }

func.h

#ifndef GTESTLEARN_FUNC_H
#define GTESTLEARN_FUNC_H

struct test_t {
    int a;
    int b;
    int (*p_func)(struct test_t *test);
};

int add(int a, int b);
int multi(int a, int b);
int add_struct(struct test_t *test);
int test_struct_func(struct test_t *test);
int test_stub_func();

#endif //GTESTLEARN_FUNC_H

func.c

#include <stdio.h>
#include "ex_func.h"
#include "func.h"

int add(int a, int b)
{
    printf("start to compute the sum of a %d and b %d\n", a, b);
    return a + b;
}

int multi(int a, int b)
{
    printf("start to compute the multi of a %d and b %d\n", a, b);
    return a * b;
}

int add_struct(struct test_t *test)
{
    int sum;
    int multi_v;

    printf("start to compute the sum of a %d and b %d\n", test->a, test->b);

    sum = test->a + test->b;
    multi_v = multi(test->a, test->b);
    if (sum > multi_v) {
        return sum;
    }

    return multi_v;
}

int test_struct_func(struct test_t *test)
{
    if (test->p_func == NULL) {
        printf("get null func pointer\n");
        return 0xFFFF;
    }
    printf("start ro run test_struct_func with a %d b %d\n", test->a, test->b);
    return test->p_func(test);
}

int test_stub_func()
{
    int ret;
    int a = 0;

    ret = ex_get_value(&a);
    if (ret == 0xFFFF) {
        printf("get extern value failed, ret %d\n", ret);
        return ret;
    }

    printf("get extern value succeed, ex value %d\n", a);
    return ret;
}

ex_func.h

#ifndef GTESTLEARN_EX_FUNC_H
#define GTESTLEARN_EX_FUNC_H
int ex_get_value(int *a);
#endif //GTESTLEARN_EX_FUNC_H

ex_func.c

#include "ex_func.h"

int ex_get_value(int *a)
{
    *a = 101010;
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(GtestLearn C)

set(CMAKE_C_STANDARD 99)
set(CMAKE_C_FLAGS "${CAMKE_C_FLAGS} -fprofile-arcs -ftest-coverage")
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include/)
set(SOURCE_FILES
main.c
include/func.h
func.c
include/ex_func.h
include/ex_func.c)

add_executable(GtestLearn ${SOURCE_FILES})
target_link_libraries(GtestLearn ${LIBRARIES})

這里保證通過

cmake .

make

然后執行,預期正確。

2. 創建測試工程GtestLearnLLT

獨立一個工程是因為在正式開發環境中,做LLT的代碼往往是與業務代碼分離的,更好的模擬真實使用環境。

目錄結構:

這里將googlemock,googletest和mockcpp放在third_party文件夾,都是由之前下載的源碼解壓而來,其中:

mockcpp只需要保留3rdparty文件夾,include文件用之前編譯的頭文件代替(准備工作第3步);lib目錄里為之前編譯產生的靜態庫;

stubs文件夾保存的是樁函數;

googlemock和googletest新建lib目錄,存放之前編譯的靜態庫;

main.cpp

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

GTEST_API_ int main(int argc, char **argv) {
    printf("Running main() from gtest_main.cc\n");
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

gtest_ut.cpp

extern "C" {
#include <stdio.h>
#include <stdlib.h>
#include "func.h"
}

#include <limits.h>
#include <mockcpp/mockcpp.hpp>
#include "gmock/gmock.h"
#include "gtest/gtest.h"

// using namespace std;
using namespace testing;

class GtestUt : public testing::Test
{
protected:
    void SetUp() override
    {
        std::cout << "--Gtest_Ut SetUP--" << std::endl;
    }

    void TearDown() override
    {
        std::cout << "--Gtest_Ut TearDown--" << std::endl;
    }
};

class Mock_FOO {
public:
    MOCK_METHOD1(mock_test_struct_func, int(struct test_t *test));
};

Mock_FOO mocker;

int mock_test_struct_func(struct test_t *test)
{
    return mocker.mock_test_struct_func(test);
}

TEST_F(GtestUt, ut_add_01)
{
    int ret;

    ret = add(1, 2);
    EXPECT_EQ(3, ret);
}

TEST_F(GtestUt, ut_add_02)
{
    int ret;
    struct test_t test;

    test.a = 1;
    test.b = 1;

    MOCKER(multi)
    .expects(atMost(20))
    .will(returnValue(100));
    ret = add_struct(&test);
    EXPECT_EQ(ret, 100);
    GlobalMockObject::verify();
}

TEST_F(GtestUt, ut_add_03)
{
    int ret;
    struct test_t test;

    test.a = 10;
    test.b = 11;

    MOCKER(multi)
    .expects(atMost(20))
    .will(returnValue(20));
    ret = add_struct(&test);
    EXPECT_EQ(ret, 21);
    GlobalMockObject::verify();
}

TEST_F(GtestUt, ut_add_04)
{
    int ret;
    int a, b;
    struct test_t test;

    test.a = 10;
    test.b = 11;
    test.p_func = mock_test_struct_func;
    EXPECT_CALL(mocker, mock_test_struct_func(&test)).WillRepeatedly(Return(10));

    ret = test_struct_func(&test);
    EXPECT_EQ(ret, 10);
    GlobalMockObject::verify();
}

TEST_F(GtestUt, ut_add_05)
{
    int ret;
    int ex_value;

    ret = test_stub_func();
    EXPECT_EQ(ret, 1011);
}

my_stubs.c

#include <stdio.h>
#include "ex_func.h"

int ex_get_value(int *a)
{
    if (a == NULL) {
        printf("get null pointer %p\n", a);
        return 0xFFFF;
    }
    *a = 1011;
    printf("run stub func, get value %d\n", *a);
    return *a;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(GtestLearnLLT)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_FLAGS "${CAMKE_CXX_FLAGS} -std=c++11 -pthread -fprofile-arcs -ftest-coverage")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCONFIG_LLT -fprofile-arcs -ftest-coverage")
set(THIRD_PARTY_PATH ${CMAKE_CURRENT_SOURCE_DIR}/third_party)

link_directories(${THIRD_PARTY_PATH}/mockcpp/lib
${THIRD_PARTY_PATH}/googlemock/lib
${THIRD_PARTY_PATH}/googletest/lib)

#${THIRD_PARTY_PATH}/googletest/
include_directories(${THIRD_PARTY_PATH}/googletest/include/
${THIRD_PARTY_PATH}/googlemock/include/
${THIRD_PARTY_PATH}/mockcpp/include/
${THIRD_PARTY_PATH}/mockcpp/3rdparty/
${THIRD_PARTY_PATH}/googlemock/
${CMAKE_CURRENT_SOURCE_DIR}/../GtestLearn/include/
${CMAKE_CURRENT_SOURCE_DIR}/stubs/)

#${THIRD_PARTY_PATH}/googletest/src/gtest-all.cc
set(SRC_FILES
${CMAKE_CURRENT_SOURCE_DIR}/../GtestLearn/func.c
${CMAKE_CURRENT_SOURCE_DIR}/../GtestLearn/main.c
${CMAKE_CURRENT_SOURCE_DIR}/stubs/my_stubs.c
${THIRD_PARTY_PATH}/googlemock/src/gmock-all.cc
gtest_ut.cpp
main.cpp)

set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin")

add_executable(GtestLearnLLT ${SRC_FILES})
target_link_libraries(GtestLearnLLT libmockcpp.a libgmock.a libgtest.a)

編譯執行:

cmake .

make

執行結果:

 注意:這里還是在編譯的時候編譯了gmock而不是直接鏈接,是因為直接使用靜態庫,會導致mock對象泄露的問題,目前沒有定位到原因:

 

 

 三、代碼分析

1. 不需要打樁的普通函數LLT:

TEST_F(GtestUt, ut_add_01)
{
    int ret;

    ret = add(1, 2);
    EXPECT_EQ(3, ret);
}

這個比較簡單,沒有什么說的,依靠gtest框架,檢查函數功能的正確性;

2. 使用mockcpp來mocker一般C函數:

TEST_F(GtestUt, ut_add_02)
{
    int ret;
    struct test_t test;

    test.a = 1;
    test.b = 1;

    MOCKER(multi)
    .expects(atMost(20))
    .will(returnValue(100));
    ret = add_struct(&test);
    EXPECT_EQ(ret, 100);
    GlobalMockObject::verify();
}

使用mockcpp提供的MOCKER來對需要驗證的目標函數add_struct中的multi來進行mocker,進行打樁,常用來覆蓋不同的異常分支。

3. 使用自定義樁函數

 
         
int ex_get_value(int *a)
{
    if (a == NULL) { printf("get null pointer %p\n", a); return 0xFFFF; } *a = 1011; printf("run stub func, get value %d\n", *a); return *a; }
TEST_F(GtestUt, ut_add_05)
{
    int ret; int ex_value; ret = test_stub_func(); EXPECT_EQ(ret, 1011); }

對test_stub_func里面調用的ex_get_value來進行打樁,來實現自己想要完成的代碼邏輯;

4. 使用gmock來打樁對象類函數,本例中的對象類函數是放在結構體test_t中的:

struct test_t {
    int a;
    int b;
    int (*p_func)(struct test_t *test);
};

使用gmock打樁:

class Mock_FOO {
public:
    MOCK_METHOD1(mock_test_struct_func, int(struct test_t *test));
};

Mock_FOO mocker;

int mock_test_struct_func(struct test_t *test)
{
    return mocker.mock_test_struct_func(test);
}

用Mock_FOO類的mock_test_struct_func函數來mocker結構體test_t的成員函數p_func:

TEST_F(GtestUt, ut_add_04)
{
    int ret;
    int a, b;
    struct test_t test;

    test.a = 10;
    test.b = 11;
    test.p_func = mock_test_struct_func;
    EXPECT_CALL(mocker, mock_test_struct_func(&test)).WillRepeatedly(Return(10));

    ret = test_struct_func(&test);
    EXPECT_EQ(ret, 10);
    GlobalMockObject::verify();
}

代碼中 test.p_func = mock_test_struct_func; 使用Mock_FOO的函數來替代原結構體中的成員函數,通過 EXPECT_CALL  在test_struct_func函數調用結構體成員函數p_func的時候使用Mock_FOO的 mock_test_struct_func 來代替,達到打樁的目的

 

四、使用Lcov來查看LLT覆蓋率

1、lcov下載與安裝

wget http://downloads.sourceforge.net/ltp/lcov-1.9.tar.gz

tar -zxvf lcov-1.9.tar.gz

cd lcov-1.9

make install

2、LLT工程CMakeLists.txt修改

添加 -fprofile-arcs -ftest-coverage 為lcov的工作生成.gcda .gcno文件

set(CMAKE_CXX_FLAGS "${CAMKE_CXX_FLAGS} -std=c++11 -pthread -fprofile-arcs -ftest-coverage")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DCONFIG_LLT -fprofile-arcs -ftest-coverage")

3、lcov需要代碼運行之后才能生成.gcda和.gcno文件

編譯運行LLT工程之后可以看到生成的文件:

可以看到我們需要進行LLT的代碼main.c和func.c的gcda和gcno文件再CMakeFiles文件下;

4、使用lcov獲取代碼被執行次數的信息:

lcov -d ./CMakeFiles/GtestLearnLLT.dir/home/xxxx/GtestLearn/ -t bin/GtestLearnLLT -o test.info -b . -c

-d  指向你需要進行統計覆蓋率代碼所在的目錄

-t  為測試工程可執行文件所在目錄

-o 生成的輸出文件

-b 相對路徑,之前的路徑基於此路徑

-c 獲取覆蓋率信息

 5、使用genhtml生成網頁

genhtml -o result/ test.info

-o 指向生成的html信息存放的目錄

test.info是之前生成的包含覆蓋率信息的文件

6、查看生成的目錄

 

7、使用瀏覽器打開index.html

點開一個文件查看LLT覆蓋情況:

遇到的問題:

*.gcda:stamp mismatch with notes file

解決方法:

當.gcda文件比.gcno文件更新時,您可能會收到“戳記不匹配”.

使用 hexdump -e '"%x\n"' -s8 -n4 main.c.gcda 查看時間戳,會發現gcda文件和gcno文件時間戳不一致;

它主要有兩個原因:
1.您可能在運行測試之后和跟蹤文件生成之前重新構建代碼.
2.二進制文件可以在一台機器上構建,測試在其他機器上運行,其時間早於構建機器.

我的解決方法:

rm -rf CMakeFiles/    徹底刪除這個文件夾,刪除所有.gcda和 .gcno文件

然后重新編譯和執行LLT工程就可以了

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM