人妻系列无码专区av在线,国内精品久久久久久婷婷,久草视频在线播放,精品国产线拍大陆久久尤物

c語言double(C語言的單元測試與代碼覆蓋率)

c語言double(C語言的單元測試與代碼覆蓋率)

秋伶伶 2025-04-11 科技 12 次瀏覽 0個(gè)評(píng)論

對(duì)代碼進(jìn)行單元測試是幾乎每個(gè)軟件工程師都要完成的工作。本文以C++語言為基礎(chǔ),講解如何進(jìn)行單元測試并生成測試報(bào)告。

前言

測試是軟件開發(fā)過程中一個(gè)必須的環(huán)節(jié),測試確保軟件的質(zhì)量符合預(yù)期。

對(duì)于工程師自己來說,單元測試也是提升自信心的一種方式。

直接交付沒有經(jīng)過測試的代碼是不太好的,因?yàn)檫@很可能會(huì)浪費(fèi)整個(gè)團(tuán)隊(duì)的時(shí)間,在一些原本早期就可以發(fā)現(xiàn)的問題上。而單元測試,就是發(fā)現(xiàn)問題一個(gè)很重要的環(huán)節(jié)。

本文以C++語言為基礎(chǔ),講解如何進(jìn)行單元測試并生成測試報(bào)告。

在工具上,我們會(huì)使用下面這些:

GCCCMakeGoogle Testgcovlcov演示項(xiàng)目

為了方便本文的講解,我專門編寫了一個(gè)演示項(xiàng)目作為代碼示例。

演示項(xiàng)目的源碼可以在我的Github上獲?。簆aulQuei/gtest-and-coverage。

你可以通過下面幾條命令下載和運(yùn)行這個(gè)項(xiàng)目:

git clone https://github.com/paulQuei/gtest-and-coverage.gitcd gtest-and-coverage./make_all.sh

要運(yùn)行這個(gè)項(xiàng)目,你的機(jī)器上必須先安裝好前面提到的工具。如果沒有,請(qǐng)閱讀下文以了解如何安裝它們。

如果你使用的是Mac系統(tǒng),下文假設(shè)你的系統(tǒng)上已經(jīng)安裝了brew包管理器。如果沒有,請(qǐng)通過下面這條命令安裝它:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"項(xiàng)目結(jié)構(gòu)

演示項(xiàng)目的目錄結(jié)構(gòu)如下:

.├── CMakeLists.txt├── googletest-release-1.8.1.zip├── include│ └── utility.h├── make_all.sh├── src│ └── utility.cpp└── test └── unit_test.cpp

這里演示的內(nèi)容是:以測試一個(gè)我們要提供的軟件庫為例,講解如何對(duì)其進(jìn)行單元測試并生成測試報(bào)告。

為了簡單起見,這個(gè)軟件庫只有一個(gè)頭文件和一個(gè)實(shí)現(xiàn)文件。

當(dāng)然,在實(shí)際上的項(xiàng)目中,一個(gè)軟件庫會(huì)通常包含更多的文件,不過這并不影響我們要說明的問題。

演示項(xiàng)目中的文件說明如下:

文件名稱說明make_all.sh入口文件,會(huì)執(zhí)行:編譯,測試和生成報(bào)告等所有工作CMakeLists.txt項(xiàng)目的編譯文件googletest-release-1.8.1.zipgoogle test源碼壓縮包utility.h待測試的軟件庫的頭文件utility.cpp待測試的軟件庫的實(shí)現(xiàn)文件unit_test.cpp對(duì)軟件庫進(jìn)行單元測試的代碼

測試環(huán)境

演示項(xiàng)目在如下的環(huán)境中測試過。

