框架概述

Google Test(也称为 googletest)是由 Google 开发的 C++ 单元测试框架。它的首个版本是在2004年发布的,作为 Google 内部的测试框架使用。随后,Google Test 在开源社区中得到广泛应用,并在许多项目和组织中成为首选的 C++ 单元测试框架。

Google Test 提供了丰富的断言函数和测试宏,使开发人员能够编写清晰、易读、易维护的单元测试。它支持测试夹具、参数化测试、测试套件等功能,可以满足各种测试需求。

随着时间的推移,Google Test 持续改进和更新,添加了许多新功能和改进。它的代码库托管在 GitHub 上,并由社区进行维护和更新。

在2017年,Google Test 的最新版本是 1.8.1。然而,Google Test 框架的开发并没有止步于此,后续版本的开发和更新由广大的开发者社区共同推动,以满足不断变化的测试需求。

Google Test 的成功和流行,得益于其简单易用、灵活可扩展的特性,以及对高质量代码和单元测试的推崇。它已成为 C++ 社区中一个广泛采用的单元测试框架,为开发人员提供了强大的测试工具和实践。

googletest的官网地址:https://google.github.io/googletest/

googletest的开源地址:https://github.com/google/googletest

googletest的sample例子:https://github.com/google/googletest/tree/main/googletest/samples

bazel使用运行方式

在bazel中我们想要使用googletest,是非常简单的。

官网有一篇文章是直接说如何使用bazel引入googletest的:https://google.github.io/googletest/quickstart-bazel.html

在WORKSPACE中引入googletest

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
name = "com_google_googletest",
urls = ["https://github.com/google/googletest/archive/5ab508a01f9eb089207ee87fd547d290da39d015.zip"],
strip_prefix = "googletest-5ab508a01f9eb089207ee87fd547d290da39d015",
)

编写BUILD的cc_test

假设目标文件是:

# https://docs.bazel.build/versions/master/be/c-cpp.html#cc_library
cc_library(
name = "sample1",
srcs = ["sample1.cc"],
hdrs = ["sample1.h"],
) cc_test(
name = "sample1_unittest",
srcs = ["sample1_unittest.cc"],
deps = [
"@com_google_googletest//:gtest_main",
":sample1",
],
)

运行bazel test

bazel test --test_output=all media_cpp_demo/cpp_unit_test:sample1_unittest

这里会输出类似的信息:

INFO: From Testing //media_cpp_demo/cpp_unit_test:sample1_unittest:
==================== Test output for //media_cpp_demo/cpp_unit_test:sample1_unittest:
Running main() from gmock_main.cc
[==========] Running 6 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 3 tests from FactorialTest
[ RUN ] FactorialTest.Negative
[ OK ] FactorialTest.Negative (0 ms)
[ RUN ] FactorialTest.Zero
[ OK ] FactorialTest.Zero (0 ms)
[ RUN ] FactorialTest.Positive
media_cpp_demo/cpp_unit_test/sample1_unittest.cc:25: Failure
Expected equality of these values:
3
Factorial(3)
Which is: 6
[ FAILED ] FactorialTest.Positive (0 ms)
[----------] 3 tests from FactorialTest (0 ms total) [----------] 3 tests from IsPrimeTest
[ RUN ] IsPrimeTest.Negative
[ OK ] IsPrimeTest.Negative (0 ms)
[ RUN ] IsPrimeTest.Trivial
[ OK ] IsPrimeTest.Trivial (0 ms)
[ RUN ] IsPrimeTest.Positive
[ OK ] IsPrimeTest.Positive (0 ms)
[----------] 3 tests from IsPrimeTest (0 ms total) [----------] Global test environment tear-down
[==========] 6 tests from 2 test suites ran. (0 ms total)
[ PASSED ] 5 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] FactorialTest.Positive 1 FAILED TEST
================================================================================

其中很明确告诉我们哪些case运行了,哪些case成功/失败了。

具体写法

最好的写法参考就是googletest的git官网的sample例子。

googletest的sample例子:https://github.com/google/googletest/tree/main/googletest/samples

断言语句

