Reach

Grow

Manage

Automate

Reach

Grow

Manage

Automate

开始使用 CppUTest

2017年5月14日

电子邮件

1 min read

开始使用 CppUTest

2017年5月14日

电子邮件

1 min read

开始使用 CppUTest

在SparkPost,我们投入大量时间和精力来测试我们的代码。我们的平台使用C语言编写,最近我研究了与一个名为“CppUTest”的单元测试框架的集成,该框架为C/C++提供了xUnit风格的测试。这个框架稳健、功能丰富,并且在积极开发中,是一个很好的选择。它还提供了C集成层,使其能够轻松与我们的平台C代码配合使用,尽管这个框架的大部分内容是C++。本教程介绍如何在自己的项目中开始使用CppUTest。

在 SparkPost,我们投入了大量时间和精力来测试我们的代码。我们的平台是用 C 语言编写的,最近我研究了如何与一个名为 “CppUTest” 的单元测试框架集成,该框架为 C/C++ 提供 xUnit 风格的测试。这个框架功能强大、功能丰富,并且在积极开发中,因此是一个很好的选择。它还提供了一个 C 集成层,使得即使框架的大部分是 C++,也能轻松与我们的平台 C 代码一起使用。本教程涵盖了如何在您的项目中开始使用 CppUTest。

正在下载 CppUTest

CppUTest项目页面可在此处找到,存储库在github上。它还包含在许多Linux发行版的软件包管理库中,以及Mac OS上的homebrew。以下示例是在Mac OS X上执行的,但它们源自为我们的平台运行的Red Hat操作系统编写的代码。

CppUTest的基本知识在其主页上有详细记录。我们将快速浏览一下这些内容,然后进入一些更有趣的功能。

奠定基础

首先,让我们写一些代码!

我们的测试项目将有一个‘main’文件,并将包含一个名为‘code’的实用程序库。该库将提供一个简单的函数,返回1(目前如此)。文件布局如下:

├── src │ ├── code │ │ ├── code.cpp │ │ └── code.h │ └── main.cpp └── t ├── main.cpp └── test.cpp

让我们从编写 src/ 文件开始

// src/main.cpp #include <stdlib.h> #include <stdio.h> #include "code.h" int main(void) { test_func(); printf("hello world!\n"); exit(0); }

// src/code/code.cpp #include <stdlib.h> #include "code.h" int test_func () { return 1; }

// src/code/code.h #ifndef __code_h__ #define __code_h__ int test_func (); #endif

现在,让我们进行测试,这些测试将位于 t/ 目录中。首先要做的是设置一个测试运行器,它将运行我们的测试文件。这也是‘main’函数,它将在所有编译后执行:

// t/main.cpp #include "CppUTest/CommandLineTestRunner.h" int main(int ac, char** av) { return CommandLineTestRunner::RunAllTests(ac, av); }

现在我们可以编写第一个测试模块:

// t/test.cpp #include "CppUTest/TestHarness.h" #include "code.h" TEST_GROUP(AwesomeExamples) { }; TEST(AwesomeExamples, FirstExample) { int x = test_func(); CHECK_EQUAL(1, x); }

接下来,我们需要编写makefiles。我们需要两个:一个用于 src/ 下的项目文件,另一个用于测试。

首先,让我们写一些代码!

我们的测试项目将有一个‘main’文件,并将包含一个名为‘code’的实用程序库。该库将提供一个简单的函数,返回1(目前如此)。文件布局如下:

├── src │ ├── code │ │ ├── code.cpp │ │ └── code.h │ └── main.cpp └── t ├── main.cpp └── test.cpp

让我们从编写 src/ 文件开始

// src/main.cpp #include <stdlib.h> #include <stdio.h> #include "code.h" int main(void) { test_func(); printf("hello world!\n"); exit(0); }

// src/code/code.cpp #include <stdlib.h> #include "code.h" int test_func () { return 1; }

// src/code/code.h #ifndef __code_h__ #define __code_h__ int test_func (); #endif

现在,让我们进行测试,这些测试将位于 t/ 目录中。首先要做的是设置一个测试运行器,它将运行我们的测试文件。这也是‘main’函数,它将在所有编译后执行:

// t/main.cpp #include "CppUTest/CommandLineTestRunner.h" int main(int ac, char** av) { return CommandLineTestRunner::RunAllTests(ac, av); }

现在我们可以编写第一个测试模块:

// t/test.cpp #include "CppUTest/TestHarness.h" #include "code.h" TEST_GROUP(AwesomeExamples) { }; TEST(AwesomeExamples, FirstExample) { int x = test_func(); CHECK_EQUAL(1, x); }

接下来,我们需要编写makefiles。我们需要两个:一个用于 src/ 下的项目文件,另一个用于测试。