MacBook Pro操作系統(tǒng):macOS Mojave 10.14.1編譯器:Apple LLVM version 10.0.0 (clang-1000.11.45.2)CMake:cmake version 3.12.1Google Test: 1.8.1lcov: lcov version 1.13Ubuntu操作系統(tǒng):Ubuntu 16.04.5 LTS編譯器:gcc (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609CMake:cmake version 3.5.1Google Test:1.8.1lcov:lcov version 1.12關(guān)于CMake

為了簡化編譯的過程,這里使用CMake作為編譯工具。關(guān)于CMake的更多內(nèi)容請(qǐng)參見請(qǐng)官網(wǎng):https://cmake.org。

關(guān)于如何安裝CMake請(qǐng)參見這里:Installing CMake。

另外,你也可以通過一條簡單的命令來安裝CMake:

Mac系統(tǒng):

brew install cmake

Ubuntu系統(tǒng)

sudo apt install cmake

由于篇幅所限,這里不打算對(duì)CMake做過多講解,讀者可以訪問其官網(wǎng)或者在網(wǎng)絡(luò)上搜尋其使用方法。

這里僅僅對(duì)演示項(xiàng)目中用到的內(nèi)容做一下說明。演示項(xiàng)目中的CMakeLists.txt內(nèi)容如下:

cmake_minimum_required(VERSION 2.8.11) ①project(utility) ②set(CMAKE_CXX_STANDARD 11) ③set(GTEST googletest-release-1.8.1) ④include_directories("./include" "${GTEST}/googletest/include/")link_directories("build/gtest/googlemock/gtest/")SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage") ⑤add_library(${CMAKE_PROJECT_NAME}_lib src/utility.cpp) ⑥add_executable(unit_test test/unit_test.cpp) ⑦target_link_libraries(unit_test ${CMAKE_PROJECT_NAME}_lib gtest gtest_main pthread) ⑧

以編號(hào)為序,這段代碼說明如下:

設(shè)置使用的CMake最低版本號(hào)為2.8.11。指定項(xiàng)目的名稱為”utility”,項(xiàng)目名稱可以通過${CMAKE_PROJECT_NAME}進(jìn)行引用。指定使用C++11。這里的三行是編譯google test,并將其頭文件路徑和編譯結(jié)果的庫文件路徑添加到環(huán)境中。因?yàn)楹竺嬖诰幾g單元測試代碼的時(shí)候需要用到。添加--coverage到編譯器flag中,這個(gè)參數(shù)是很重要的,因?yàn)檫@是生成代碼覆蓋率所必須的。關(guān)于該編譯參數(shù)的說明見這里:Program Instrumentation Options。編譯我們的軟件庫,這里將生成libutility_lib.a庫文件。編譯單元測試的可執(zhí)行文件。單元測試的可執(zhí)行文件需要鏈接我們開發(fā)的軟件庫以及google test的庫。另外,google test依賴了pthread,所以這個(gè)庫也需要。關(guān)于測試

軟件測試有很多種分類方式。從測試的級(jí)別來說,可以大致分為:

單元測試集成測試系統(tǒng)測試

這其中,單元測試是最局部和具體的。它通常需要對(duì)代碼中的每一個(gè)類和函數(shù)進(jìn)行測試。

單元測試通常由開發(fā)者完成,需要針對(duì)代碼邏輯進(jìn)行測試。所以它是一種白盒測試。

關(guān)于xUnit

xUnit是幾種單元測試框架的總稱。最早源于Smalltalk的單元測試框架SUnit,它是由Kent Beck開發(fā)的。

除此之外,還有針對(duì)Java語言的JUnit,針對(duì)R語言的RUnit。

在本文中,我們使用Google開發(fā)的xUnit框架:Google Test。

Google Test介紹

Google Test的項(xiàng)目主頁在Github上:Github: Google Test。

實(shí)際上,這個(gè)項(xiàng)目中同時(shí)包含了GoogleTest和GoogleMock兩個(gè)工具,本文中我們只會(huì)講解第一個(gè)。

Google Test支持的操作系統(tǒng)包含下面這些:

LinuxMac OS XWindowsCygwinMinGWWindows MobileSymbian

目前有很多的項(xiàng)目都使用了Google Test,例如下面這些:

Chromium projectsLLVMProtocol BuffersOpenCVtiny-dnn編譯Google Test

關(guān)于如何編譯Google Test請(qǐng)參見這里:Generic Build Instructions。

為了便于讀者使用,我們?cè)谘菔卷?xiàng)目中包含了Google Test 1.8.1的源碼壓縮包。并且在CMake文件中,同時(shí)包含了Google Test的編譯和使用配置工作。

如果使用演示項(xiàng)目,讀者將不需要手動(dòng)處理Google Test的編譯和安裝工作。

使用Google Test演示項(xiàng)目代碼說明

為了便于下文說明,演示項(xiàng)目中包含了幾個(gè)簡單的函數(shù)。

可以從這里下載源碼以便查看其中的內(nèi)容:paulQuei/gtest-and-coverage。

演示項(xiàng)目中的軟件庫包含一個(gè)頭文件和一個(gè)實(shí)現(xiàn)文件。頭文件內(nèi)容如下:

// utility.h#ifndef INCLUDE_UTILITY_#define INCLUDE_UTILITY_enum CalcType { ADD, MINUS, MULTIPLE, DIVIDE};class Utility {public: int ArithmeticCalculation(CalcType op, int a, int b); double ArithmeticCalculation(CalcType op, double a, double b); bool IsLeapYear(int year);};#endif