在 Google Test 中,有多种断言函数可用于编写测试断言,包括 EXPECT_* 系列和 ASSERT_* 系列。这两个系列的断言函数在用法上非常相似,但在断言失败时的行为上略有不同。

下面列出了常用的断言函数示例:

  • EXPECT_EQASSERT_EQ: 验证两个值是否相等。
  • EXPECT_NEASSERT_NE: 验证两个值是否不相等。
  • EXPECT_TRUEASSERT_TRUE: 验证条件是否为真。
  • EXPECT_FALSEASSERT_FALSE: 验证条件是否为假。
  • EXPECT_LTASSERT_LT: 验证第一个值是否小于第二个值。
  • EXPECT_LEASSERT_LE: 验证第一个值是否小于等于第二个值。
  • EXPECT_GTASSERT_GT: 验证第一个值是否大于第二个值。
  • EXPECT_GEASSERT_GE: 验证第一个值是否大于等于第二个值。

这些断言函数在断言失败时的行为略有不同:

  • EXPECT_* 系列:如果断言失败,将会输出错误信息,但测试函数继续执行。
  • ASSERT_* 系列:如果断言失败,将会输出错误信息,并终止当前测试函数的执行。

以下是一个示例,演示了如何使用这些断言函数:

#include <gtest/gtest.h>

TEST(MyTestSuite, ExampleTest) {
int x = 5;
int y = 10; // 验证相等关系
EXPECT_EQ(x, 5);
ASSERT_NE(x, y); // 验证条件
EXPECT_TRUE(x > 0);
ASSERT_FALSE(y < 0); // 验证大小关系
EXPECT_LT(x, y);
ASSERT_GE(y, 10);
} int main(int argc, char* argv[]) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

在上述示例中,我们定义了一个测试用例 MyTestSuite.ExampleTest。在测试用例中,我们使用了不同的断言函数来验证条件和关系。根据断言函数的使用,你可以选择使用 EXPECT_*ASSERT_* 系列来满足测试需求。

当运行测试时,如果断言失败,Google Test 将会输出错误信息以指示具体的断言失败的位置和条件。

根据测试需求和个人偏好,你可以选择使用适当的断言函数来编写测试断言,并根据测试情况选择使用 EXPECT_*ASSERT_* 系列。

简单的测试用例

简单的测试用例就是使用宏Test

可以参考:https://github.com/google/googletest/blob/main/googletest/samples/sample1_unittest.cc

TEST(FactorialTest, Positive) {
EXPECT_EQ(1, Factorial(1));
EXPECT_EQ(2, Factorial(2));
EXPECT_EQ(6, Factorial(3));
EXPECT_EQ(40320, Factorial(8));
}

这里的 FactorialTest 是 测试夹具(TestSuite),而 Positive 是 测试用例(TestCase)。

最后输出的时候,会把相同的TestSuite放在一起展示。

多个测试用例共享数据

如果我们多个测试用例都有一些初始化操作操作呢?这里就需要用到宏 TEST_F 了。

可以参考:https://github.com/google/googletest/blob/main/googletest/samples/sample3_unittest.cc

TEST_F 是 Google Test 框架中的一个宏,用于定义基于测试夹具(Test Fixture)的测试用例。测试夹具提供了在多个测试用例之间共享的设置和状态。

使用 TEST_F 宏,你可以在测试夹具类中定义多个测试用例,并共享该类中的成员变量和函数。每个测试用例都会在运行之前执行夹具的设置(SetUp)函数,运行完毕后执行夹具的清理(TearDown)函数。

下面是一个使用 TEST_F 宏定义测试用例的示例代码:

#include <gtest/gtest.h>

// 测试夹具类
class MyTestFixture : public ::testing::Test {
protected:
void SetUp() override {
// 在每个测试用例之前执行的设置
} void TearDown() override {
// 在每个测试用例之后执行的清理
} // 夹具类中的成员变量和函数
int value;
}; // 使用 TEST_F 宏定义测试用例
TEST_F(MyTestFixture, TestCase1) {
// 使用夹具类中的成员变量和函数进行测试断言
value = 42;
EXPECT_EQ(value, 42);
} TEST_F(MyTestFixture, TestCase2) {
// ...
} // ... int main(int argc, char* argv[]) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

