【C/C++】GoogleTest单元测试与代码覆盖率 | 您所在的位置:网站首页 › excel计算覆盖率函数 › 【C/C++】GoogleTest单元测试与代码覆盖率 |
本博客转载自 知乎,仅作学习记录,侵删~ 文章目录 前言演示项目项目结构测试环境关于CMake关于测试关于xUnitGoogle Test介绍编译Google Test使用Google Test演示项目代码说明开始测试测试判断布尔判断二进制判断字符串判断浮点数判断异常判断EXPECT_DEATH、EXPECT_EXIT、EXPECT_THROW Test Fixture运行测试 代码覆盖率覆盖率的类型先来看一下,当我们在说“覆盖率”的时候我们到底是指的什么。gcov使用 lcov安装 使用 更进一步使用Google Mock持续集成 前言测试是软件开发过程中一个必须的环节,测试确保软件的质量符合预期。 对于工程师自己来说,单元测试也是提升自信心的一种方式。 直接交付没有经过测试的代码是不太好的,因为这很可能会浪费整个团队的时间,在一些原本早期就可以发现的问题上。而单元测试,就是发现问题一个很重要的环节。 本文以C++语言为基础,讲解如何进行单元测试并生成测试报告。 在工具上,我们会使用下面这些: GCCCMakeGoogle Testgcovlcov 演示项目为了方便本文的讲解,我专门编写了一个演示项目作为代码示例。 演示项目的源码可以在我的Github上获取:paulQuei/gtest-and-coverage[1]。 你可以通过下面几条命令下载和运行这个项目: git clone https://github.com/paulQuei/gtest-and-coverage.git cd gtest-and-coverage ./make_all.sh要运行这个项目,你的机器上必须先安装好前面提到的工具。如果没有,请阅读下文以了解如何安装它们。 如果你使用的是Mac系统,下文假设你的系统上已经安装了brew[2]包管理器。如果没有,请通过下面这条命令安装它: /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 项目结构演示项目的目录结构如下: . ├── CMakeLists.txt ├── googletest-release-1.8.1.zip ├── include │ └── utility.h ├── make_all.sh ├── src │ └── utility.cpp └── test └── unit_test.cpp这里演示的内容是:以测试一个我们要提供的软件库为例,讲解如何对其进行单元测试并生成测试报告。 为了简单起见,这个软件库只有一个头文件和一个实现文件。 当然,在实际上的项目中,一个软件库会通常包含更多的文件,不过这并不影响我们要说明的问题。 演示项目在如下的环境中测试过。 MacBook Pro 操作系统: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 操作系统: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 关于CMake为了简化编译的过程,这里使用CMake作为编译工具。关于CMake的更多内容请参见请官网:https://cmake.org[3]。 或其他CMake资料:CMake[4]。 另外,你也可以通过一条简单的命令来安装CMake: Mac系统: brew install cmakeUbuntu系统 sudo apt install cmake由于篇幅所限,这里不打算对CMake做过多讲解,读者可以访问其官网或者在网络上搜寻其使用方法。 这里仅仅对演示项目中用到的内容做一下说明。演示项目中的CMakeLists.txt内容如下: 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) ⑧以编号为序,这段代码说明如下: 设置使用的CMake最低版本号为2.8.11。指定项目的名称为”utility”,项目名称可以通过 ${CMAKE_PROJECT_NAME} 进行引用。指定使用C++11。这里的三行是编译google test,并将其头文件路径和编译结果的库文件路径添加到环境中。因为后面在编译单元测试代码的时候需要用到。添加 - -coverage 到编译器flag中,这个参数是很重要的,因为这是生成代码覆盖率所必须的。关于该编译参数的说明见这里:Program Instrumentation Options[5]。编译我们的软件库,这里将生成libutility_lib.a库文件。编译单元测试的可执行文件。单元测试的可执行文件需要链接我们开发的软件库以及google test的库。另外,google test依赖了pthread,所以这个库也需要。(待编出的可执行文件的依赖库) 关于测试软件测试有很多种分类方式。从测试的级别来说,可以大致分为: 单元测试集成测试系统测试这其中,单元测试是最局部和具体的。它通常需要对代码中的每一个类和函数进行测试。 单元测试通常由开发者完成,需要针对代码逻辑进行测试。所以它是一种白盒测试[6]。 关于xUnitxUnit是几种单元测试框架的总称。最早源于Smalltalk的单元测试框架SUnit,它是由Kent Beck[7]开发的。 除此之外,还有针对Java语言的JUnit,针对R语言的RUnit。 在本文中,我们使用Google开发的xUnit框架:Google Test。 Google Test介绍Google Test的项目主页在Github上:Google Test[8]。 实际上,这个项目中同时包含了GoogleTest和GoogleMock两个工具,本文中我们只会讲解第一个。 Google Test支持的操作系统包含下面这些: LinuxMac OS XWindowsCygwinMinGWWindows MobileSymbian目前有很多的项目都使用了Google Test,例如下面这些: Chromium projects[9]LLVM[10]Protocol Buffers[11]OpenCV[12]tiny-dnn[13]目前有很多的项目都使用了Google Test,例如下面这些: Chromium projects[9]LLVM[10]Protocol Buffers[11]OpenCV[12]tiny-dnn[13] 编译Google Test关于如何编译Google Test请参见这里:Generic Build Instructions[14]。 为了便于读者使用,我们在演示项目中包含了Google Test 1.8.1的源码压缩包。并且在CMake文件中,同时包含了Google Test的编译和使用配置工作。 如果使用演示项目,读者将不需要手动处理Google Test的编译和安装工作。 使用Google Test 演示项目代码说明为了便于下文说明,演示项目中包含了几个简单的函数。 可以从这里下载源码以便查看其中的内容:paulQuei/gtest-and-coverage。 演示项目中的软件库包含一个头文件和一个实现文件。头文件内容如下: // 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这个头文件说明如下: 头文件包含了三个函数,前两个用来做int和double类型的四则运算。最后一个判断输入的年份是否是闰年。当然,在实际的工程中,前两个函数合并实现为一个泛型函数更为合适。但这里之所以分成两个,是为了查看代码覆盖率所用。关于闰年说明如下: 能被4整除但不能被100整除的年份为普通闰年。能被100整除,也同时能被400整除的为世纪闰年。其他都不是闰年。例如:1997年不是闰年,2000年是闰年,2016年是闰年,2100不是闰年。 这三个函数的实现也不复杂: // 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 return a + b; } else if (op == MINUS) { return a - b; } else if (op == MULTIPLE) { return a * b; } else { if (b == 0) { cout return true; } if (year % 100 != 0 && year % 4 == 0) { return true; } return false; } 开始测试接下来我们就要对上面这些代码进行测试了。 要使用Google Test进行测试,整个过程也非常的简单。只要进行下面三步: 创建一个测试用的cpp文件 为上面这个测试用的cpp文件编写Makefile(或者CMake文件)。同时链接: - 待测试的软件库 - gtest库 - gtest_main库 - pthread库(Google Test使用了这个库所以需要)编写测试代码,编译并运行测试的可执行程序。 并且,测试代码写起来也非常的简单,像下面这样: #include "utility.h" #include "gtest/gtest.h" TEST(TestCaculationInt, ArithmeticCalculationInt){ Utility util; //格式:EXPECT_XX(函数调用(a,b,c),期望的函数输出结果); 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); }是的,就是这么简单的几行代码,就对整数四则运算的函数进行了测试。 TEST后面所包含的内容称之为一条case,通常我们会为每个函数创建一个独立的case来进行测试。一个测试文件中可以包含很多条case。同时,一条case中会包含很多的判断(例如EXPECT_EQ…)。注意:在做单元测试的时候,保证每条case是独立的,case之间没有前后依赖关系是非常重要的。 当然,测试代码中包含的判断的多少将影响测试结果的覆盖率。所以在编写每条case的时候,我们需要仔细思考待测试函数的可能性,有针对性的进行测试代码的编写。 这段代码应该很好理解,它分别进行了下面这些测试: 1 + 1 = 22 - 1 = 13 x 3 = 910 / 2 = 510 / 0 > 999999999你可能会发现,这段代码里面甚至没有main函数。它也依然可以生成一个可执行文件。这就是我们链接gtest_main所起的作用。 在实际的测试过程中,你想判断的情况可能不止上面这么简单。下面我们来看看Google Test还能做哪些测试。 测试判断Google Test 对于结果的判断,有两种形式: ASSERT_*:这类判断是Fatal的。一旦这个判断出错,则直接从测试函数中返回,不会再继续后面的测试。EXPECT_*:这类判断是Nonfatal的。它的效果是,如果某个判断出错,则输出一个错误信息,但是接下来仍然会继续执行后面的测试。可以进行的判断方法主要有下面这些: 布尔判断
做单元测试,一般是验证函数输出数据与预期是否相符。但是也有例外,我们可能需要验证函数在接收到非法参数时是否会断言或抛异常,例如下面这个例子: int foo(int *p) { assert(p != nullptr); return *p; }当传给foo的实参为nullptr时,运行的预期结果就是断言后进程异常退出。 这在单元测试中如何验证呢?如何不让这些验证异常值的用例干扰单元测试的工作流呢? googletest提供了三组宏分别用于测试导致进程崩溃、导致进程退出、抛异常的函数,他们分别是EXPECT_DEATH、EXPECT_EXIT、EXPECT_THROW及其各自的ASSERT版本。 使用这些宏测试相应的函数不会干扰单元测试的工作流,换言之,函数断言并不会导致单元测试进程异常退出。而如果使用一般的宏,例如EXPECT_EQ,来验证传入异常值时函数是否会断言时,单元测试进程也会异常退出,无法执行后续的测试用例。 以使用EXPECT_DEATH宏测试foo函数为例,测试用例需要这么写: TEST(foo, death) { EXPECT_DEATH(foo(nullptr), ".*"); }EXPECT_DEATH,接受两个参数,分别是要测试的语句和要匹配的错误信息的正则表达式,这里我们并不关心实际的错误信息,所以直接传入".*"匹配任意信息。如果错误信息匹配不上的话,这个测试用例还是会被认为失败。 EXPECT_DEATH的基本原理就是在执行测试语句前fork一个进程出来执行,这样这个进程的死活就不会干扰单元测试进程本体了。 宏的手册以及更详细的原理,请查看googletest官方文档。 Test Fixture在某些情况下,我们可能希望多条测试case使用相同的测试数据。例如,我们的演示项目中,每条case都会需要创建Utility对象。 有些时候,我们要测试的对象可能很大,或者创建的过程非常的慢。这时,如果每条case反复创建这个对象就显得浪费资源和时间了。此时,我们可以使用Test Fixture来共享测试的对象。 要使用Test Fixture我们需要创建一个类 继承自Google Test中的 ::testing::Test 。 还记得我们前面说过,我们要尽可能的保证每条测试case是互相独立的。但是,当我们在多条case之间共享有状态的对象时,就可能出现问题。 例如,我们要测试的是一个队列数据结构。有的case会向队列中添加数据,有的case会从队列中删除数据。case执行的顺序不同,则会导致Queue中的数据不一样,这就可能会影响case的结果。 为了保证每条case是独立的,我们可以在每条case的执行前后分别完成准备工作和清理工作,例如,准备工作是向队列中添加三个数据,而清理工作是将队列置空。 这两项重复性的工作可以由 ::testing::Test类中的 Setup 和 TearDown 两个函数来完成。 我们演示用的Utility类是无状态的,所以不存在这个问题。因此,这里我们仅仅在Setup和TearDown两个函数中打印了一句日志。 使用Test Fixture后,我们的代码如下所示: class UtilityTest : public ::testing::test{ protected: void SetUp() override{ cout ... test body ... }这里的TestCaseName必须是Test Fixture的类名。 所以我们的测试代码写起来是这样: TEST_F(UtilityTest, ArithmeticCalculationDouble){ EXPECT_EQ(util.ArithmeticCaculation(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方法故意只进行了一种情况的测试。这是为了最终生成代码覆盖率所用。 运行测试编写完单元测试之后,再执行编译工作便可以运行测试程序以查看测试结果了。 测试结果像下面这样: 像下面这样: $ ./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.如果想要更好的理解这些内容。请读者下载演示项目之后完成下面这些操作: 在utility.h和utility.cpp中添加一些新的函数。在新添加的函数中故意包含一个bug。为新添加的函数编写测试代码,并测试出函数中包含的bug。 代码覆盖率在进行单元测试之后,我们当然希望能够直观的看到我们的测试都覆盖了哪些代码。 理论上,如果我们能做到100%的覆盖我们的所有代码,则可以说我们的代码是没有Bug的。 但实际上,100%的覆盖率要比想象得困难。对于大型项目来说,能够达到80% ~ 90%的语句覆盖率就已经很不错了。 覆盖率的类型先来看一下,当我们在说“覆盖率”的时候我们到底是指的什么。实际上,代码覆盖率有下面几种类型: 函数覆盖率:描述有多少比例的函数经过了测试。语句覆盖率:描述有多少比例的语句经过了测试。分支覆盖率:描述有多少比例的分支(例如:if-else,case语句)经过了测试。条件覆盖率:描述有多少比例的可能性经过了测试。这其中,函数覆盖率最为简单,就不做说明了。 语句覆盖率是我们最常用的。因为它很直观的对应到我们写的每一行代码。 而分支覆盖率和条件覆盖率可能不太好理解,需要做一下说明。 以下面这个C语言函数为例: int foo (int x, int y) { int z = 0; if ((x > 0) && (y > 0)) { z = x; } return z; }这个函数中包含了一个if语句,因此if语句成立或者不成立构成了两个分支。所以如果只测试了if成立或者不成立的其中之一,其分支覆盖率只有 1/2 = 50%。 而条件覆盖率需要考虑每种可能性的情况。 对于if (a && b)这样的语句,其一共有四种可能的情况: a = true, b = truea = true, b = falsea = false, b = truea = false, b = false思考一下:对于三层if嵌套,每个if语句包含三个布尔变量的代码,如果要做到100%的条件覆盖率,一共要测试多少种情况。 很显示,在编写代码的时候,尽可能的减少代码嵌套,并且简化逻辑运算是一项很好的习惯。 便于测试的代码也是便于理解和维护的,反之则反。 有了这些概念之后,我们就可以看懂测试报告中的覆盖率了。 gcovgcov是由GCC工具链提供的代码覆盖率生成工具。它可以很方便的和GCC编译器配合使用。 通常情况下,安装好GCC工具链,也就同时包含了gcov命令行工具。 对于代码覆盖率工具所做的工作,可以简单的理解为:标记一次运行过程中,哪些代码被执行过,哪些没有执行。 因此,即便没有测试代码,直接运行编译产物也可以得到代码的覆盖率。只不过,通常情况下这样得到的覆盖率较低罢了。 使用这里我们以另外一个简单的代码示例来说明gcov的使用。 这段代码如下: // test.c #include int main (void) { for (int i = 1; i -: 6: 20: 7: for (int i = 1; i |
CopyRight 2018-2019 实验室设备网 版权所有 |