這個(gè)頭文件說明如下:

頭文件包含了三個(gè)函數(shù),前兩個(gè)用來做int和double類型的四則運(yùn)算。最后一個(gè)判斷輸入的年份是否是閏年。當(dāng)然,在實(shí)際的工程中,前兩個(gè)函數(shù)合并實(shí)現(xiàn)為一個(gè)泛型函數(shù)更為合適。但這里之所以分成兩個(gè),是為了查看代碼覆蓋率所用。關(guān)于閏年說明如下:能被4整除但不能被100整除的年份為普通閏年。能被100整除,也同時(shí)能被400整除的為世紀(jì)閏年。其他都不是閏年。例如:1997年不是閏年,2000年是閏年,2016年是閏年,2100不是閏年。

這三個(gè)函數(shù)的實(shí)現(xiàn)也不復(fù)雜:

// utility.cpp#include "utility.h"#include <iostream>#include <limits>using namespace std;int Utility::ArithmeticCalculation(CalcType op, int a, int b) { if (op == ADD) { return a + b; } else if (op == MINUS) { return a - b; } else if (op == MULTIPLE) { return a * b; } else { if (b == 0) { cout << "CANNO Divided by 0" << endl; return std::numeric_limits<int>::max(); } return a / b; }}double Utility::ArithmeticCalculation(CalcType op, double a, double b) { if (op == ADD) { return a + b; } else if (op == MINUS) { return a - b; } else if (op == MULTIPLE) { return a * b; } else { if (b == 0) { cout << "CANNO Divided by 0" << endl; return std::numeric_limits<double>::max(); } return a / b; }}bool Utility::IsLeapYear(int year) { if (year % 100 == 0 && year % 400 == 0) { return true; } if (year % 100 != 0 && year % 4 == 0) { return true; } return false;}開始測試

接下來我們就要對(duì)上面這些代碼進(jìn)行測試了。

要使用Google Test進(jìn)行測試,整個(gè)過程也非常的簡單。只要進(jìn)行下面三部:

創(chuàng)建一個(gè)測試用的cpp文件為上面這個(gè)測試用的cpp文件編寫Makefile(或者CMake文件)。同時(shí)鏈接:待測試的軟件庫gtest庫gtest_main庫pthread庫(Google Test使用了這個(gè)庫所以需要)編寫測試代碼,編譯并運(yùn)行測試的可執(zhí)行程序。

并且,測試代碼寫起來也非常的簡單,像下面這樣:

#include "utility.h"#include "gtest/gtest.h"TEST(TestCalculationInt, ArithmeticCalculationInt) { Utility util; EXPECT_EQ(util.ArithmeticCalculation(ADD, 1, 1), 2); EXPECT_EQ(util.ArithmeticCalculation(MINUS, 2, 1), 1); EXPECT_EQ(util.ArithmeticCalculation(MULTIPLE, 3, 3), 9); EXPECT_EQ(util.ArithmeticCalculation(DIVIDE, 10, 2), 5); EXPECT_GT(util.ArithmeticCalculation(DIVIDE, 10, 0), 999999999);}

是的,就是這么簡單的幾行代碼,就對(duì)整數(shù)四則運(yùn)算的函數(shù)進(jìn)行了測試。

TEST后面所包含的內(nèi)容稱之為一條case,通常我們會(huì)為每個(gè)函數(shù)創(chuàng)建一個(gè)獨(dú)立的case來進(jìn)行測試。一個(gè)測試文件中可以包含很多條case。同時(shí),一條case中會(huì)包含很多的判斷(例如EXPECT_EQ...)。

注意:在做單元測試的時(shí)候,保證每條case是獨(dú)立的,case之間沒有前后依賴關(guān)系是非常重要的。

當(dāng)然,測試代碼中包含的判斷的多少將影響測試結(jié)果的覆蓋率。所以在編寫每條case的時(shí)候,我們需要仔細(xì)思考待測試函數(shù)的可能性,有針對(duì)性的進(jìn)行測試代碼的編寫。

這段代碼應(yīng)該很好理解,它分別進(jìn)行了下面這些測試:

1 + 1 = 22 – 1 = 13 x 3 = 910 / 2 = 510 / 0 > 999999999

你可能會(huì)發(fā)現(xiàn),這段代碼里面甚至沒有main函數(shù)。它也依然可以生成一個(gè)可執(zhí)行文件。這就是我們鏈接gtest_main所起的作用。

在實(shí)際的測試過程中,你想判斷的情況可能不止上面這么簡單。下面我們來看看Google Test還能做哪些測試。

測試判斷

