主要参考了Google test(gtest)和知乎上的一篇文章qtest: 一个单元测试库的从头实现以及作者提供的代码,尤其是宏的部分。一直不喜欢也没有学明白宏的各种用法,但是实现这种风格的测试框架也绕不开宏。 在其基础上进行了整理和重构,并且扩展和完善了一些细节的功能。

本文作为 C++ 学习中的小练习,若有疏漏,欢迎指正。

MTest 介绍

MTest 是一个 head-only 的简易框架,只包含两个头文件:

  • mtest.hpp 负责 MTest 和 MTest::MTestMessage 两个类的实现,不含有任何的宏。
  • mtest_macro.hpp 负责对外提供相应的宏,可以在编译时使用-DUNUSE_MTEST关闭所有的宏,避免与 gtest 产生冲突。

测试文件需要 include 这两个文件,include顺序无所谓,也可以直接合并成一个文件,但是我个人的喜好是对宏敬而远之,因此单独仍在一个头文件中。主要功能实现在mtest.hpp,其中不含有任何的宏。

为什么重复造轮子?

  • 我希望将 MTest 作为 gtest 的简易替代,尤其是它具有 head-only 的特点:不需要编译和链接相应的库,使用非常轻量,实现细节完全透明。mtest.hpp只有五百多行代码,并且代码可读性较高。
  • 实现 MTest 也是学习提升的机会:
    • 可以学习 gtest 的使用,并且利用简洁的语法完成 gtest 的一个小子集的功能。
    • 可以发现语法上的盲点,例如 mtest 利用了全局静态变量在 main 函数之前初始化的特点,将测试函数自动注册,但是这里根据 clang-tidy 的语法检查,需要保证注册时不会抛出任何异常。(std::string构造可能抛异常,因此需要避免使用)
    • 在 filter 的实现中,字符串匹配的判断基于动态规划,测试中发现网上参考代码的小 bug,进行了修正和完善。
    • 在保证功能正确的前提下,不断打磨,写出更加干净漂亮,可读性高的代码。

完成内容

已经完成的部分:

  • 最基本的EXPECT_XX宏和ASSERT_XX宏;
  • TEST宏;(没有支持 gtest 的TEST_F宏以及其它高级用法)
  • 支持使用 filter 对测试进行过滤筛选,只能使用c*.1?.2这种简易的 filter,并且只支持一个 filter;
  • 输出格式和内容基本与 gtest 相同;
  • 支持如下的命令行参数:
    • --mtest_filter=XXX,设置 filter;
    • --mtest_list_tests,列出(满足当前 filter 的)所有测试,不执行;
    • --mtest_use_color,开启彩色输出模式;
    • --mtest_brief,开启简洁输出模式。

可以继续完善的部分:

  • TEST_F宏的实现;
  • 当前的 filter 支持比较简单,可以进一步实现与 gtest 相同的 filter 规则;
  • 使用模板元编程,进一步丰富代码的功能。

MTest模仿gtest,使用如下两种方式提供main函数。

第一种方式是直接在测试文件的合适位置添加宏MTEST_MAIN(注意避免重复定义main函数),这个宏会自动展开为

1
2
3
4
int main(int argc, char *argv[]) {
MTest::InitMTest(argc, argv, __FILE__);
return MTest::RunAllTests();
}

当然在定义了UNUSE_MTEST时,MTEST_MAIN宏和其它宏一样都会定义为空,不会产生冲突。

第二种方式是模仿gtest_main的,创建如下文件并编译为一个静态库,在编译测试文件时链接到一起即可

mtest_main.cpp
1
2
3
4
#include "mtest.hpp"
#include "mtest_macro.hpp"

MTEST_MAIN

MTest切换gtest

MTest 在实现中尽可能地保持了与 gtest 子集的兼容性:所有 MTest 已经实现的宏和功能都保持了与 gtest 相同的语法,并且可以在不改动测试源文件的前提下,直接从 MTest 切换为 gtest。

从 MTest 切换回 gtest 需要使用编译选项:

  • -DUNUSE_MTEST关闭 MTest 提供的宏,它们都定义在mtest_macro.hpp中;
  • 由于测试源文件没有包含 gtest 所需的头文件,因此需要使用-include编译选项导入头文件,例如
