使用宏来作为C++单元测试开关

Posted elloop

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用宏来作为C++单元测试开关相关的知识,希望对你有一定的参考价值。


layout: post
title: “使用宏来作为C++单元测试开关”
category: c++
tags: [tools]
description: “”

#前言

本文介绍了如何使用宏来控制C++单元测试的开启和关闭,本文的单元测试工具指的是google的C++测试框架gtest。

gtest目前最新的稳定release版本是1.7,关于gtest的介绍请移步gtest的主页,它的编译安装和使用都很简单,你可以直接把gtest的源码copy到自己项目里来使用,也可以使用header+静态库的方式集成到项目中。入门的话建议从它源码仓库里自带的测试用例看起,模仿着这些sample写用例可以快速上手。本文不对具体使用方法做介绍。

当单元测试变多的时候遇到的麻烦

我对gtest使用比较多的特性是基本的TEST测试,典型的用法像下面的代码这样:

#include <iostream>
#include "gtest/gtest.h"

using namespace std;

TEST(TempTest, Test1)

    cout << "============== Test1 ================" << endl;

    int i = 4 * 5;
    EXPECT_EQ(20, i);           // 20 == i
    EXPECT_NE(21, i);           // 21 != i

    EXPECT_TRUE(20 == i);
    EXPECT_FALSE(21 == i);

    cout << i << endl;          // 输出这么多行是在模拟复杂测试的时候,测试用例的输出可能会很多。
    cout << i << endl;
    cout << i << endl;
    cout << i << endl;
    cout << i << endl;
    cout << i << endl;


<!--more-->

TEST(TempTest, Test2)

    cout << "============== Test2 ================" << endl;

    int i = 20 * 5;
    EXPECT_GT(101, i);      // 101 > i
    EXPECT_LT(99, i);       // 99 < i

    cout << i << endl;
    cout << i << endl;
    cout << i << endl;
    cout << i << endl;
    cout << i << endl;
    cout << i << endl;


TEST(TempTest, Test3)

    cout << "============== Test3 ================" << endl;

    int i = 2 * 5;
    EXPECT_GT(1, i);        // 1 > i ?  will not pass.

    cout << i << endl;
    cout << i << endl;
    cout << i << endl;
    cout << i << endl;
    cout << i << endl;
    cout << i << endl;


int main(int argc, char** argv) 

	testing::InitGoogleTest(&argc, argv);
	return RUN_ALL_TESTS();

测试代码中,我定义了一个测试用例,包含三个子测试Test123,其中输出那么多行的i是为了模拟测试用例可能的复杂输出,上面的测试结果如下:

Test1, Test2通过,Test3中1 > i的条件失败,导致其未通过。

遇到的麻烦

从结果中可以看到,仅仅三个子测试就已经可能有这么多输出了,当我添加更多的文件,每个文件里有更多的测试用例的时候麻烦就出现了,我要从好几页的输出结果中,找到某个特定的测试用例的输出可是要看好一会呢。

解决问题

解决这个麻烦,我的需求是:

每次运行程序时,只显示我要测试的用例,这样测试结果就一目了然了。

方案1:把其它测试删掉或注释掉?

不行,不能删除用例,因为好些用例是很有价值的,包括正在学习的类库用法什么的要保留下来以后复习。注释掉太麻烦了,写的用例越来越多,一个个注释很麻烦,以后想再运行某个用例的时候还有再解注释,麻烦。

方案2:修改gtest源码,给TEST加上附加参数来控制某个TEST要不要运行

不行,这样搞代价太大而且有些有力过猛的感觉,以后gtest升级的话会受影响。

方案3:弄一个全局开关,在每个TEST体内,根据开关状态判断,如果开关打开,那就直接返回,不输出。

bool gSkipAllTest = true;

// 禁止输出的用例
TEST(A, a)

    if (gSkipAllTest)
        return;

    // ......


// 需要输出的用例
TEST(N, n)

    // ......

在需要输出的用例中,不加条件判断,在测试完毕之后再加上条件。这样只有每次新加的测试会有输出。当想打开所有测试的时候,把全局的开关打开就行了。看起来不错,但是运行起来的时候会发现,虽然那些用例没有输出但是gtest还是会打印出那些测试用例的测试报告,即使它是个没有任何测试条件和输出的用例。

因此,要想不让其它不需要运行的测试干扰我的视线,最根本的办法应该是在编译层面来做控制,因为只要TEST()一旦被编译,这个测试用例就会在最终的输出中出现。

控制编译过程?那就要靠宏了,虽然它的名声不咋地。

最终方案:使用宏来控制编译过程,过滤掉不需要运行的TEST

思路:

1. 使用RUN_GTESTBEGIN_TEST来代替TEST来定义测试用例 (“通过简单地增加另外一个间接层就可以解决软件的任何问题”)

2. 使用RUN_GTEST定义的用例是在本次编译后,肯定会被执行的用例

3. 使用BEGIN_TEST定义的用例是可能被执行的用例,它取决于控制宏SKIP_ALL_TEST是否被定义,如果定义,那么BEGIN_TEST所定义的用例就被一个哑巴函数代替,即不执行;如果未定义SKIP_ALL_TEST,那么BEGIN_TEST等同于RUN_GTEST,就成为了一个将被执行的测试用例。