Google Test對(duì)于結(jié)果的判斷,有兩種形式:

ASSERT_*:這類判斷是Fatal的。一旦這個(gè)判斷出錯(cuò),則直接從測試函數(shù)中返回,不會(huì)再繼續(xù)后面的測試。EXPECT_*:這類判斷是Nonfatal的。它的效果是,如果某個(gè)判斷出錯(cuò),則輸出一個(gè)錯(cuò)誤信息,但是接下來仍然會(huì)繼續(xù)執(zhí)行后面的測試。

可以進(jìn)行的判斷方法主要有下面這些:

布爾判斷

FatalNonfatal說明ASSERT_TRUE(condition)EXPECT_TRUE(condition)判斷 condition 為 trueASSERT_FALSE(condition)EXPECT_FALSE(condition)判斷 condition 為 false

二進(jìn)制判斷

FatalNonfatal說明ASSERT_EQ(expected, actual)EXPECT_EQ(expected, actual)判斷兩個(gè)數(shù)值相等ASSERT_NE(val1, val2)EXPECT_NE(val1, val2)val1 != val2ASSERT_LT(val1, val2)EXPECT_LT(val1, val2)val1 < val2ASSERT_LE(val1, val2)EXPECT_LE(val1, val2)val1 <= val2ASSERT_GT(val1, val2)EXPECT_GT(val1, val2)val1 > val2ASSERT_GE(val1, val2)EXPECT_GE(val1, val2)val1 >= val2

說明:

EQ:EQualNE:Not EqualLT:Less ThanLE:Less EqualGT:Greater ThanGE:Greater Equal

字符串判斷

FatalNonfatal說明ASSERT_STREQ(expected, actual)EXPECT_STREQ(expected, actual)兩個(gè)C string相同ASSERT_STRNE(str1, str2)EXPECT_STRNE(str1, str2)兩個(gè)C string不相同ASSERT_STRCASEEQ(exp, act)EXPECT_STRCASEEQ(exp, act)忽略大小寫,兩個(gè)C string相同ASSERT_STRCASENE(str1, str2)EXPECT_STRCASENE(str1, str2)忽略大小寫,兩個(gè)C string不相同

浮點(diǎn)數(shù)判斷

FatalNonfatal說明ASSERT_FLOAT_EQ(exp, act)EXPECT_FLOAT_EQ(exp, act)兩個(gè)float數(shù)值相等ASSERT_DOUBLE_EQ(exp, act)EXPECT_DOUBLE_EQ(exp, act)兩個(gè)double數(shù)值相等ASSERT_NEAR(val1, val2, abs_err)EXPECT_NEAR(val1, val2, abs_err)val1和val2的差距不超過abs_err

異常判斷

FatalNonfatal說明ASSERT_THROW(stmt, exc_type)EXPECT_THROW(stmt, exc_type)stmt拋出了exc_type類型的異常ASSERT_ANY_THROW(stmt)EXPECT_ANY_THROW(stmt)stmt拋出了任意類型的異常ASSERT_NO_THROW(stmt)EXPECT_NO_THROW(stmt)stmt沒有拋出異常

Test Fixture

在某些情況下,我們可能希望多條測試case使用相同的測試數(shù)據(jù)。例如,我們的演示項(xiàng)目中,每條case都會(huì)需要?jiǎng)?chuàng)建Utility對(duì)象。

有些時(shí)候,我們要測試的對(duì)象可能很大,或者創(chuàng)建的過程非常的慢。這時(shí),如果每條case反復(fù)創(chuàng)建這個(gè)對(duì)象就顯得浪費(fèi)資源和時(shí)間了。此時(shí),我們可以使用Test Fixture來共享測試的對(duì)象。

要使用Test Fixture我們需要?jiǎng)?chuàng)建一個(gè)類繼承自Google Test中的::testing::Test。

還記得我們前面說過,我們要盡可能的保證每條測試case是互相獨(dú)立的。但是,當(dāng)我們?cè)诙鄺lcase之間共享有狀態(tài)的對(duì)象時(shí),就可能出現(xiàn)問題。

例如,我們要測試的是一個(gè)隊(duì)列數(shù)據(jù)結(jié)構(gòu)。有的case會(huì)向隊(duì)列中添加數(shù)據(jù),有的case會(huì)從隊(duì)列中刪除數(shù)據(jù)。case執(zhí)行的順序不同,則會(huì)導(dǎo)致Queue中的數(shù)據(jù)不一樣,這就可能會(huì)影響case的結(jié)果。

