<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          軟件工程師必備的技能 | 單元測試

          共 4413字,需瀏覽 9分鐘

           ·

          2021-11-04 14:43

          關(guān)注、星標(biāo)公眾號,直達(dá)精彩內(nèi)容

          源:保羅的酒吧

          前言

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

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

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

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

          在工具上,我們會使用下面這些:

          • GCC
          • CMake
          • Google Test
          • gcov
          • lcov

          演示項目

          為了方便本文的講解,我專門編寫了一個演示項目作為代碼示例。

          演示項目的源碼可以在我的Github上獲取:paulQuei/gtest-and-coverage[1]

          你可以通過下面幾條命令下載和運行這個項目:

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

          要運行這個項目,你的機器上必須先安裝好前面提到的工具。如果沒有,請閱讀下文以了解如何安裝它們。

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

          /usr/bin/ruby?-e?"$(curl?-fsSL?https://raw.githubusercontent.com/Homebrew/install/master/install)"

          項目結(jié)構(gòu)

          演示項目的目錄結(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)容是:以測試一個我們要提供的軟件庫為例,講解如何對其進(jìn)行單元測試并生成測試報告。

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

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

          演示項目中的文件說明如下:

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

          測試環(huán)境

          演示項目在如下的環(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.1
            • Google Test: 1.8.1
            • lcov: lcov version 1.13
          • Ubuntu
            • 操作系統(tǒng):Ubuntu 16.04.5 LTS
            • 編譯器:gcc (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609
            • CMake:cmake version 3.5.1
            • Google Test:1.8.1
            • lcov:lcov version 1.12

          關(guān)于CMake

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

          關(guān)于如何安裝CMake請參見這里:Installing CMake[4]

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

          • Mac系統(tǒng):
          brew?install?cmake
          • Ubuntu系統(tǒng)
          sudo?apt?install?cmake

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

          這里僅僅對演示項目中用到的內(nèi)容做一下說明。演示項目中的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)?⑧

          以編號為序,這段代碼說明如下:

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

          關(guān)于測試

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

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

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

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

          關(guān)于xUnit

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

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

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

          Google Test介紹

          Google Test的項目主頁在Github上:Github: Google Test[8]

          實際上,這個項目中同時包含了GoogleTest和GoogleMock兩個工具,本文中我們只會講解第一個。

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

          • Linux
          • Mac OS X
          • Windows
          • Cygwin
          • MinGW
          • Windows Mobile
          • Symbian

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

          • Chromium projects[9]
          • LLVM[10]
          • Protocol Buffers[11]
          • OpenCV[12]
          • tiny-dnn[13]

          編譯Google Test

          關(guān)于如何編譯Google Test請參見這里:Generic Build Instructions[14]

          為了便于讀者使用,我們在演示項目中包含了Google Test 1.8.1的源碼壓縮包。并且在CMake文件中,同時包含了Google Test的編譯和使用配置工作。

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

          使用Google Test

          演示項目代碼說明

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

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

          演示項目中的軟件庫包含一個頭文件和一個實現(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

          這個頭文件說明如下:

          • 頭文件包含了三個函數(shù),前兩個用來做intdouble類型的四則運算。最后一個判斷輸入的年份是否是閏年。

          • 當(dāng)然,在實際的工程中,前兩個函數(shù)合并實現(xiàn)為一個泛型函數(shù)更為合適。但這里之所以分成兩個,是為了查看代碼覆蓋率所用。

          • 關(guān)于

            閏年

            說明如下:

            • 能被4整除但不能被100整除的年份為普通閏年。
            • 能被100整除,也同時能被400整除的為世紀(jì)閏年。
            • 其他都不是閏年。
            • 例如:1997年不是閏年,2000年是閏年,2016年是閏年,2100不是閏年。

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

          //?utility.cpp

          #include?"utility.h"

          #include?
          #include?

          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"?<????????????return?std::numeric_limits::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"?<????????????return?std::numeric_limits::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;
          }

          開始測試

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

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

          1. 創(chuàng)建一個測試用的cpp文件
          2. 為上面這個測試用的cpp文件編寫Makefile(或者CMake文件)。同時鏈接:
            • 待測試的軟件庫
            • gtest
            • gtest_main
            • pthread庫(Google Test使用了這個庫所以需要)
          3. 編寫測試代碼,編譯并運行測試的可執(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);
          }

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

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

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

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

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

          • 1 + 1 = 2
          • 2 - 1 = 1
          • 3 x 3 = 9
          • 10 / 2 = 5
          • 10 / 0 > 999999999

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

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

          測試判斷

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

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

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

          布爾判斷

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

          二進(jìn)制判斷

          FatalNonfatal說明
          ASSERT_EQ(expected, actual)EXPECT_EQ(expected, actual)判斷兩個數(shù)值相等
          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

          說明:

          • EQ:EQual
          • NE:Not Equal
          • LT:Less Than
          • LE:Less Equal
          • GT:Greater Than
          • GE:Greater Equal

          字符串判斷

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

          浮點數(shù)判斷

          FatalNonfatal說明
          ASSERT_FLOAT_EQ(exp, act)EXPECT_FLOAT_EQ(exp, act)兩個float數(shù)值相等
          ASSERT_DOUBLE_EQ(exp, act)EXPECT_DOUBLE_EQ(exp, act)兩個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ù)。例如,我們的演示項目中,每條case都會需要創(chuàng)建Utility對象。

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

          要使用Test Fixture我們需要創(chuàng)建一個類繼承自Google Test中的::testing::Test

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

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

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

          這兩項重復(fù)性的工作可以由::testing::Test類中的SetupTearDown兩個函數(shù)來完成。

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

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

          class?UtilityTest?:?public?::testing::Test?{

          protected:

          void?SetUp()?override?{
          ????cout?<"SetUp?runs?before?each?case."?<}

          void?TearDown()?override?{
          ????cout?<"TearDown?runs?after?each?case."?<}

          Utility?util;

          };

          這段代碼說明如下:

          1. SetupTearDown兩個函數(shù)標(biāo)記了override以確認(rèn)是重寫父類中的方法,這是C++11新增的語法。
          2. 我們的Utility類是無狀態(tài)的,因此SetupTearDown兩個函數(shù)中我們僅僅打印日志以便確認(rèn)。
          3. Utility util設(shè)置為protected以便測試代碼中可以訪問。(從實現(xiàn)上來說,測試case的代碼是從這個類繼承的子類,當(dāng)然,這個關(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));
          }

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

          運行測試

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

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


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


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

          像下面這樣:

          $?./build/unit_test?--gtest_filter=*ArithmeticCalculationInt
          Running?main()?from?googletest/src/gtest_main.cc
          Note:?Google?Test?filter?=?*ArithmeticCalculationInt
          [==========]?Running?1?test?from?1?test?case.
          [----------]?Global?test?environment?set-up.
          [----------]?1?test?from?TestCalculationInt
          [?RUN??????]?TestCalculationInt.ArithmeticCalculationInt
          CANNO?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)容。請讀者下載演示項目之后完成下面這些操作:

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

          代碼覆蓋率

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

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

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

          覆蓋率的類型

          先來看一下,當(dāng)我們在說“覆蓋率”的時候我們到底是指的什么。

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

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

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

          語句覆蓋率是我們最常用的。因為它很直觀的對應(yīng)到我們寫的每一行代碼。

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

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

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

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

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

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

          1. a = true, b = true
          2. a = true, b = false
          3. a = false, b = true
          4. a = false, b = false

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

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

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

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

          gcov

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

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

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

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

          使用

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

          這段代碼如下:

          //?test.c

          #include?

          int?main?(void)?{

          ??for?(int?i?=?1;?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;
          }

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

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

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

          gcc?--coverage?test.c

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

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

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

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

          關(guān)于這兩個文件的說明請參見這里:Brief description of gcov data files[17]

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

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

          $?gcc?--coverage?test.c
          $?ll
          total?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?3
          6?is?divisible?by?3
          9?is?divisible?by?3
          $?ll
          total?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?test
          File?'test.c'
          Lines?executed:85.71%?of?7
          test.c:creating?'test.c.gcov'

          $?ll
          total?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?
          ????????-:????4:
          ????????-:????5:int?main?(void)?{
          ????????-:????6:
          ???????20:????7:??for?(int?i?=?1;?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:}

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

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

          lcov

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

          此時就可以使用lcov[19]了。

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

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

          這個鏈接是lcov生成的報告樣例:lcov - code coverage report[20]

          安裝

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

          • Mac系統(tǒng)
          brew?install?lcov
          • Ubuntu系統(tǒng)
          sudo?apt?install?lcov

          使用

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

          lcov?--help

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

          這里主要關(guān)注的下面這幾個參數(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)不會打開分支覆蓋率,因此我們還需要增加這個參數(shù)來打開分支覆蓋率的計算:--rc lcov_branch_coverage=1
          • lcov輸出的仍然是一個中間產(chǎn)物,我們還需要通過lcov軟件包提供的另外一個命令genhtml來生成最終需要的html格式的覆蓋率報告文件。同樣的,為了打開分支覆蓋率的計算,我們也要為這個命令增加--rc lcov_branch_coverage=1參數(shù)

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

          COVERAGE_FILE=coverage.info
          REPORT_FOLDER=coverage_report
          lcov?--rc?lcov_branch_coverage=1?-c?-d?build?-o?${COVERAGE_FILE}_tmp
          lcov?--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文件中。但是這里面會包含非項目源碼的覆蓋率(例如google test),所以我們又通過另外一條命令來指定”src”文件夾進(jìn)行過濾。最后,通過genhtml得到html格式的報告。

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


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


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

          更進(jìn)一步

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

          但實際上,對于這項工作我們還可以做得更多一些。例如下面這兩項工作:

          使用Google Mock

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

          在面向?qū)ο蟮木幊讨校?span style="color: #1e6bb8;font-weight: bold;">Mock對象[21]是模擬對象,它們以預(yù)先設(shè)定的方式模仿真實對象的行為。程序員通常會創(chuàng)建一個Mock對象來測試某個其他對象的行為,這與汽車設(shè)計師使用碰撞測試假人來模擬人類在車輛碰撞中的動態(tài)行為的方式非常相似。

          關(guān)于Google Mock的更多內(nèi)容請參見:Google Mock的文檔[22]

          持續(xù)集成

          對于演示項目的覆蓋率報告是通過手動執(zhí)行腳本文件生成的。

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

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

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

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

          參考文獻(xiàn)與推薦讀物

          • CMake Cookbook
          • Google Test
          • googletest Generic Build Instructions
          • Googletest Primer
          • A quick introduction to the Google C++ Testing Framework
          • Google Test Quick Reference
          • Coverage testing with gcov
          • Generating Code Coverage with Qt 5 and GCOV on Mac OS
          • Wikipedia: xUnit
          • Wikipedia: Code Coverage
          • lcov - the LTP GCOV extension
          • gcov—a Test Coverage Program

          參考資料

          [1]

          paulQuei/gtest-and-coverage: https://github.com/paulQuei/gtest-and-coverage

          [2]

          brew: https://brew.sh/

          [3]

          https://cmake.org: https://cmake.org/

          [4]

          Installing CMake: https://cmake.org/install/

          [5]

          Program Instrumentation Options: https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html#Instrumentation-Options

          [6]

          白盒測試: https://en.wikipedia.org/wiki/White-box_testing

          [7]

          Kent Beck: https://en.wikipedia.org/wiki/Kent_Beck

          [8]

          Github: Google Test: https://github.com/google/googletest

          [9]

          Chromium projects: http://www.chromium.org/

          [10]

          LLVM: http://llvm.org/

          [11]

          Protocol Buffers: https://github.com/google/protobuf

          [12]

          OpenCV: http://opencv.org/

          [13]

          tiny-dnn: https://github.com/tiny-dnn/tiny-dnn

          [14]

          Generic Build Instructions: https://github.com/google/googletest/tree/master/googletest

          [15]

          paulQuei/gtest-and-coverage: https://github.com/paulQuei/gtest-and-coverage

          [16]

          gcov: https://gcc.gnu.org/onlinedocs/gcc/gcov.html

          [17]

          Brief description of gcov data files: https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/gcov-Data-Files.html

          [18]

          gcov: https://gcc.gnu.org/onlinedocs/gcc/gcov.html

          [19]

          lcov: http://ltp.sourceforge.net/coverage/lcov.php

          [20]

          lcov - code coverage report: http://ltp.sourceforge.net/coverage/lcov/output/index.html

          [21]

          Mock對象: https://en.wikipedia.org/wiki/Mock_object

          [22]

          Google Mock的文檔: https://github.com/google/googletest/blob/master/googlemock/README.md

          [23]

          持續(xù)集成: https://en.wikipedia.org/wiki/Continuous_integration

          [24]

          《C++語言的單元測試與代碼覆蓋率》: https://paul.pub/gtest-and-coverage/

          作者:保羅的酒吧

          https://paul.pub/gtest-and-coverage/


          版權(quán)歸原作者所有,如有侵權(quán),請聯(lián)系刪除。
          ???????????????? ?END ?????????????????

          關(guān)注我的微信公眾號,回復(fù)“加群”按規(guī)則加入技術(shù)交流群。

          歡迎關(guān)注我的視頻號:


          點擊“閱讀原文”查看更多分享,歡迎點分享、收藏、點贊、在看。

          瀏覽 40
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产黄片在线播放 | 国产精品久久久久久久久久两年半 | 人人塞人人艹 | 亚洲1v| www.大香蕉日日撸 |