在上述示例中,我们创建了一个名为 MyTestFixture 的测试夹具类,继承自 ::testing::Test。在夹具类中,我们可以定义成员变量和函数,以及重写 SetUpTearDown 函数来设置和清理测试的共享状态。

然后,我们使用 TEST_F 宏定义测试用例。在每个测试用例中,我们可以使用夹具类中的成员变量和函数,并编写测试断言。

最后,我们初始化 Google Test 框架并运行所有的测试用例。

通过使用 TEST_F 宏,我们可以更方便地组织和管理基于测试夹具的测试用例,以确保它们共享相同的设置和状态,并提供更灵活的测试场景。

其实不难看出,简单的测试用例 TEST 其实是 TEST_F 的一个特例,TEST 更像是将 TEST_F 定义的 TestSuite 给后台隐式自动填补了。

自定义main函数

同样的,我们定义的测试用例中没有main函数,其实也是被框架隐式自动填补了。它的原型是:

int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

我们也是可以在单元测试的文件中写上这么一个函数。

什么时候可能需要自己写main函数呢?

如果你的所有TestSuite都有一个通用的数据初始化逻辑,那么只能写在这个main函数中了。

如何才能写好的单元测试?

官网 https://google.github.io/googletest/primer.html 有简要说一下如何写一个good test case,其实写好的单元测试和语言无关,以下的指导原则适用所有语言的单元测试。

编写良好的单元测试是确保代码正确性和可靠性的关键。下面是一些关于如何编写好的单元测试的指导原则:

  1. 明确测试目标:在编写单元测试之前,明确测试的目标是什么。了解要测试的代码单元的行为、输入和预期输出,以及可能的边界条件和异常情况。

  2. 单一责任原则:确保每个单元测试只关注一个特定的功能或行为。将测试用例分解为独立且可重复执行的单元,以便更容易定位和修复问题。

  3. 覆盖各种情况:编写测试用例时,确保覆盖不同的输入组合、边界条件和异常情况。测试应该验证代码在各种情况下的行为是否正确。

  4. 独立性和可重复性:每个测试用例应该是相互独立且可重复执行的。测试用例之间不应该有依赖关系,避免测试之间的相互影响。

  5. 名称清晰明确:给测试用例和断言起一个清晰明确的名称,以便能够准确描述测试的目的和预期结果。这样可以更容易理解测试的用途,并且在测试失败时更容易定位问题。

  6. 使用合适的断言:选择适当的断言函数来验证代码的预期行为。确保断言清晰明确,并且测试失败时能够提供有用的错误信息,以便快速定位问题。

  7. 辅助工具和框架:使用适当的单元测试框架和辅助工具,如 Google Test、Catch2、Mockito 等,来简化测试的编写和管理。这些工具可以提供丰富的断言和辅助函数,以及测试运行和报告生成等功能。

  8. 保持测试的可维护性:随着代码的演进和变化,及时更新和维护测试用例。确保测试与代码保持同步,并且仍然能够有效地验证代码的正确性。

  9. 考虑边界情况和异常处理:测试应该覆盖各种边界情况和异常处理,以验证代码在这些情况下的行为是否符合预期。包括边界值、异常输入、空值等。

  10. 定期运行测试套件:确保定期运行整个测试套件,以便及早发现潜在的问题。集成测试运行到持续集成(CI)系统中,以便自动执行测试,并及时获取测试结果。

编写好的单元测试可以提供高度的代码覆盖率和可靠性,帮助你捕捉问题、防止回归和提供代码改进的信心。遵循上述原则并积极进行测试是确保代码质量的重要实践。

如何才能生成测试覆盖率

googletest 本身并不提供直接的测试覆盖率功能,但你可以结合其他工具来生成测试覆盖率报告。

如果你是使用bazel,那么这个步骤就更简单了。

生成代码覆盖率数据

这个bazel有个bazel coverage 的代码,就能生成覆盖率。

首先,先将BUILD文件做下修改,增加下生成覆盖率对应的参数。