為了保證每條case是獨(dú)立的,我們可以在每條case的執(zhí)行前后分別完成準(zhǔn)備工作和清理工作,例如,準(zhǔn)備工作是向隊(duì)列中添加三個(gè)數(shù)據(jù),而清理工作是將隊(duì)列置空。

這兩項(xiàng)重復(fù)性的工作可以由::testing::Test類中的Setup和TearDown兩個(gè)函數(shù)來完成。

我們演示用的Utility類是無狀態(tài)的,所以不存在這個(gè)問題。因此,這里我們僅僅在Setup和TearDown兩個(gè)函數(shù)中打印了一句日志。

使用Test Fixture后,我們的代碼如下所示:

class UtilityTest : public ::testing::Test {protected:void SetUp() override { cout << "SetUp runs before each case." << endl;}void TearDown() override { cout << "TearDown runs after each case." << endl;}Utility util;};

這段代碼說明如下:

Setup和TearDown兩個(gè)函數(shù)標(biāo)記了override以確認(rèn)是重寫父類中的方法,這是C++11新增的語法。我們的Utility類是無狀態(tài)的,因此Setup和TearDown兩個(gè)函數(shù)中我們僅僅打印日志以便確認(rèn)。將Utility util設(shè)置為protected以便測試代碼中可以訪問。(從實(shí)現(xiàn)上來說,測試case的代碼是從這個(gè)類繼承的子類,當(dāng)然,這個(gè)關(guān)系是由Google Test工具完成的)。

要使用這里定義的Test Fixture,測試case的代碼需要將開頭的TEST變更為TEST_F。

這里_F就是Fixture的意思。

使用TEST_F的case的代碼結(jié)構(gòu)如下:

TEST_F(TestCaseName, TestName) { ... test body ...}

這里的TestCaseName必須是Test Fixture的類名。

所以我們的測試代碼寫起來是這樣:

TEST_F(UtilityTest, ArithmeticCalculationDouble) { EXPECT_EQ(util.ArithmeticCalculation(ADD, 1.1, 1.1), 2.2);}TEST_F(UtilityTest, ArithmeticCalculationIsLeapYear) { EXPECT_FALSE(util.IsLeapYear(1997)); EXPECT_TRUE(util.IsLeapYear(2000)); EXPECT_TRUE(util.IsLeapYear(2016)); EXPECT_FALSE(util.IsLeapYear(2100));}

我們針對(duì)ArithmeticCalculation方法故意只進(jìn)行了一種情況的測試。這是為了最終生成代碼覆蓋率所用。

運(yùn)行測試

編寫完單元測試之后,再執(zhí)行編譯工作便可以運(yùn)行測試程序以查看測試結(jié)果了。

測試的結(jié)果像下面這樣:

C++語言的單元測試與代碼覆蓋率

如果測試中包含了失敗的case,則會(huì)以紅色的形式輸出。同時(shí),會(huì)看到失敗的case所處的源碼行數(shù),這樣可以很方便的知道哪一個(gè)測試失敗了,像下面這樣:

C++語言的單元測試與代碼覆蓋率

只想有選擇性的跑部分case,可以通過--gtest_filter參數(shù)進(jìn)行過濾,這個(gè)參數(shù)支持*通配符。

像下面這樣:

$ ./build/unit_test --gtest_filter=*ArithmeticCalculationIntRunning main() from googletest/src/gtest_main.ccNote: Google Test filter = *ArithmeticCalculationInt[==========] Running 1 test from 1 test case.[----------] Global test environment set-up.[----------] 1 test from TestCalculationInt[ RUN ] TestCalculationInt.ArithmeticCalculationIntCANNO Divided by 0[ OK ] TestCalculationInt.ArithmeticCalculationInt (0 ms)[----------] 1 test from TestCalculationInt (0 ms total)[----------] Global test environment tear-down[==========] 1 test from 1 test case ran. (0 ms total)[ PASSED ] 1 test.

如果想要更好的理解這些內(nèi)容。請(qǐng)讀者下載演示項(xiàng)目之后完成下面這些操作:

在utility.h和utility.cpp中添加一些新的函數(shù)。在新添加的函數(shù)中故意包含一個(gè)bug。為新添加的函數(shù)編寫測試代碼,并測試出函數(shù)中包含的bug。

代碼覆蓋率

在進(jìn)行單元測試之后,我們當(dāng)然希望能夠直觀的看到我們的測試都覆蓋了哪些代碼。

理論上,如果我們能做到100%的覆蓋我們的所有代碼,則可以說我們的代碼是沒有Bug的。

但實(shí)際上,100%的覆蓋率要比想象得困難。對(duì)于大型項(xiàng)目來說,能夠達(dá)到80% ~ 90%的語句覆蓋率就已經(jīng)很不錯(cuò)了。