首先,让我们写一些代码!

我们的测试项目将有一个‘main’文件,并将包含一个名为‘code’的实用程序库。该库将提供一个简单的函数,返回1(目前如此)。文件布局如下:

├── src │ ├── code │ │ ├── code.cpp │ │ └── code.h │ └── main.cpp └── t ├── main.cpp └── test.cpp

让我们从编写 src/ 文件开始

// src/main.cpp #include <stdlib.h> #include <stdio.h> #include "code.h" int main(void) { test_func(); printf("hello world!\n"); exit(0); }

// src/code/code.cpp #include <stdlib.h> #include "code.h" int test_func () { return 1; }

// src/code/code.h #ifndef __code_h__ #define __code_h__ int test_func (); #endif

现在,让我们进行测试,这些测试将位于 t/ 目录中。首先要做的是设置一个测试运行器,它将运行我们的测试文件。这也是‘main’函数,它将在所有编译后执行:

// t/main.cpp #include "CppUTest/CommandLineTestRunner.h" int main(int ac, char** av) { return CommandLineTestRunner::RunAllTests(ac, av); }

现在我们可以编写第一个测试模块:

// t/test.cpp #include "CppUTest/TestHarness.h" #include "code.h" TEST_GROUP(AwesomeExamples) { }; TEST(AwesomeExamples, FirstExample) { int x = test_func(); CHECK_EQUAL(1, x); }

接下来,我们需要编写makefiles。我们需要两个:一个用于 src/ 下的项目文件,另一个用于测试。

Project Makefile

项目的 makefile 将位于项目根目录的 ‘src’ 和 ‘t’ 目录的同一级别。它应如下所示:

# Makefile SRC_DIR=./src CODE_DIR=$(SRC_DIR)/code OUT=example TEST_DIR=t test: make -C $(TEST_DIR) test_clean: make -C $(TEST_DIR) clean code.o: gcc -c -I$(CODE_DIR) $(CODE_DIR)/code.cpp -o $(CODE_DIR)/code.o main: code.o gcc -I$(CODE_DIR) $(CODE_DIR)/code.o $(SRC_DIR)/main.cpp -o $(OUT) all: test main clean: test_clean rm $(SRC_DIR)/*.o $(CODE_DIR)/*.o $(OUT)

注意,这里为测试目标使用了‘make -C’——这意味着它将在测试目录中再次调用‘make’。

在这一点上,我们可以使用 makefile 编译‘src’代码,并验证其工作:

[]$ make main gcc -c -I./src/code ./src/code/code.cpp -o ./src/code/code.o gcc -I./src/code ./src/code/code.o ./src/main.cpp -o example []$ ./example 你好,世界!

Tests Makefile

对于测试,由于我们需要正确加载并与CppUTest库集成,因此事情稍微复杂一些。

CppUTest存储库提供了一个名为“MakefileWorker.mk”的文件。它提供了许多功能,使得使用CppUTest进行构建变得简单。此文件位于git存储库的“build”目录下。在本教程中,我们假设它已经被复制到‘t/’目录。可以如下使用:

# 我们不想使用相对路径,因此我们设置这些变量 PROJECT_DIR=/path/to/project SRC_DIR=$(PROJECT_DIR)/src TEST_DIR=$(PROJECT_DIR)/t # 指定源代码和包含位于何处 INCLUDE_DIRS=$(SRC_DIR)/code SRC_DIRS=$(SRC_DIR)/code # 指定位于何处的测试代码 TEST_SRC_DIRS = $(TEST_DIR) # 测试二进制文件的命名 TEST_TARGET=example # CppUTest库的位置 CPPUTEST_HOME=/usr/local # 使用此处定义的变量运行MakefileWorker.mk include MakefileWorker.mk

请注意,CPPUTEST_HOME必须设置为CppUTest安装的位置。如果您安装了发行版包,这通常位于linux/mac系统的/usr/local下。如果您自己查看了存储库,则是您签出的地方。

所有这些选项都在MakefileWorker.mk中有记录。

MakefileWorker.mk还增加了一些Makefile目标,包括以下内容:

  1. all – 构建Makefile指示的测试

  2. clean – 删除为测试生成的所有对象和gcov文件

  3. realclean – 删除整个目录树中的任何对象或gcov文件

  4. flags – 列出用于编译测试的所有配置标志

  5. debug – 列出所有源文件、对象、依赖项和‘要清理的内容’

代码覆盖率

单元测试如果没有覆盖率报告就不完整。对于使用gcc的项目,常用的工具是gcov,它是gcc标准工具套件的一部分。Cpputest与gcov集成简单,您只需在makefile中添加这一行:

CPPUTEST_USE_GCOV=Y

