汪年第一贴:单元测试

为啥是单元测试。

最近手上的东西已经都定了第一个版本了,最近几周一直在做测试的工作。我闲暇时间,放弃了其他的事情,专心在为我的项目做单元测试。以前都是断断续续的弄了点单元测试,这次就结合自己的实际项目,再次实施了一次单元测试。并且有了很深刻的体会。

1.什么是单元测试

单元测试这个概念的话,在网上,一拉一大堆。这里我就不描述了。我只是说说我自己的理解。谈到单元测试,基本就要说到敏捷软件开发,为啥?我个人理解的敏捷软件开发的精髓在于快速迭代和重构,特别是重构。然而在迭代和重构的过程,如何保证之前代码流程和结构不被破坏?这个就需要测试,特别是白盒测试,因为重构过程中,除了要保证流程和逻辑无误之外,有时候,还需要保证中间变量一致。这个基本就是单元测试了。这个后面会有实例说明。

2.单元测试的要求

单元测试其实对团队要求很高的,特别是配合度上面。之前了解过,有的说单元测试是测试来做,有的说的是开发来做。其实无论谁来做,对团队的要求都很高的,这些不仅仅是技能上面要求的,更多的是思想上的。我清楚的记得,在第一次听单元测试的培训的时候,老师就说过,其实单元测试的要求还是比较高的,特别是需要开发有测试的意识,也就是说,开发的代码需要具有可测试性,之前不理解,现在做了以后才明白确实如此。为啥需要具有可测试性,后面讲解。

如果开发自己来做单元测试,那么问题来了,单元测试的代码其实工作量很大的我统计过被测代码和测试的代码的比例至少是1:1.5,而且还不包括打桩的基础代码,那么这个对开发的工作量来说是致命的。如果单元测试由测试来做,那么恭喜你们,对测试和研发的要求很高很高,特别是头的,因为这样就需要测试理解开发的流程,还需要研发配合测试来做,想想都觉得恐怖。

3.单元测试怎么做

我自己东西是用的C来做的,那么我单元测试的工具选用的CPPUTEST-3.8。这里我只是简单阐述下,我自己对这块的理解,和我的做法。每个人对于单元测试理解不同,做法也不同。并且测试,不可能保证测出所有的问题。

简单说来,就是通过编译和链接的手法,将被测代码和测试代码通过不同的编译,链接到一块,将测试和实际的产品工程给分开。举个简单的例子,我有个C/S模式的软件,客户端Clinet 启动以后,会通过TCP连接向服务器端Server发送一个字符串”hello”,然后服务器回应一个”world”。

第一步就是工程布局,这里就是第一个考研需要研发具有其代码可测试性的地方,cpputest具有自己的main函数,也就是说这里需要有3个可执行文件,这里以windows为例,只调用socket接口则可以无缝切换到在linux下,Clinet.exe、Service.exe、unit_test.exe。这里就需要把client和server的main函数给单独分开,才能进行单元测试。

如下图所示,Clinet.o和Client_main.o链接libwinsocket32.a和libwin32.a以后,则可以生成实际的产品Client.exe。而通过Client.o和测试代码中.o文件链接libwin32.a以后,则为测试的unit_test.exe。这里Clinet.exe相比unit_test.exe多链接了libwinsocket.a。那么socket的函数如何处理,我这里是在socket_mock.cpp中实现的。socket_mock.cpp中实现了对又有socket调用的“桩”。在什么地方打桩和如何打桩,在我看来,是整个单元测试的灵魂,换句话说,需要对整个工程有全面的掌握,因为桩很关键,如果桩打不好,不仅测试恼火,后面的维护更恼火。这块后面在说明。


下面是client.c被测代码的实际代码,功能很简单,连接服务器,发送字符串"hello",并且接受服务器的回应。

Clinet.c代码:

#include <winsock2.h>

#include <stdio.h>


static int g_socket_fd = 0;


int init_client()

{

   SOCKADDR_IN addrServer;//服务端地址


   g_socket_fd = socket( AF_INET,SOCK_STREAM,0 );

   addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//目标IP(127.0.0.1是回送地址)

   addrServer.sin_family = AF_INET;

   addrServer.sin_port = htons(6000);//连接端口6000


   connect(g_socket_fd,(SOCKADDR*)&addrServer,sizeof(SOCKADDR));


   return 0;

}


