寄语:很值得思考,最近在看代码大全,看到开发者测试那章,于是便在网上查这方面的资料,看到了作者的烦恼,作者在第二篇里说了应该在写代码的时候就开始考虑写易于测试的代码,实际中希望自己也能注意到这点.
这篇文章还有第二篇,我就不转载了直接贴地址了,地址: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类代码
- /// Message.h
- #pragma once
- /// class represents the message transferred between the client and the server
- /// the message consists of two part: header + message body
- /// the header part, 4 bytes, stores the length of the message body
- /// the max length of the message box is : 512
- class Message
- {
- public:
- enum { HeaderLength = 4, MaxBodyLength = 511 };
- public:
- Message(void);
- ~Message(void);
- public:
- void EncodeHeader();
- bool DecodeHeader();
- const char* Data() const;
- char* Data() ;
- const char* Body() const;
- char* Body() ;
- int SetData(const char* src, const int srclength);
- int Length() const;
- int BodyLength()const;
- void Reset();
- private:
- void CheckBodyLength();
- private:
- /// stores the whole message
- char Data_[HeaderLength + MaxBodyLength + 1];
- /// the body length
- int BodyLength_;
- };
- /// Message.cpp
- #include "Message.h"
- #include <cstdio>
- #include <boost/lexical_cast.hpp>
- #include <algorithm>
- #include "TraceLog.h"
- Message::Message(void)
- {
- Reset();
- }
- Message::~Message(void)
- {
- }
- void Message::CheckBodyLength()
- {
- BodyLength_ = BodyLength_ > MaxBodyLength ? MaxBodyLength : BodyLength_;
- }
- void Message::EncodeHeader()
- {
- /// Check the body length
- CheckBodyLength();
- /// wirte the body length to the message header
- /// we make sure that the buffer is enough after we call CheckBodyLength()
- ::_snprintf_s( Data_, HeaderLength, HeaderLength, "%d", BodyLength_ );
- }
- bool Message::DecodeHeader()
- {
- int bodyLength = 0;
- bool ret = false;
- /// get the message body length from the message
- try
- {
- char buf[HeaderLength + 1] = "";
- std::strncat( buf, Data_, HeaderLength );
- bodyLength = boost::lexical_cast<int> (buf);
- if( bodyLength > MaxBodyLength )
- {
- bodyLength = MaxBodyLength;
- }
- else
- {
- ret = true;
- }
- }
- catch(boost::bad_lexical_cast& e)
- {
- /// cast error happens
- bodyLength = 0;
- TraceLog::WriteLine("Message::DecodeHeader(),error:%s, orinal message:%s", e.what(), Data_ );
- }
- /// set the value and return
- BodyLength_ = bodyLength;
- return ret;
- }
- char* Message::Data()
- {
- return Data_ ;
- }
- const char* Message::Data() const
- {
- return Data_ ;
- }
- char* Message::Body()
- {
- return Data_ + HeaderLength;
- }
- const char* Message::Body() const
- {
- return Data_ + HeaderLength;
- }
- int Message::SetData(const char* src, const int srclength)
- {
- /// check the length of source
- int length = srclength;
- if( length > MaxBodyLength )
- {
- length = MaxBodyLength;
- }
- /// copy the data into the local buffer
- /// std::snprintf is unavailable in this c++ compiler
- int ret = ::_snprintf_s(Data_+HeaderLength, MaxBodyLength + 1, length, "%s", src );
- /// set the length of the message body
- BodyLength_ = length;
- /// return the length of copied
- return ret;
- }
- int Message::Length() const
- {
- return BodyLength_ + HeaderLength;
- }
- int Message::BodyLength() const
- {
- return BodyLength_;
- }
- void Message::Reset()
- {
- BodyLength_ = 0;
- /// just for using the lamda
- std::for_each(Data_, Data_ + HeaderLength + MaxBodyLength + 1, [](char& p) { p = '\0'; } );
- }
ChatRoom类代码
- /// ChatRoom.h
- #pragma once
- #include "ChatSession.h"
- #include "Message.h"
- #include <set>
- #include <queue>
-
- /// class that manages the clients
- /// deliver the messages from one client to the others
-
- class ChatRoom
- {
- public:
- ChatRoom(void);
- ~ChatRoom(void);
-
- public:
- /// a client joins in the room
- void Join(ChatParticipantPtr participant);
-
- /// a client leaves the room
- void leave(ChatParticipantPtr participant);
-
- /// deliver the message from one client to all of the users in the room
- void Deliver(const Message& msg);
-
- private:
- /// all of the participants are stored here
- std::set<ChatParticipantPtr> Participants_;
-
- /// recent messages
- /// questions, how to synchronize this object in threads
- typedef std::deque<Message> MessageQueue;
- MessageQueue RecentMessages_;
- enum { MaxRecentMsgs = 100 };
- };
-
-
- /// ChatRoom.cpp
- #include "ChatRoom.h"
- #include <boost/bind.hpp>
- #include <algorithm>
- #include "TraceLog.h"
-
- ChatRoom::ChatRoom(void)
- {
- }
-
-
- ChatRoom::~ChatRoom(void)
- {
- }
-
- /// a client joins in the room
- void ChatRoom::Join(ChatParticipantPtr participant)
- {
- TraceLog::WriteLine("ChatRoom::Join(), a new user joins in");
-
- /// add into the queue
- Participants_.insert( participant );
-
- /// sending the recent message to the client
- std::for_each(RecentMessages_.begin(), RecentMessages_.end(),
- boost::bind( &ChatParticipant::Deliver, participant, _1 ) );
- }
-
-
- /// a client leaves the room
- void ChatRoom::leave(ChatParticipantPtr participant)
- {
- TraceLog::WriteLine("ChatRoom::leave(), a user leaves");
-
- /// remove it from the queue
- Participants_.erase( participant );
- }
-
-
- /// deliver the message from one client to all of the users in the room
- void ChatRoom::Deliver(const Message& msg)
- {
- TraceLog::WriteLine("ChatRoom::Deliver(), %s", msg.Body() );
-
- /// add the msg to queue
- RecentMessages_.push_back( msg );
-
- /// check the length
- while( RecentMessages_.size() > MaxRecentMsgs )
- {
- RecentMessages_.pop_front();
- }
-
- /// deliver the msg to clients
- std::for_each(Participants_.begin(), Participants_.end(),
- boost::bind( &ChatParticipant::Deliver, _1, boost::ref(msg) ) );
- }
-
- /// ChatRoom.h
- #pragma once
- #include "ChatSession.h"
- #include "Message.h"
- #include <set>
- #include <queue>
- /// class that manages the clients
- /// deliver the messages from one client to the others
- class ChatRoom
- {
- public:
- ChatRoom(void);
- ~ChatRoom(void);
- public:
- /// a client joins in the room
- void Join(ChatParticipantPtr participant);
- /// a client leaves the room
- void leave(ChatParticipantPtr participant);
- /// deliver the message from one client to all of the users in the room
- void Deliver(const Message& msg);
- private:
- /// all of the participants are stored here
- std::set<ChatParticipantPtr> Participants_;
- /// recent messages
- /// questions, how to synchronize this object in threads
- typedef std::deque<Message> MessageQueue;
- MessageQueue RecentMessages_;
- enum { MaxRecentMsgs = 100 };
- };
- /// ChatRoom.cpp
- #include "ChatRoom.h"
- #include <boost/bind.hpp>
- #include <algorithm>
- #include "TraceLog.h"
- ChatRoom::ChatRoom(void)
- {
- }
- ChatRoom::~ChatRoom(void)
- {
- }
- /// a client joins in the room
- void ChatRoom::Join(ChatParticipantPtr participant)
- {
- TraceLog::WriteLine("ChatRoom::Join(), a new user joins in");
- /// add into the queue
- Participants_.insert( participant );
- /// sending the recent message to the client
- std::for_each(RecentMessages_.begin(), RecentMessages_.end(),
- boost::bind( &ChatParticipant::Deliver, participant, _1 ) );
- }
- /// a client leaves the room
- void ChatRoom::leave(ChatParticipantPtr participant)
- {
- TraceLog::WriteLine("ChatRoom::leave(), a user leaves");
- /// remove it from the queue
- Participants_.erase( participant );
- }
- /// deliver the message from one client to all of the users in the room
- void ChatRoom::Deliver(const Message& msg)
- {
- TraceLog::WriteLine("ChatRoom::Deliver(), %s", msg.Body() );
- /// add the msg to queue
- RecentMessages_.push_back( msg );
- /// check the length
- while( RecentMessages_.size() > MaxRecentMsgs )
- {
- RecentMessages_.pop_front();
- }
- /// deliver the msg to clients
- std::for_each(Participants_.begin(), Participants_.end(),
- boost::bind( &ChatParticipant::Deliver, _1, boost::ref(msg) ) );
- }
开始单元测试
由于到手了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了啊?