# https://docs.bazel.build/versions/master/be/c-cpp.html#cc_library
cc_library(
name = "sample1",
srcs = ["sample1.cc"],
hdrs = ["sample1.h"],
) cc_test(
name = "sample1_unittest",
srcs = ["sample1_unittest.cc"],
copts = ["-fprofile-arcs", "-ftest-coverage"],
linkopts = ["-fprofile-arcs"],
deps = [
"@com_google_googletest//:gtest_main",
":sample1",
],
)

其次,运行命令 bazel coverage media_cpp_demo/cpp_unit_test:sample1_unittest

输出展示覆盖率数据路径:

INFO: Build completed successfully, 25 total actions
//media_cpp_demo/cpp_unit_test:sample1_unittest PASSED in 0.3s
/root/.cache/bazel/_bazel_root/aa4e8447fb143c448ba118077e918987/execroot/__main__/bazel-out/k8-fastbuild/testlogs/media_cpp_demo/cpp_unit_test/sample1_unittest/coverage.dat

这里的coverage.dat就是我们生成的覆盖率数据。

生成代码覆盖率html

这一步就需要依赖一个genhtml 工具。下载安装就不说了。

直接上命令:

genhtml ./bazel-out/k8-fastbuild/testlogs/media_cpp_demo/cpp_unit_test/sample1_unittest/coverage.dat --output-directory ./cov

将coverage.dat 生成html,并且将html放到./cov目录下。

![image-20230710172120373](../../../Library/Application Support/typora-user-images/image-20230710172120373.png)

打开就能看到如下的测定报告了

可以进一步点击查看是哪些行被覆盖了。

总结

CPP的单元测试和其他语言的单测也没有什么非常特别的地方,概念,方法都一样。

相较于如何写单元测试,更难的是如何写好单元测试。那是另外一个话题了。

参考

一文掌握谷歌 C++ 单元测试框架 GoogleTest

