C++单元测试一:并非看上去那么简单——几个很实际的问题

寄语:很值得思考,最近在看代码大全,看到开发者测试那章,于是便在网上查这方面的资料,看到了作者的烦恼,作者在第二篇里说了应该在写代码的时候就开始考虑写易于测试的代码,实际中希望自己也能注意到这点.

这篇文章还有第二篇,我就不转载了直接贴地址了,地址:http://blog.csdn.net/henan_lujun/article/details/9009395


原文地址:http://blog.csdn.net/henan_lujun/article/details/9002420

为Java和C#做单元测试,基本上都有比较统一的工具、模式可用,IDE的支持也非常到位;可是到了C++这里,一切就变的那样的“不走寻常路”,各种单元测试框架盛行,例如CppUnit, CppUnitLite, CxxUnit,Google Test等等,以及微软在VS2012中也加入了对原生C++代码单元测试的支持MSTest。面对如此诸多的测试框架,选择哪一个其实无所谓,只要顺手就好,用习惯了,什么都好;因为,单元测试的代码总归还是要自己来写——无论你用哪一个框架。

以前写过不少代码,可是都没怎么注意单元测试,现在终于认真对待起来,就开始在网络上搜寻资料,看看各种框架下的单元测试如何写。幸运的是,这方面的资料真不少,很多框架也都会带有示例或者文档告诉你怎么写;不幸的是,这些文档或者示例都太远离工程实践,他们一般遵循一个这样的模式:写出一个待测类CMyClass,定义一定的成员变量和方法,然后给出针对CMyClass的测试类如CMyClassTest;呵呵,看起来示例是够好了,可是很少涉及到这样的实际问题: 

  • 工程实践中,一个项目往往有很多类,而且相互之间,总有这或多或少的依赖、包含等关系;
  •  实际的测试项目,该如何组织被测代码和测试代码(注意,这里所说的组织不是指单元测试数据如何组织,而是指工程中的测试代码和产品代码的组织)
  •  被测代码如何引入测试工程中
  •  一个测试工程如何测试多个被测类

实际的代码

好吧,我们来看一下一个“较为”实际的工程以及我在为该工程编写单元测试过程中所遇到的问题;该工程的代码来自于boost.asio中的一个示例代码:

http://www.boost.org/doc/libs/1_53_0/doc/html/boost_asio/example/chat/chat_server.cpp

我把该cpp中的几个类分拆开了,放在一个VisualStudio 2012工程中,代码结构看起来是:


其中几个主要类Message,ChatSession,ChatRoom之间的关系如下图:


在我做单元测试过程中,首先从软柿子Message入手,然后为ChatRoom写UT。所以,先把这两个类的相关代码贴上来;其实贴不贴代码无关紧要,上面的类图已经可以说明问题,不过为了方便较真,还是贴出来吧。

Message类代码

