huaxiazhihuo

 

c++单元测试框架关键点记录成员函数地址

原则上,C++下最好的单元测试代码应该长成这样子,用起来才是最方便的
TEST_CLASS(className)
{
    
// 变量
    TEST_METHOD(fn1)
    {
        
// 
    }    
    TEST_METHOD(fn1)
    {
        
// 
    }
    
//
}
vczh大神的测试代码是这样子,这是最方便使用的形式,但因为是以测试方法为粒度,大括号里面就是一个函数体,所以显得功能上有些不足。
TEST_CASE(ThisIsATestCase)
{
TEST_ASSERT(1+1==2);
}
      当然,这里隐藏了很多宏的丑陋实现,但是,那又有什么要紧呢。好不好并不是在于用了什么东西,goto,多继承,宏,隐式类型转换,……,这些,如果能够显著地减少重复性相似性代码,还能带来类型安全,然后又其潜在的问题又在可控的范围之内,那么,又有什么理由拒绝呢。老朽一向认为,语言提供的语法糖功能要多多益善,越多越好,当然,必须像C++那样,不用它们的时候,就不会带来任何代价,那怕是一点点,就好像它们不存在,并且它们最好能正交互补。但是,你看看,cppunit,gtest的测试代码又是什么货色呢。
      据说cppunit里面用了很多模式,其架构什么的非常巧妙。反正使用起来这么麻烦,要做的重复事情太多了,这里写测试函数,那里注册测试函数,只能表示,慢走不送。gtest据说其架构也大有讲究,值得学习,用起来,也比cppunit方便,但是,看看TEST_F,什么SetUp,TearDown,各种鬼麻烦,谁用谁知道。一句话,我们其实只需要class粒度的测试代码,其他的一切问题就都是小case了。
      当然,class粒度的单元测试实现的难点在于收集要测试的成员函数。这里不能用虚函数。必须类似于mfc里面的消息映射成员函数表。也即是当写下TEST_METHOD(fn1),宏TEST_METHOD就要记录下来fn1的函数指针。后面跟着的一对大括号体是fn1的函数体,已经越出宏的控制范围了,所以只能在前面大做文章。下面是解决这个问题的思路。这个问题在C++03之前的版本,比较棘手。但是,所幸,C++11带来很多逆天的新功能,这个问题做起来就没那么难了。下面的思路省略其他各种次要的细节问题。
首先,我们定义一个空类和要测试的成员函数的形式。
struct EmptyClass{};
typedef void(EmptyClass::*TestMethodPtr)();
还有存放成员函数地址的链表节点
struct MethodNode
{
    MethodNode(MethodNode
*& head, TestMethodPtr method)
    {
        mNext 
= head;
        head 
= this;
        mMethod 
= method;
    }
    MethodNode
* mNext;
    TestMethodPtr mMethod;
};
还有提取成员函数地址的函数

template 
<class OutputClass, class InputClass>
union horrible_union{
    OutputClass 
out;
    InputClass 
in;
};

template 
<class OutputClass, class InputClass>
inline 
void union_cast(OutputClass& outconst InputClass input){
    horrible_union
<OutputClass, InputClass> u;
    static_assert(
sizeof(InputClass) == sizeof(u) && sizeof(InputClass) == sizeof(OutputClass), "out and in should be the same size");
    u.
in = input;
    
out = u.out;
}
template
<typename Ty>
TestMethodPtr GetTestMethod(
void(Ty::*testMethod)())
{
    TestMethodPtr methodPtr;
    union_cast(methodPtr, testMethod);
    
return methodPtr;
}
方法是每定义一个测试函数,在其上面就先定义一个链表节点变量,其构造函数记录测试函数地址,并把自身加入到链表中。但是,在此之前,我们将遭遇到编译器的抵触。比如
struct TestCase
{
    typedef TestCase ThisType;
    MethodNode
* mMethods = nullptr;

    TestMethodPtr mTestMethodfn1 
= GetTestMethod(&fn1);
    void fn1(){}
};
      vc下面,编译器报错 error C2276: “&”: 绑定成员函数表达式上的非法操作
      原来在就地初始化的时候,不能以这种方式获取到地址。然后,试试在TestCase里面的其他函数中,包括静态函数,就可以将取地址符号用到成员函数前面。
      这好像分明是编译器在故意刁难,不过,任何代码上的问题都可以通过引入中间层来予以解决。用内部类。
struct TestCase
{
    typedef TestCase ThisType;
    MethodNode
* mMethods = nullptr;

   
struct Innerfn1 : public MethodNode
    {
        Innerfn1(ThisType
* pThis) : MethodNode(pThis->mMethods, GetTestMethod(&ThisType::fn1))
        {
        }
    } mTestMethodfn1 
= this;
    
void fn1(){}

    
struct Innerfn2 : public MethodNode
    {
        Innerfn2(ThisType
* pThis) : MethodNode(pThis->mMethods, GetTestMethod(&ThisType::fn2))
        {
        }
    } mTestMethodfn2 
= this;
    
void fn2(){}
};
      有多少个测试方法,就动用多少种内部类。然后,一旦定义一个测试类的变量,那么这些内部类的构造函数就执行了,把测试方法串联在一块,逆序,也就是说最后定义测试方法反而跑到前面去了。这样子就自动记录下来所有的测试方法的地址。有了这些函数地址信息,后面怎么玩都可以。包括漂亮的测试结果显示,日志记录,甚至嵌入到vs的单元测试界面中,又或者是生成配置文件,各种花招,怎么方便就怎么玩。这个时候,可以拿来主义,把cppunit,gtest等的优点都吸收过来。
      是否觉得这还不够,好像有很多事情要做。比如说,测试方法逆序了,在同一个测试类的变量上执行这些测试方法,会不会就扰乱类的内部信息了,每次new一个测试类,所有的测试方法都要重复记录,内部类变量要占内存……。咳咳,这些都可以一一解决。这里只是用最简明的方式展示自动记录测试方法,产品级的写法肯定大有讲究了。
      可以看到上面的代码都是有意做成很相似的,这些都是准备给宏大展身手的。这些低级宏太容易编写了,任何经历mfc或者boost代码折磨的猿猴,都完全能够胜任,这就打住了。对了,这里的自动记录成员函数的宏手法,可以大量地使用到其他地方,比如说,自动生成消息映射表,比mfc的那一套要好一百倍,应用范围太广了。当初老朽以为就只能用于单元测试框架的编写上面,想不到其威力如此巨大,消息系统全靠它了。C++的每一项奇技淫巧和功能被发现后,其价值都难以估量,好像bs所说的,他老人家不会给c++增添一项特性,其应用范围一早就可以预料的。对付一个问题,C++有一百种解决方案,当然里面只有几种才最贴切问题领域,但是很多时候,我们往往只选择或者寻找到另外的那90多种,最后注定要悲剧。

posted on 2016-05-11 18:01 华夏之火 阅读(1515) 评论(0)  编辑 收藏 引用 所属分类: c++技术探讨


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


导航

统计

常用链接

留言簿(6)

随笔分类

随笔档案

搜索

积分与排名

最新评论

阅读排行榜

评论排行榜