1
-I../../external/googletest -include gtest/gtest.h
  • 由于测试源文件中没有 main 函数,MTest 使用宏的方式去生成固定的 main 函数,因此改成 gtest 时必须链接gtestgtest_main这两个库,例如
1
-L../../external/googletest/lib -lgtest -lgtest_main

这里编译选项中的路径由 gtest 实际的位置决定。

实例

实例一

这里我们使用 gtest 提供的 sample1 进行展示,略去了文件中的注释内容。sample1 包含两个函数:一个是阶乘,一个是素数判断。

sample1.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int Factorial(int n) {
int result = 1;
for (int i = 1; i <= n; i++) {
result *= i;
}

return result;
}

// Returns true if and only if n is a prime number.
bool IsPrime(int n) {
if (n <= 1) return false;
if (n % 2 == 0) return n == 2;

for (int i = 3;; i += 2) {
if (i > n / i) break;
if (n % i == 0) return false;
}
return true;
}

测试文件如下,分别对两个函数进行测试,即两个测试组:FactorialTestIsPrimeTest,各自包含三个测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// include ...

namespace {

// Tests factorial of negative numbers.
TEST(FactorialTest, Negative) {
EXPECT_EQ(1, Factorial(-5));
EXPECT_EQ(1, Factorial(-1));
EXPECT_GT(Factorial(-10), 0);
}

// Tests factorial of 0.
TEST(FactorialTest, Zero) { EXPECT_EQ(1, Factorial(0)); }

// Tests factorial of positive numbers.
TEST(FactorialTest, Positive) {
EXPECT_EQ(1, Factorial(1));
EXPECT_EQ(2, Factorial(2));
EXPECT_EQ(6, Factorial(3));
EXPECT_EQ(40320, Factorial(8));
}

// Tests negative input.
TEST(IsPrimeTest, Negative) {
// This test belongs to the IsPrimeTest test case.

EXPECT_FALSE(IsPrime(-1));
EXPECT_FALSE(IsPrime(-2));
EXPECT_FALSE(IsPrime(INT_MIN));
}

// Tests some trivial cases.
TEST(IsPrimeTest, Trivial) {
EXPECT_FALSE(IsPrime(0));
EXPECT_FALSE(IsPrime(1));
EXPECT_TRUE(IsPrime(2));
EXPECT_TRUE(IsPrime(3));
}

// Tests positive input.
TEST(IsPrimeTest, Positive) {
EXPECT_FALSE(IsPrime(4));
EXPECT_TRUE(IsPrime(5));
EXPECT_FALSE(IsPrime(6));
EXPECT_TRUE(IsPrime(23));
}
} // namespace

MTEST_MAIN

编译运行的结果如下图,这里 gtest 没有使用彩色输出,绿色是 powershell 自带的,MTest 多了一个 logo,输出内容和格式基本相同。

实例二

由于 sample1 只有正确的测试算例,我们再给出一个含有错误测试的例子,测试文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "mtest.hpp"
#include "mtest_macro.hpp"

namespace {

TEST(Equal, 1) { EXPECT_EQ(1, 1) << "is 1==1 ?"; }

TEST(Equal, 2) { EXPECT_EQ(2, 3) << "is 2==3 ?"; }

TEST(NotEqual, 1) { EXPECT_NE(2, 3) << "is 2!=3 ?"; }

TEST(IsTrue, 1) { EXPECT_TRUE(2 < 3); }

TEST(isTrue, 2) { EXPECT_TRUE(2 > 4); }

} // namespace

MTEST_MAIN

分别使用 MTest 和 gtest 进行编译,得到结果如下,这里 MTest 开启了彩色输出模式--mtest_use_color

我们再对两者都开启简洁输出模式,分别使用--mtest_brief--gtest_brief选项,只显示未通过的测试信息。

我们对两者都使用过滤器,分别使用--mtest_filter=*.1--gtest_filter=*.1选项,过滤之后的测试全部通过。

源代码

源文件放置在Github仓库:cpptoybox