[cpp]  view plain copy
  1. /// Message.h  
  2. #pragma once  
  3.    
  4. /// class represents the message transferred between the client and the server  
  5. /// the message consists of two part: header + message body  
  6. /// the header part, 4 bytes, stores the length of the message body  
  7. /// the max length of the message box is : 512  
  8.    
  9. class Message  
  10. {  
  11. public:  
  12.       enum { HeaderLength = 4, MaxBodyLength = 511 };  
  13.    
  14. public:  
  15.       Message(void);  
  16.       ~Message(void);  
  17.    
  18. public:  
  19.       void EncodeHeader();  
  20.       bool DecodeHeader();  
  21.    
  22.       const char* Data() const;  
  23.       char* Data() ;  
  24.       const char* Body() const;  
  25.       char* Body() ;  
  26.    
  27.       int SetData(const char* src, const int srclength);  
  28.    
  29.       int Length() const;  
  30.       int BodyLength()const;  
  31.    
  32.       void Reset();  
  33.    
  34. private:  
  35.     void CheckBodyLength();  
  36.    
  37. private:  
  38.       /// stores the whole message  
  39.       char Data_[HeaderLength + MaxBodyLength + 1];  
  40.    
  41.       /// the body length  
  42.       int BodyLength_;  
  43. };  
  44.    
  45. /// Message.cpp  
  46. #include "Message.h"  
  47. #include <cstdio>  
  48. #include <boost/lexical_cast.hpp>  
  49. #include <algorithm>  
  50. #include "TraceLog.h"  
  51.    
  52.    
  53. Message::Message(void)  
  54. {  
  55.       Reset();  
  56. }  
  57.    
  58.    
  59. Message::~Message(void)  
  60. {  
  61. }  
  62.    
  63. void Message::CheckBodyLength()  
  64. {  
  65.     BodyLength_ = BodyLength_ > MaxBodyLength ? MaxBodyLength : BodyLength_;  
  66. }  
  67.    
  68. void Message::EncodeHeader()  
  69. {  
  70.     /// Check the body length  
  71.     CheckBodyLength();  
  72.    
  73.       /// wirte the body length to the message header  
  74.       /// we make sure that the buffer is enough after we call CheckBodyLength()  
  75.       ::_snprintf_s( Data_, HeaderLength, HeaderLength, "%d", BodyLength_ );     
  76. }  
  77.    
  78. bool Message::DecodeHeader()  
  79. {  
  80.       int bodyLength = 0;  
  81.       bool ret = false;  
  82.    
  83.       /// get the message body length from the message  
  84.       try  
  85.       {  
  86.             char buf[HeaderLength + 1] = "";  
  87.             std::strncat( buf, Data_, HeaderLength );  
  88.             bodyLength = boost::lexical_cast<int> (buf);  
  89.    
  90.             if( bodyLength > MaxBodyLength )  
  91.             {  
  92.                   bodyLength = MaxBodyLength;                
  93.             }  
  94.             else  
  95.             {  
  96.                   ret = true;  
  97.             }  
  98.       }  
  99.       catch(boost::bad_lexical_cast& e)  
  100.       {  
  101.             /// cast error happens  
  102.             bodyLength = 0;  
  103.    
  104.             TraceLog::WriteLine("Message::DecodeHeader(),error:%s, orinal message:%s", e.what(), Data_ );  
  105.       }  
  106.    
  107.       /// set the value and return  
  108.       BodyLength_ = bodyLength;  
  109.       return ret;  
  110. }  
  111.    
  112. char* Message::Data()  
  113. {  
  114.       return Data_ ;  
  115. }  
  116.    
  117. const char* Message::Data() const  
  118. {  
  119.       return Data_ ;  
  120. }  
  121.    
  122. char* Message::Body()  
  123. {  
  124.       return Data_ + HeaderLength;  
  125. }  
  126.    
  127. const char* Message::Body() const  
  128. {  
  129.       return Data_ + HeaderLength;  
  130. }  
  131.    
  132. int Message::SetData(const char* src, const int srclength)  
  133. {  
  134.       /// check the length of source  
  135.       int length = srclength;  
  136.       if( length > MaxBodyLength )  
  137.       {  
  138.             length = MaxBodyLength;  
  139.       }  
  140.    
  141.       /// copy the data into the local buffer  
  142.       /// std::snprintf is unavailable in this c++ compiler  
  143.       int ret = ::_snprintf_s(Data_+HeaderLength, MaxBodyLength + 1, length, "%s", src );     
  144.    
  145.       /// set the length of the message body  
  146.       BodyLength_ = length;  
  147.    
  148.       /// return the length of copied  
  149.       return ret;  
  150. }  
  151.    
  152. int Message::Length() const  
  153. {  
  154.       return BodyLength_ + HeaderLength;  
  155. }  
  156.    
  157. int Message::BodyLength() const  
  158. {  
  159.       return BodyLength_;  
  160. }  
  161.    
  162. void Message::Reset()  
  163. {  
  164.       BodyLength_ = 0;  
  165.    
  166.       /// just for using the lamda  
  167.       std::for_each(Data_, Data_ + HeaderLength + MaxBodyLength + 1, [](char& p) { p = '\0'; } );  
  168. }  

ChatRoom类代码