覆蓋率的類型

先來看一下,當(dāng)我們?cè)谡f“覆蓋率”的時(shí)候我們到底是指的什么。

實(shí)際上,代碼覆蓋率有下面幾種類型:

函數(shù)覆蓋率:描述有多少比例的函數(shù)經(jīng)過了測試。語句覆蓋率:描述有多少比例的語句經(jīng)過了測試。分支覆蓋率:描述有多少比例的分支(例如:if-else,case語句)經(jīng)過了測試。條件覆蓋率:描述有多少比例的可能性經(jīng)過了測試。

這其中,函數(shù)覆蓋率最為簡單,就不做說明了。

語句覆蓋率是我們最常用的。因?yàn)樗苤庇^的對(duì)應(yīng)到我們寫的每一行代碼。

而分支覆蓋率和條件覆蓋率可能不太好理解,需要做一下說明。

以下面這個(gè)C語言函數(shù)為例:

int foo (int x, int y) { int z = 0; if ((x > 0) && (y > 0)) { z = x; } return z;}

這個(gè)函數(shù)中包含了一個(gè)if語句,因此if語句成立或者不成立構(gòu)成了兩個(gè)分支。所以如果只測試了if成立或者不成立的其中之一,其分支覆蓋率只有 1/2 = 50%。

而條件覆蓋率需要考慮每種可能性的情況。

對(duì)于if (a && b)這樣的語句,其一共有四種可能的情況:

a = true, b = truea = true, b = falsea = false, b = truea = false, b = false

請(qǐng)讀者思考一下:對(duì)于三層if嵌套,每個(gè)if語句包含三個(gè)布爾變量的代碼,如果要做到100%的條件覆蓋率,一共要測試多少種情況。

很顯示,在編寫代碼的時(shí)候,盡可能的減少代碼嵌套,并且簡化邏輯運(yùn)算是一項(xiàng)很好的習(xí)慣。

便于測試的代碼也是便于理解和維護(hù)的,反之則反。

有了這些概念之后,我們就可以看懂測試報(bào)告中的覆蓋率了。

gcov

gcov是由GCC工具鏈提供的代碼覆蓋率生成工具。它可以很方便的和GCC編譯器配合使用。

通常情況下,安裝好GCC工具鏈,也就同時(shí)包含了gcov命令行工具。

對(duì)于代碼覆蓋率工具所做的工作,可以簡單的理解為:標(biāo)記一次運(yùn)行過程中,哪些代碼被執(zhí)行過,哪些沒有執(zhí)行。

因此,即便沒有測試代碼,直接運(yùn)行編譯產(chǎn)物也可以得到代碼的覆蓋率。只不過,通常情況下這樣得到的覆蓋率較低罷了。

使用

這里我們以另外一個(gè)簡單的代碼示例來說明gcov的使用。

這段代碼如下:

// test.c#include <stdio.h>int main (void) { for (int i = 1; i < 10; i++) { if (i % 3 == 0) printf ("%d is divisible by 3\n", i); if (i % 11 == 0) printf ("%d is divisible by 11\n", i); } return 0;}

這是一個(gè)僅僅包含了main函數(shù)的c語言代碼,main函數(shù)的邏輯也很簡單。

我們將這段代碼保存到文件test.c。

要通過gcov生成代碼覆蓋率。需要在編譯時(shí),增加參數(shù)--coverage:

gcc --coverage test.c

--coverage等同于編譯參數(shù)-fprofile-arcs -ftest-coverage以及在鏈接時(shí)增加-lgcov。

此處的編譯結(jié)果除了得到可執(zhí)行文件a.out,還會(huì)得到一個(gè)test.gcno文件。該文件包含了代碼與行號(hào)的信息,在生成覆蓋率時(shí)會(huì)需要這個(gè)文件。

很顯然,帶--coverage編譯參數(shù)得到的編譯產(chǎn)物會(huì)比不帶這個(gè)參數(shù)要包含更多的信息,因此編譯產(chǎn)物會(huì)更大。所以這個(gè)參數(shù)只適合在需要生成代碼覆蓋率的時(shí)候才加上。對(duì)于正式發(fā)布的編譯產(chǎn)物,不應(yīng)該添加這個(gè)編譯參數(shù)。

當(dāng)我們執(zhí)行上面編譯出來的可執(zhí)行文件a.out時(shí),我們還會(huì)得到每個(gè)源碼文件對(duì)應(yīng)的gcda后綴的文件。由test.gcno和test.gcda這兩個(gè)文件,便可以得到代碼的覆蓋率結(jié)果了。