接下来,我们需要确保这个仓库中的filterGcov.sh脚本位于‘/scripts/filterGcov.sh’,相对于您设置的‘CPPUTEST_HOME’。它还需要具备执行权限。

在示例Makefile中,它将被部署到‘/usr/local/scripts/filterGcov.sh’。如果您从仓库签出运行CppUTest,所有功能应无需修改即可工作。




准备就绪后,您可以简单地运行‘make gcov’,分析将为您生成。在我们的例子中,我们需要‘make -B’以启用gcov重建目标文件:

[]$ make -B gcov < 编译输出 > 用于 d in /Users/ykuperman/code/blogpost/qa/src/code ; do \ FILES=`ls $d/*.c $d/*.cc $d/*.cpp 2> /dev/null` ; \ gcov --object-directory objs/$d $FILES >> gcov_output.txt 2>>gcov_error.txt ; \ done for f in ; do \ gcov --object-directory objs/$f $f >> gcov_output.txt 2>>gcov_error.txt ; \ done /usr/local/scripts/filterGcov.sh gcov_output.txt gcov_error.txt gcov_report.txt example.txt cat gcov_report.txt 100.00% /Users/ykuperman/code/blogpost/qa/src/code/code.cpp mkdir -p gcov mv *.gcov gcov mv gcov_* gcov 请查看gcov目录以获取详细信息

这将输出多个文件到一个新的‘gcov’目录。这些文件是:

  1. code.cpp.gcov – 被测试代码的实际‘gcov’文件

  2. gcov_error.txt – 错误报告(在我们的例子中,它应该是空的)

  3. gcov_output.txt – 实际运行的gcov命令的输出

  4. gcov_report.txt – 每个被测试文件的覆盖率摘要

  5. gcov_report.txt.html – gcov_report的html版本

Cpputest Memory Leak 检测

Cpputest 通过重新定义标准的“malloc/free”系列函数来使用其自身的包装器,从而自动检测内存泄漏。这使它能快速捕捉泄漏并在每次测试执行时报告。这在 MakefileWorker.mk 中默认启用,因此在前面的步骤中已经是启用状态。

为了说明这一点,让我们在 test_func() 中泄露一些内存!

回到 code.c,我们在函数中添加一个 malloc(),如下所示:

int test_func() { malloc(1); return 1; }

现在,重新编译后,会产生以下错误:

test.cpp:9: error: Failure in TEST(AwesomeExamples, FirstExample) 发现内存泄漏。分配编号(4)泄漏大小:1 分配于:./code.c 和行:6。类型:“malloc” 内存:<0x7fc9e94056d0> 内容:0000: 00 |.| 总泄漏次数:1

这显示了哪个测试导致泄漏、源码中泄漏发生的位置以及泄漏内存中的内容。非常有帮助!

此功能有几个注意事项:

  1. Cpputest 使用预处理器宏动态重新定义所有对标准内存管理函数的调用。 这意味着它只对正在测试的源代码中的调用有效,因为那些代码是使用 CppUTest 的覆盖代码进行编译的。 链接库中的泄漏将不会被捕获。

  2. 有时分配给整个进程生命周期的内存并不打算被释放。如果您正在测试一个具有此行为的模块,这可能会产生很多垃圾错误。 要禁用泄漏检测,您可以这样做:

CPPUTEST_USE_MEM_LEAK_DETECTION=N

感兴趣?

这只是冰山一角,这个工具包含的所有功能远不止于此。除了这里讨论的基础知识之外,它还具有一个mock框架、一个直接的C集成层和一个插件框架,仅举几个重要的功能。这个repo还包含一个完整的帮助手册目录,可以帮助自动化处理框架中的一些日常工作部分。

我希望这里的信息能帮助您使用这个出色的工具提高您的C/C++代码质量!

让我们为您联系Bird专家。
在30分钟内见证Bird的全部威力。

通过提交,您同意 Bird 可能会就我们的产品和服务与您联系。

您可以随时取消订阅。查看Bird的隐私声明以获取有关数据处理的详细信息。

Newsletter

通过每周更新到您的收件箱,随时了解 Bird 的最新动态。

让我们为您联系Bird专家。
在30分钟内见证Bird的全部威力。

通过提交,您同意 Bird 可能会就我们的产品和服务与您联系。

您可以随时取消订阅。查看Bird的隐私声明以获取有关数据处理的详细信息。

Newsletter

通过每周更新到您的收件箱,随时了解 Bird 的最新动态。

让我们为您联系Bird专家。
在30分钟内见证Bird的全部威力。

通过提交,您同意 Bird 可能会就我们的产品和服务与您联系。

您可以随时取消订阅。查看Bird的隐私声明以获取有关数据处理的详细信息。

R

Reach

G

Grow

M

Manage

A

Automate

Newsletter

通过每周更新到您的收件箱,随时了解 Bird 的最新动态。