[cpp]  view plain copy
  1. /// ChatRoom.h  
  2. #pragma once  
  3. #include "ChatSession.h"  
  4. #include "Message.h"  
  5. #include <set>  
  6. #include <queue>  
  7.    
  8. /// class that manages the clients  
  9. /// deliver the messages from one client to the others  
  10.    
  11. class ChatRoom  
  12. {  
  13. public:  
  14.       ChatRoom(void);  
  15.       ~ChatRoom(void);  
  16.    
  17. public:  
  18.       /// a client joins in the room  
  19.       void Join(ChatParticipantPtr participant);  
  20.    
  21.       /// a client leaves the room  
  22.       void leave(ChatParticipantPtr participant);  
  23.    
  24.       /// deliver the message from one client to all of the users in the room  
  25.       void Deliver(const Message& msg);  
  26.    
  27. private:  
  28.       /// all of the participants are stored here  
  29.       std::set<ChatParticipantPtr> Participants_;  
  30.    
  31.       /// recent messages  
  32.       /// questions, how to synchronize this object in threads  
  33.       typedef std::deque<Message> MessageQueue;  
  34.       MessageQueue RecentMessages_;  
  35.       enum { MaxRecentMsgs = 100 };  
  36. };  
  37.    
  38.    
  39. /// ChatRoom.cpp  
  40. #include "ChatRoom.h"  
  41. #include <boost/bind.hpp>  
  42. #include <algorithm>  
  43. #include "TraceLog.h"  
  44.    
  45. ChatRoom::ChatRoom(void)  
  46. {  
  47. }  
  48.    
  49.    
  50. ChatRoom::~ChatRoom(void)  
  51. {  
  52. }  
  53.    
  54. /// a client joins in the room  
  55. void ChatRoom::Join(ChatParticipantPtr participant)  
  56. {  
  57.       TraceLog::WriteLine("ChatRoom::Join(), a new user joins in");  
  58.    
  59.       /// add into the queue  
  60.       Participants_.insert( participant );  
  61.    
  62.       /// sending the recent message to the client  
  63.       std::for_each(RecentMessages_.begin(), RecentMessages_.end(),  
  64.             boost::bind( &ChatParticipant::Deliver, participant, _1 ) );  
  65. }  
  66.    
  67.    
  68. /// a client leaves the room  
  69. void ChatRoom::leave(ChatParticipantPtr participant)  
  70. {  
  71.       TraceLog::WriteLine("ChatRoom::leave(), a user leaves");  
  72.    
  73.       /// remove it from the queue  
  74.       Participants_.erase( participant );  
  75. }  
  76.    
  77.    
  78. /// deliver the message from one client to all of the users in the room  
  79. void ChatRoom::Deliver(const Message& msg)  
  80. {  
  81.       TraceLog::WriteLine("ChatRoom::Deliver(), %s", msg.Body() );  
  82.    
  83.       /// add the msg to queue  
  84.       RecentMessages_.push_back( msg );  
  85.    
  86.       /// check the length  
  87.       while( RecentMessages_.size() > MaxRecentMsgs )  
  88.       {  
  89.             RecentMessages_.pop_front();  
  90.       }  
  91.    
  92.       /// deliver the msg to clients  
  93.       std::for_each(Participants_.begin(), Participants_.end(),  
  94.             boost::bind( &ChatParticipant::Deliver, _1, boost::ref(msg) ) );  
  95. }  
  96.    


开始单元测试

由于到手了VisualStudio 2012,这货已经原始支持了C++Native代码的单元测试,就用这货开始做UT吧。

如何引入被测代码

好了,我们开始单元测试。首先创建一个C++单元测试的工程,这个很easy。接着我们就要让测试工程能够“看到”被测的代码,这如何搞呢?有这样几种方法:

  • 如果被测代码是静态库或者动态库,包含对应的.h文件,让测试工程链接DLL及LIB,这样测试工程。
  • 或者,让测试工程链接对应的obj文件,直接编译进测试工程
  • 或者,直接把被测是的代码,如上述的Message.h和Message.cpp包含进测试工程(注意这里不要拷贝一份Message.h和Message.cpp,用“Add->ExsitingItem”将他们包含进去,这样只保留一份代码)
  •  或者在单元测试代码文件,如TestMessage.cpp中直接用#include把Message.h和Message.cpp包含进来,如: 

               #include "../ChatroomServer/ChatRoom.h"

               #include "../ChatroomServer/ChatRoom.cpp"

上面这几种方法,其实原理都是一样的,反正就是让测试工程能够看到到被测的代码,我们使用把被测代码引入测试工程的方法,这样测试工程的代码结构看起来是这样:

Ok,现在在测试工程里面,可以看到Message类的声明和定义了,然后你的单元测试代码,该怎么写,就怎么写了。

一个测试工程只能测一个类吗?

使用VS2012中的单元测试框架,写完了对Message的的单元测试,在TestExplorer中RunAll,一切正常;好了,至此,一切还算顺利,那我们继续吧,来对ChatRoom类编写单元测试;

