1. 程式人生 > >C++單元測試一:並非看上去那麼簡單——幾個很實際的問題

C++單元測試一:並非看上去那麼簡單——幾個很實際的問題

理想與現實

為Java和C#做單元測試,基本上都有比較統一的工具、模式可用,IDE的支援也非常到位;可是到了C++這裡,一切就變的那樣的“不走尋常路”,各種單元測試框架盛行,例如CppUnit, CppUnitLite, CxxUnit,Google Test等等,以及微軟在VS2012中也加入了對原生C++程式碼單元測試的支援MSTest。面對如此諸多的測試框架,選擇哪一個其實無所謂,只要順手就好,用習慣了,什麼都好;因為,單元測試的程式碼總歸還是要自己來寫——無論你用哪一個框架。

以前寫過不少程式碼,可是都沒怎麼注意單元測試,現在終於認真對待起來,就開始在網路上搜尋資料,看看各種框架下的單元測試如何寫。幸運的是,這方面的資料真不少,很多框架也都會帶有示例或者文件告訴你怎麼寫;不幸的是,這些文件或者示例都太遠離工程實踐,他們一般遵循一個這樣的模式:寫出一個待測類CMyClass,定義一定的成員變數和方法,然後給出針對CMyClass的測試類如CMyClassTest;呵呵,看起來示例是夠好了,可是很少涉及到這樣的實際問題: 

  • 工程實踐中,一個專案往往有很多類,而且相互之間,總有這或多或少的依賴、包含等關係;
  •  實際的測試專案,該如何組織被測程式碼和測試程式碼(注意,這裡所說的組織不是指單元測試資料如何組織,而是指工程中的測試程式碼和產品程式碼的組織)
  •  被測程式碼如何引入測試工程中
  •  一個測試工程如何測試多個被測類

實際的程式碼

好吧,我們來看一下一個“較為”實際的工程以及我在為該工程編寫單元測試過程中所遇到的問題;該工程的程式碼來自於boost.asio中的一個示例程式碼:

我把該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) ) );
}
 


開始單元測試

由於到手了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了啊?