#define BUFFSIZE 1024

int process_client()

{

   unsigned char p_buff[BUFFSIZE];


   snprintf( p_buff, BUFFSIZE, "hello" );

   send( g_socket_fd, p_buff, BUFFSIZE, 0 );


   recv( g_socket_fd, p_buff, BUFFSIZE, 0 );

   return ;

}


void uninit_clinet()

{

   close( g_socket_fd );

   g_socket_fd = -1;

   return ;

}

         被测试代码通过调用posix标准的socket函数发送字符串到服务器端。现在就针对client代码进行测试。第一就需要打桩,将socket的函数封装一次,其代码在socket_mock.cpp 中。对于单元测试,我个人理解桩,实际就是类似进出口的钩子,合理的设置好桩了以后,就可以任意构造自己需要的数据,和返回的流程。在这个例子中通过socket的桩,可以获取每一个自己想要获取的发送数据,也可以构造自己所需要的任意的接收数据。从而可以达到对被测代码逻辑和流程的测试。

#include <string.h>

#include <stdio.h>

#include <assert.h>


int (*socket_mock)(int domain, int type,int protocol) = NULL;

int (*connect_mock)( int sockfd, conststruct sockaddr_t *addr, int addrlen ) = NULL;

int (*send_mock)( int sockfd, const void*buf, int len, int flags ) = NULL;

int (*recv_mock)( int sockfd, void *buff,int len, int flags ) = NULL;


#ifdef __cplusplus

extern "C" {

#endif


int socket( int domain, int type, intprotocol )

{

   if( socket_mock )

    {

       return socket_mock( domain, type, protocol );

    }


   return 0;

}


int inet_addr( char *s )

{

   return 0;

}


int htons( int short )

{

   return 24;

}


int connect( int sockfd, const structsockaddr_t *addr, int addrlen )

{

   if( connect_mock )

    {

       return connect_mock( sockfd, addr, addrlen );

    }

   return 0;

}


int send( int sockfd, const void *buf, intlen, int flags )

{

   if( send_mock )

    {

       return send_mock( sockfd, buf, len, flags );

    }

   return 0;

}


int recv( int sockfd, void *buff, int len,int flags )

{

   if( recv_mock )

    {

       return recv_mock( sockfd, buff, len, flags );

    }


   return 0;

}


#ifdef __cplusplus

}

#endif


void init_socket_mock()

{

   socket_mock = NULL;

   connect_mock = NULL;

   send_mock = NULL;

   recv_mock = NULL;

}


void unini_socket_mock()

{

   socket_mock = NULL;

   connect_mock = NULL;

   send_mock = NULL;

   recv_mock = NULL;

}


         最后我们看下client的测试代码,通过对send_mock函数指针的赋值,可以将clinet.c中的send函数定向到测试的代码,从而可以获取其发送出去的数据,通过对数据的校验达到测试的目的。在这个例子中,我只是简答的对发送出去的数据是否是hello进行判断。但是这里可以测试的case应该是比较多的。

#include <stdio.h>

#include <strings.h>


#include "socket_mock.h"

#include "CppUTest/TestHarness.h"


TEST_GROUP( test_group_clinet )

{

   void setup()

    {

       init_socket_mock();

       init_client();

    }

   void teardown()

    {

       uninit_clinet();

       unini_socket_mock();

    }

};

static const char *p_hellow_string ="hello";

static int first_send( int sockfd, constvoid *buff, int len, int flags )

{

   const char *p_tmp = (const char*)buff;

   if( strncmp( p_tmp,p_hellow_string, strlen(p_tmp) ) )

    {

       FAIL( p_tmp );

    }

   return len;

}

TEST( test_group_clinet, first_test )

{

   send_mock = first_send;

   process_client();


   return ;

}


其运行结果如图。


假如某次,研发在重构时候,将字符串“hello”写错为“hallo”,单元测试运行结果如下图。


这里只是我对单元测试的简单的理解,后面会结合点实际的项目更多的说明。

推荐阅读更多精彩内容