继续按照前面的方法,我们很容易让测试工程看到ChatRoom的被测代码;然而从ChatRoom的实现来看,这个类和Message类有着关联关系,而且在ChatRoom的方法中,也的确使用了Message类的方法,从单元测试的角度来看,我们应该将他们俩之间的关系隔断,从而保证我们只对ChatRoom进行测试,那这样,我们就需要Mock一份Message的实现。

可是问题来了,由于之前我们在测试Message类的时候,已经引入了Message.cpp文件,使得测试工程中,已经有了一份Message的实现,那么我们如何再能为Message提供一份“伪”实现呢??(使用其他几种引入方式,结果都是一样的)

是的,惯用的DependencyInjection在这里不起作用。查了不少资料,也没找到一个像样的说明如何解决这个问题;现在唯一可以采用的,就是在一个测试工程里面,只测试一个被测类,虽然可以工作,但是,未免又过于繁琐和“愚蠢”,那聪明的方法,又是什么呢?不瞒你说,这正是目前困扰我的问题之一。

 

追加一个后记:

其实,在关于如何为Message提供一份“伪”实现的问题上,原来想法是在测试工程中包含Message的头文件,然后在测试工程里面,直接写Message::Message()等方法,事实上是在测试工程里面定义一个Message的实现;这样由于我们已经引入了Message的真正实现,从而必然导致链接器报符号重复定义的错误,因此,这种思路并不可行,故而强迫我去创建另外一个工程;后来想一想,其实也不必真的去创建一个新工程,他们是可以在一个工程里面完成的,方法是新建一个MockMessage类,让他从Message继承下来,然后重新定义自己想Mock的方法就可以了。但是,这种方法创建出来的Mock,还是真的Mock吗,他首先已经是一个“真”的Message了啊?


wings是一款用于单元测试测试用例驱动框架自动生成工具,这款工具主要是全自动生成单元测试驱动代码与测试数据。解决做单元测试耗时耗力,编写难度大等问题。提升开发和测试效率。 特点: (1) 程序参数深度分析问题 Wings通过编译器底层技术,将输入的源文件,按照函数为单位,形成模块对象。对象中包含函数的输入参数,返回值类型等信息,供驱动函数模块和测试用例模块使用。每个文件作为一个单元,针对其中的每个函数的每个参数进行深度解析,对于嵌套类型,复杂类型等都可以实现精确的解析和分解,将复杂类型逐层讲解为基础数据类型,并产生参数结构的描述文件(PSD)。 (2) 函数驱动自动生成模块 依据PSD文件的格式信息,自动生成被测源程序的所有驱动函数,单元测试过程不再依赖开发人员手动编写测试函数,只需将生成的驱动函数和被测源文件一起编译,即可执行测试并查看测试结果。测试驱动自动生成程序基于PSD描述,全自动构建驱动被测程序运行的所有参数,必须的全局变量,并可根据复杂变量的层级结构产生结构化的测试驱动程序,可以节省大量的单元测试用例的编写时间。 (3) 测试数据自动生成与管理 用于自动生成测试数据,测试数据与被测函数提取的信息相互对应,数据以一定的层次逻辑关系存 储在json文件中。数据和经过分解和展开后的数据类型是一一对应的。这些数据用户可以根据业务要求随意边际,并且用json文件进行结构化,层次化展示,非常的清晰。其中的测试数据包括全局变量值、被测函数调用时的参数值。 优点: 1. 可以为任意复杂参数结构C语言开发的系统全自动生成测试驱动程序 2. 可完成对于被测试函数的参数进行多层编译解析,并完成复杂参数赋值的代码的自动生成。 3. 支持被测函数引用的全局变量的分析和自动赋值程序的生成。 4. 能够区分系统变量和用户变量,对于复杂的系统变量可由用户自定义赋值模板。 例如File类型,而不是把复杂的系统变量全部展开。 5. 支持多层次的可视化的数据表格来对变量进行赋值,而无需关注驱动程序本身。 数据表格可以表达任意深度和多层次的数据关系,用户只需要对表格数据进行编辑,自动生成的驱动程序会自动完成表格数据的读取和参数赋值的构造过程。 6. Wings支持所有C语言的数据类型(基础类型,结构体,指针,数组,枚举等)以及高层级数据结构。 例如链表的分析和对应的驱动和数据表格框架的生成。 7. Wings生成的代码与人工写的非常相近,可读性强,自带注释和按照层次的缩进和代·码编排。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值