用googletest写cpp单测的更多相关文章

  1. 【spock】单测竟然可以如此丝滑

    0. 为什么人人都讨厌写单测 在之前的关于swagger文章里提到过,程序员最讨厌的两件事,一件是别人不写文档,另一件就是自己写文档.这里如果把文档换成单元测试也同样成立. 每个开发人员都明白单元测试 ...

  2. 如何优雅地执行dubbo"单测"

    很多小伙伴所在的公司是基于Dubbo来构建技术栈的,日常开发中必不可少要写dubbo单测(单元测试),如果单测数据依赖已有的外部dubbo服务,一般是mock数据,如果数据比较复杂,其实mock数据也 ...

  3. 使用Groovy+Spock轻松写出更简洁的单测

    当无法避免做一件事时,那就让它变得更简单. 概述 单测是规范的软件开发流程中的必不可少的环节之一.再伟大的程序员也难以避免自己不犯错,不写出有BUG的程序.单测就是用来检测BUG的.Java阵营中,J ...

  4. 你真的会写单测吗?TDD初体验

    前言: 昨天读到了一篇文章,讲的是TDD,即Test-Driven Development,测试驱动开发.大体意思是,它要求在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过 ...

  5. 使用Java函数接口及lambda表达式隔离和模拟外部依赖更容易滴单测

    概述 单测是提升软件质量的有力手段.然而,由于编程语言上的支持不力,以及一些不好的编程习惯,导致编写单测很困难. 最容易理解最容易编写的单测,莫过于独立函数的单测.所谓独立函数,就是只依赖于传入的参数 ...

  6. 输入输出无依赖型函数的GroovySpock单测模板的自动生成工具(上)

    目标 在<使用Groovy+Spock轻松写出更简洁的单测> 一文中,讲解了如何使用 Groovy + Spock 写出简洁易懂的单测. 对于相对简单的无外部服务依赖型函数,通常可以使用 ...

  7. 一分钟了解ruby中的单测

    之前用gtest写过很多c++的单测case, 对gtest的强大和灵活印象深刻:最近需要用ruby写一个小工具, 接触了下ruby, 写了代码就要写单测啊(好的单测确实对代码的健壮性和正确性保证上太 ...

  8. Allure对单测结果以及robotframework结果的处理

    Allure对单测结果以及robotframework结果的处理 Allure只能针对pytest的单测结果生成相应的报告: 如果需要对unittest的测试框架结果进行展示,可以使用pytest执行 ...

  9. 【踩坑记录】单测中@PostConstruct多次执行

    问题复现: 单测中@PostConstruct修饰的方法被多次执行 原因: @PostConstruct在Spring中常用于在构造函数后初始化对象,执行顺序如下: 构造方法->成员变量注入-& ...

  10. 超详细!手把手教你用 JaCoCo 生成单测覆盖率报告!

    我们都知道 Spock 是一个单测框架,其特点是语法简明.但当我们使用 Spock 写了一堆单元测试之后,如何生成对应的单测覆盖率报告呢?一般来说,我们会使用两个插件来一起完成单测覆盖率报告的生成,分 ...

随机推荐

  1. bash shell 无法使用 perl 正则

    哈喽大家好,我是咸鱼.今天跟大家分享一个关于正则表达式的案例,希望能够对你有所帮助 案例现象 前几天有一个小伙伴在群里求助,说他这个 shell 脚本有问题,让大家帮忙看看   可以看到,这个脚本首先 ...

  2. graphhopper-ios 编译过程详解

    一.写在前面 GraphHopper 是一个快速且高效的路径规划引擎,它默认使用OpenStreetMap和GTFS数据, 也可以导入其他数据源.它可以用作java库或独立的web服务器,去计算两个或 ...

  3. 记一次python写爬虫爬取学校官网的文章

    有一位老师想要把官网上有关数字化的文章全部下载下来,于是找到我,使用python来达到目的 首先先查看了文章的网址 获取了网页的源代码发现一个问题,源代码里面没有url,这里的话就需要用到抓包了,因为 ...

  4. Java的final修饰符

    final 实例域 可以将实例域定义为 final.对于 final 域来说,构建对象时必须初始化 final 实例域,构造对象之后就不允许改变 final 实例域的值了.也就是说,必须确保在每一个构 ...

  5. 这可能是最全面的Redis面试八股文了

    Redis连环40问,绝对够全! Redis是什么? Redis(Remote Dictionary Server)是一个使用 C 语言编写的,高性能非关系型的键值对数据库.与传统数据库不同的是,Re ...

  6. 2023-02-19:请用go语言调用ffmepg,输出视频文件信息。

    2023-02-19:请用go语言调用ffmepg,输出视频文件信息. 答案2023-02-19: 用 github.com/moonfdd/ffmpeg-go 这个库. 代码参考ffmpeg5入门教 ...

  7. 2022-12-12:有n个城市,城市从0到n-1进行编号。小美最初住在k号城市中 在接下来的m天里,小美每天会收到一个任务 她可以选择完成当天的任务或者放弃该任务 第i天的任务需要在ci号城市完成,

    2022-12-12:有n个城市,城市从0到n-1进行编号.小美最初住在k号城市中 在接下来的m天里,小美每天会收到一个任务 她可以选择完成当天的任务或者放弃该任务 第i天的任务需要在ci号城市完成, ...

  8. 2021-12-22:回文子串。 给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。 回文字符串 是正着读和倒过来读一样的字符串。 子字符串 是字符串中的由连续字符组成的一个序列。

    2021-12-22:回文子串. 给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目. 回文字符串 是正着读和倒过来读一样的字符串. 子字符串 是字符串中的由连续字符组成的一个序列. ...

  9. 2021-06-28:最接近目标值的子序列和。给你一个整数数组 nums 和一个目标值 goal 。你需要从 nums 中选出一个子序列,使子序列元素总和最接近 goal 。也就是说,如果子序列元素和

    2021-06-28:最接近目标值的子序列和.给你一个整数数组 nums 和一个目标值 goal .你需要从 nums 中选出一个子序列,使子序列元素总和最接近 goal .也就是说,如果子序列元素和 ...

  10. docker安装es和kibana,单机模式

    操作系统:mac系统 1.安装es docker pull elasticsearch:7.14.0 docker run --name es -p 9200:9200 -p 9300:9300 -e ...