關(guān)于這兩個(gè)文件的說明請(qǐng)參見這里:Brief description of gcov data files

只需要通過gcov指定源文件的名稱(不需要帶后綴):gcov test,便可以得到包含覆蓋率的結(jié)果文件 test.c.gcov了。

回顧一下我們剛剛的操作內(nèi)容:

$ gcc --coverage test.c$ lltotal 72-rwxr-xr-x 1 Paul staff 26K 11 10 14:41 a.out-rw-r--r-- 1 Paul staff 240B 11 10 14:41 test.c-rw-r--r-- 1 Paul staff 720B 11 10 14:41 test.gcno$ ./a.out 3 is divisible by 36 is divisible by 39 is divisible by 3$ lltotal 80-rwxr-xr-x 1 Paul staff 26K 11 10 14:41 a.out-rw-r--r-- 1 Paul staff 240B 11 10 14:41 test.c-rw-r--r-- 1 Paul staff 212B 11 10 14:42 test.gcda-rw-r--r-- 1 Paul staff 720B 11 10 14:41 test.gcno$ gcov testFile 'test.c'Lines executed:85.71% of 7test.c:creating 'test.c.gcov'$ lltotal 88-rwxr-xr-x 1 Paul staff 26K 11 10 14:41 a.out-rw-r--r-- 1 Paul staff 240B 11 10 14:41 test.c-rw-r--r-- 1 Paul staff 623B 11 10 14:42 test.c.gcov-rw-r--r-- 1 Paul staff 212B 11 10 14:42 test.gcda-rw-r--r-- 1 Paul staff 720B 11 10 14:41 test.gcno

我們可以cat test.c.gcov一下,查看覆蓋率的結(jié)果:

-: 0:Source:test.c -: 0:Graph:test.gcno -: 0:Data:test.gcda -: 0:Runs:1 -: 0:Programs:1 -: 1:// test.c -: 2: -: 3:#include <stdio.h> -: 4: -: 5:int main (void) { -: 6: 20: 7: for (int i = 1; i < 10; i++) { 9: 8: if (i % 3 == 0) 3: 9: printf ("%d is divisible by 3\n", i); 9: 10: if (i % 11 == 0) #####: 11: printf ("%d is divisible by 11\n", i); 9: 12: } -: 13: 1: 14: return 0; -: 15:}

這個(gè)結(jié)果應(yīng)該還是很容易理解的,最左邊一列描述了代碼的覆蓋情況:

-: 表示該行代碼被覆蓋了整數(shù): 表示被執(zhí)行的次數(shù)#####:表示該行沒有被覆蓋lcov

gcov得到的結(jié)果是本文形式的。但很多時(shí)候,我們可能希望得到更加美觀和便于瀏覽的結(jié)果。

此時(shí)就可以使用lcov了。

lcov是gcov工具的圖形前端。它收集多個(gè)源文件的gcov數(shù)據(jù),并生成描述覆蓋率的HTML頁面。生成的結(jié)果中會(huì)包含概述頁面,以方便瀏覽。

lcov支持我們前面提到的所有四種覆蓋率。

這個(gè)鏈接是lcov生成的報(bào)告樣例:lcov – code coverage report。

安裝

lcov并非包含在GCC中,因此需要單獨(dú)安裝。

Mac系統(tǒng)

brew install lcov

Ubuntu系統(tǒng)

sudo apt install lcov

使用

對(duì)于lcov的使用方法可以通過下面這條命令查詢:

lcov --help

通過輸出我們可以看到,這個(gè)命令的參數(shù)有簡短(例如-c)和完整(例如--capture)兩種形式,其作用是一樣的。

這里主要關(guān)注的下面這幾個(gè)參數(shù):

-c 或者 --capture 指定從編譯產(chǎn)物中收集覆蓋率信息。-d DIR 或者 --directory DIR 指定編譯產(chǎn)物的路徑。-e FILE PATTERN 或者 --extract FILE PATTERN 從指定的文件中根據(jù)PATTERN過濾結(jié)果。-o FILENAME 或者 --output-file FILENAME 指定覆蓋率輸出的文件名稱。

另外還有需要說明的是:

lcov默認(rèn)不會(huì)打開分支覆蓋率,因此我們還需要增加這個(gè)參數(shù)來打開分支覆蓋率的計(jì)算:--rc lcov_branch_coverage=1lcov輸出的仍然是一個(gè)中間產(chǎn)物,我們還需要通過lcov軟件包提供的另外一個(gè)命令genhtml來生成最終需要的html格式的覆蓋率報(bào)告文件。同樣的,為了打開分支覆蓋率的計(jì)算,我們也要為這個(gè)命令增加--rc lcov_branch_coverage=1參數(shù)