下面是这几个宏的定义(END_TEST仅仅是个花括号结束。)

实现


// p(x) 是一个模板函数,这样写你应该懂吧:cout<T> << x;

#define RUN_GTEST(__CASE__, __SUB_CASE__, __TAG__) TEST(__CASE__, __SUB_CASE__)  \\
    std::fill_n(std::ostream_iterator<std::string>(std::cout, ""), 10, #__TAG__); \\
    p(" "); \\
    p(#__CASE__); \\
    p(" ---> "); \\
    p(#__SUB_CASE__); \\
    p(" "); \\
    std::fill_n(std::ostream_iterator<std::string>(std::cout, ""), 10, #__TAG__); \\
    cr;

#define SKIP_ALL_TEST           // 全局开关

#ifdef  SKIP_ALL_TEST
#define BEGIN_TEST(__CASE__, __SUB_CASE__, __TAG__) void __CASE__##__SUB_CASE__()  return;         // 编译为一个哑巴函数
#else
#define BEGIN_TEST(__CASE__, __SUB_CASE__, __TAG__) RUN_GTEST(__CASE__, __SUB_CASE__, __TAG__)      // 编译为一个测试用例
#endif

#define END_TEST 

用法

BEGIN_TEST(A, h1, @);
// ...
END_TEST;


RUN_GTEST(Hello, h2, $#);
// ...
END_TEST;

输出

[----------] 1 test from Hello
[ RUN      ] Hello.h2
$#$#$#$#$#$#$#$#$#$# Hello ---> h2 $#$#$#$#$#$#$#$#$#$#
[       OK ] Hello.h2 (2 ms)
[----------] 1 test from Hello (2 ms total)

可以看到,只有使用RUN_GTEST定义的h2被执行了。在用例的函数体里我不需要自己写log了。RUN_GTEST会把测试用例名字给我打印出来。

解释

这里面代码最多的是RUN_GTEST,它复杂在多了一个附加的参数__TAG__, TEST只需要两个参数,RUN_GTESTBEGIN_TEST为什么需要三个?

第三个参数是log里面常用的标志,用来标识这个测试用例,有了这个__TAG__参数,我就可以灵活的标识一个测试用例,即使它混杂在若干用例的输出结果之中,通过这个`TAG``我也能一眼就看到它的输出。

如果不用TAG,那么每个用例开头,我都需要写一句输出语句:

TEST(A, a1)

    cout << "============= A ---> a1 ===================" << endl;
    // test body


TEST(A, a2)

    cout << "============= A ---> a2 ===================" << endl;
    // test body

使用RUN_GTEST和TAG就可以这样写:

RUN_GTEST(A, a1, =);

// test body

END_TEST;


RUN_GTEST(A, a2, @);

// test body

END_TEST;

第三个参数__TAG__是字符串"=“和”@",从RUN_GTEST的定义中可以看到,在刚一进入测试用例,__TAG__会被输出10次,然后是__CASE__ ---> __SUB_CASE__, 接着又是10次__TAG__字符串的输出。如下:

[----------] 2 tests from A
[ RUN      ] A.a1
========== A ---> a1 ==========
[       OK ] A.a1 (1 ms)
[ RUN      ] A.a2
@@@@@@@@@@ A ---> a2 @@@@@@@@@@
[       OK ] A.a2 (2 ms)
[----------] 2 tests from A (3 ms total)

如果不够明显,还可以使用更复杂的__TAG__:

RUN_GTEST(A, a, @@@$);

END_TEST;

// 输出:
[----------] 1 test from A
[ RUN      ] A.a
@@@$@@@$@@@$@@@$@@@$@@@$@@@$@@@$@@@$@@@$ A ---> a @@@$@@@$@@@$@@@$@@@$@@@$@@@$@@@$@@@$@@@$
[       OK ] A.a (3 ms)
[----------] 1 test from A

结论

使用RUN_GTESTBEGIN_TEST这两个宏,就可以灵活控制哪个测试用例该被执行,哪个该“消失”。因为两个宏的参数是完全一样的,因此要想开启或关闭一个测试用例,使用RUN_GTEST替换掉BEGIN_TEST即可(关闭一个用例则执行相反的替换)。如果要打开所有用例,那直接定义SKIP_ALL_TEST这个宏就可以了,不必挨个替换。

参考

gtest github主页

gtest samples

gtest 安装说明


作者水平有限,对相关知识的理解和总结难免有错误,还望给予指正,非常感谢!

在这里也能看到这篇文章:github博客, CSDN博客, 欢迎访问

以上是关于使用宏来作为C++单元测试开关的主要内容,如果未能解决你的问题,请参考以下文章

在 Visual Studio (TDD) 中构建和运行 C++ 单元测试

开关盒上的 iOS 单元测试

使用宏进行 iOS 单元测试

带有内部开关盒的单元测试 API 调用

小型开关电源特性测试

使用单元格作为位置在文件结构中保存文件的宏