您还未登录! 登录 | 注册 | 帮助  

您的位置: 首页 > 软件测试技术 > 单元测试 > 正文

一个极简C++单元测试框架

发表于:2017-08-06 作者:ay27 来源:

  C++ 并没有反射机制,如何做到自动发现并执行测试函数呢?我阅读了 gtest 的关键代码,找到了一个非常有意思的办法。
  1. 思路
  首先,单元测试框架的一般实现方法是:继承一个TestSuite 类,然后将若干测试函数实现为类成员函数。之后测试框架会自动发现并调用test_* 测试函数:
class MyTest : public TestSuite {
voidsetup(){...}
voidteardown(){...}
voidtest_1(){...}
voidtest_2(){...}
}
Test::runAllTests();
  然而C++ 中没有反射机制,所以想要在运行时通过比较简单的办法获得测试函数的入口并不太现实。有什么办法能在 MyTest 对象创建时自动获取到 test_* 测试函数入口呢?
  一个不太实用的方法是:
MyTest() {
vector test_funcs;
test_funcs.push_back(&test_1);
...
}
  这种方法每添加一个测试函数,都需要写一行push_back 代码,实在繁琐至极。有没有办法能做到自动发现函数入口呢?
  一个办法是利用宏,在编译期展开代码时把函数入口放入到一个列表中:
  #defineauto_invoke(func) func_list.append(func)
  然而这代码虽然能生成,但是并不会被执行,所以这种办法并没有什么用。
  到这里,问题的关键是,如何拼接一段代码,这段代码会被提前到 Test::runAllTests() 之前执行?
  2. 方法
  直接给出答案,静态类成员变量的赋值会抢先在对象创建前执行!也就是说,对于 class MyTest ,我们需要在这个类中声明一个静态类成员变量,然后执行赋值操作,在赋值操作中顺带把 push_back() 执行。也就是说,我们的函数可能是这样实现的:
  class MyTest : public TestSuite {
  static int invoke_time;
  voidadd_test(test_name){...}
  }
  static MyTest::invoke_time = push_all_func();
  然而存在一个问题,它并不能自动获取到测试函数的入口地址,因为赋值只会执行一次。那有没有办法做到多次赋值呢?有,利用模板,一个测试函数生成一个static 赋值操作,或者更简单的,一个测试函数生成一个测试类,这个类只实现一个同名的测试接口:
#defineADDTEST(parent_class, func_name)
class GENERATE_CLASS_NAME(parent_class, func_name) : public parent_class {
public:
GENERATE_CLASS_NAME(parent_class, func_name)() {}
void TestBody();
void before() {...}
void after() {...}
private:
static const unsigned long __a_trick__;
};
const unsigned long GENERATE_CLASS_NAME(parent_class, func_name)::__a_trick__ =
Test::get_instance()->add_test(new GENERATE_CLASS_NAME(parent_class, func_name));
void GENERATE_CLASS_NAME(parent_class, func_name)::TestBody()
// add a test function
ADDTEST(BasicTest, test_neq) {
neq(a, b);
};
  为了追踪并执行所有的测试对象,我们实现一个 Singleton 类:
class Test {
private:
Test() {}
std::vector testCases;
public:
staticTest *get_instance(){
static Test instance;
return &instance;
}
voidrun(){...}
unsignedlongadd_test(TestSuite *test_case){
testCases.push_back(test_case);
return testCases.size();
}
};
  那么现在,我们可以这样来写测试代码了:
class BasicTest : public TestSuite {
protected:
int a, b;
public:
voidsetup(){
a = 1234;
b = 4321;
}
voidteardown(){
}
};
ADDTEST(BasicTest, test_neq) {
neq(a, b);
};
ADDTEST(BasicTest, test_eq) {
a = b = 10;
eq(a, b);
}
intmain(){
Test::get_instance()->run();
}
  由于 ADDTEST 每次生成一个继承了 BasicTest 的类,所以会把 BasicTest 的成员变量也继承过来,所以就可以在 ADDTEST 中直接使用变量 a,b 了。现在这种做法基本上做到自动添加、执行测试函数了。更完整的代码在这里 https://github.com/ay27/simplest_unittest
  3. 总结
  一开始我的做法是这样的:
  #defineADDTEST(func_name)
  void __func_name__() {
  func_list.push_back(&func_name);
  }
  void func_name()
  然而可惜这段代码中的push_back 并不能被执行。
  总结一下,这里实现的关键有两点:一是如何抢先在 Singleton Test 对象创建前发现所有的测试函数,二是如何利用static 成员变量只会被执行一次的机制。