最后,make_all.sh腳本中包含的相關(guān)內(nèi)容如下:

COVERAGE_FILE=coverage.infoREPORT_FOLDER=coverage_reportlcov --rc lcov_branch_coverage=1 -c -d build -o ${COVERAGE_FILE}_tmplcov --rc lcov_branch_coverage=1 -e ${COVERAGE_FILE}_tmp "*src*" -o ${COVERAGE_FILE}genhtml --rc genhtml_branch_coverage=1 ${COVERAGE_FILE} -o ${REPORT_FOLDER}

這段代碼從我們前面編譯的結(jié)果中收集覆蓋率結(jié)果,并將結(jié)果輸出到coverage.info_tmp文件中。但是這里面會(huì)包含非項(xiàng)目源碼的覆蓋率(例如google test),所以我們又通過另外一條命令來指定”src”文件夾進(jìn)行過濾。最后,通過genhtml得到html格式的報(bào)告。

可以通過瀏覽器查看覆蓋率報(bào)告的結(jié)果,像下面這樣:

C++語言的單元測試與代碼覆蓋率

從這個(gè)報(bào)告的首頁,我們已經(jīng)可以看到代碼的語句覆蓋率(Lines),函數(shù)覆蓋率(Functions)以及分支覆蓋率(Branches)。而對(duì)于條件覆蓋率可以從詳細(xì)頁面中看到。如下圖所示:

C++語言的單元測試與代碼覆蓋率

在上面這張圖中,我們可以看到哪些代碼被覆蓋了,哪些沒有。而對(duì)于對(duì)于if-else之類的語句,也能很清楚的看到條件覆蓋率的覆蓋情況。例如,對(duì)于代碼的27行,只覆蓋了if成立時(shí)的情況,沒有覆蓋if不成立時(shí)的情況。

更進(jìn)一步

本文中,我們已經(jīng)完整的完成了從編寫單元測試到覆蓋率生成的整個(gè)過程。

但實(shí)際上,對(duì)于這項(xiàng)工作我們還可以做得更多一些。例如下面這兩項(xiàng)工作:

使用Google Mock

Google Mock是Google Test的擴(kuò)展,用于編寫和使用C++ Mock類。

在面向?qū)ο蟮木幊讨校琈ock對(duì)象是模擬對(duì)象,它們以預(yù)先設(shè)定的方式模仿真實(shí)對(duì)象的行為。程序員通常會(huì)創(chuàng)建一個(gè)Mock對(duì)象來測試某個(gè)其他對(duì)象的行為,這與汽車設(shè)計(jì)師使用碰撞測試假人來模擬人類在車輛碰撞中的動(dòng)態(tài)行為的方式非常相似。

關(guān)于Google Mock的更多內(nèi)容請(qǐng)參見:Google Mock的文檔。

持續(xù)集成

對(duì)于演示項(xiàng)目的覆蓋率報(bào)告是通過手動(dòng)執(zhí)行腳本文件生成的。

而在實(shí)際的項(xiàng)目中,可能同時(shí)有很多人在開發(fā)同一個(gè)項(xiàng)目,每一天項(xiàng)目中都會(huì)有很多次的代碼提交。我們不可能每次手動(dòng)的執(zhí)行編譯和生成覆蓋率報(bào)告結(jié)果。這時(shí)就可以借助一些持續(xù)集成的工具,定時(shí)自動(dòng)地完成項(xiàng)目的編譯,測試和覆蓋率報(bào)告結(jié)果的生成工作。

可以在持續(xù)集成工具中包含我們編寫的腳本,然后將覆蓋率報(bào)告的html結(jié)果發(fā)布到某個(gè)Web服務(wù)器上,最后再以郵件的形式將鏈接地址發(fā)送給大家。

這樣就可以很方便的讓整個(gè)團(tuán)隊(duì)看到所有模塊的測試結(jié)果和覆蓋率情況了。

完成了一整套這樣的工作,可以非常好的提升整個(gè)項(xiàng)目的質(zhì)量。

轉(zhuǎn)載請(qǐng)注明來自夕逆IT,本文標(biāo)題:《c語言double(C語言的單元測試與代碼覆蓋率)》

每一天,每一秒,你所做的決定都會(huì)改變你的人生!

發(fā)表評(píng)論

快捷回復(fù):

評(píng)論列表 (暫無評(píng)論,12人圍觀)參與討論

還沒有評(píng)論,來說兩句吧...