MockServer的测试思想与实现

转载:http://blog.csdn.net/shen1936/article/details/50298901

  • 背景
  • 什么是MOCK
    • Mock的定义
    • Mock框架简介
    • Mock在单测中的应用
      • Define Interface
      • Define a Model Class
      • Create a Mock Object
      • Adding Behavior
      • Specifying Return Values
  • 什么是MockServer
    • MockServer的工作原理
      • 一般桩程序的结构
      • MockServer的结构
    • MockServer的技术实现
      • 一个CASE
      • 核心代码
      • 条件与结果
      • 行为描述
      • 远程调用
  • 综述


背景 

在Linux后台服务类模块测试中,经常会遇到被测模块需要通过socket接口调用其它模块的情况,多数时候,我们可以直接连接被调用的模块来进行测试。但有时这并不是个好主意,比如被调用的模块部署成本很高、操作比较繁琐、数据构造困难、性能不够好等,更重要的是一些接口的异常情况可能根本无法直接模拟。 

所以在实际测试中,我们少不了要自己编写一些桩程序来模拟被调用模块的行为。而当我们写过几个桩程序后就会发现,所有的桩程序都大同小异,只是具体的接口协议不同而已,而像链接管理、配置管理、日志管理等工作,完全都是一样的。那么是否可以通过某种方式,将相同的部分抽离出来,构造一个桩程序时,只需要考虑接口的逻辑,是不是就可以节省许多重复的工作呢? 

什么是MOCK 

Mock原本是一种在单测中使用的测试技术。 

Mock的定义 

“Mock Objects simulate parts of the behavior of domain code, and are able to check whether they are used as defined. Domain classes can be tested in isolation by simulating their collaborators with Mock Objects.”。前面这段话摘自EasyMock的说明文档,简单来说单测CASE可以认为是一些驱动代码,而Mock Object则像是一些桩。 

Mock框架简介 

在实际使用中,自己从头实现一个Mock对象是件繁琐的工作,而且经常出现各种低级错误,影响实际使用的效果。所以一些通用的Mock框架应运而生。如 “EasyMock”、“GMock”等。 

“Hand-writing classes for Mock Objects is not needed. 
Supports refactoring-safe Mock Objects: test code will not break at runtime when renaming methods or reordering method parameters 
Supports return values and exceptions. 
Supports checking the order of method calls, for one or more Mock Objects. ” 

上面是EasyMock文档中提到的Mock框架带来的几点好处,总结一下就是:可以使用一种非常简洁的方式描述对象的行为,而不需要真的去实现它。 

关于EasyMock的详细信息,可以参考 EasyMock的使用文档 

Mock在单测中的应用 

使用Mock进行单测,大体上有以下几个方面(摘自EasyMock官方文档) 

 

Define Interface

 

public interface Collaborator { 
void documentAdded(String title);
void documentChanged(String title);
void documentRemoved(String title);
byte voteForRemoval(String title);
byte[] voteForRemovals(String[] title);
}

 

Define a Model Class

 

public class ClassUnderTest { 
// ...
public void addListener(Collaborator listener) { // ... }
public void addDocument(String title, byte[] document) { // ... }
public boolean removeDocument(String title) { // ... }
public boolean removeDocuments(String[] titles) { // ... }
}

 

Create a Mock Object

 

protected void setUp() {
mock = createMock(Collaborator.class); // 1
classUnderTest = new ClassUnderTest();
classUnderTest.addListener(mock);
}

public void testRemoveNonExistingDocument() {
// 2 (we do not expect anything)
replay(mock); // 3
classUnderTest.removeDocument("Does not exist");
}

 

Adding Behavior

 

public void testAddDocument() {
mock.documentAdded("New Document"); // 2
replay(mock); // 3
classUnderTest.addDocument("New Document", new byte[0]);
}

 

Specifying Return Values

 

public void testVoteForRemoval() {
mock.documentAdded("Document"); // expect document addition
// expect to be asked to vote for document removal, and vote for it
expect(mock.voteForRemoval("Document")).andReturn((byte) 42);
mock.documentRemoved("Document"); // expect document removal
replay(mock);
classUnderTest.addDocument("Document", new byte[0]);
assertTrue(classUnderTest.removeDocument("Document"));
verify(mock);
}


什么是Mock Server 

前面说了好多什么是Mock,或者说什么是Mock Object,那什么是Mock Server呢?其实它相对于我们一直使用的桩程序来说的,为了方便,下文将其称为Stub Server。 

MockServer的工作原理 

一般桩程序的结构 

首先,我们先回顾一下以往的Stub Server是什么样的 

Stub Server,作为一个模拟下游模块的行为的程序,它的功能无非两方面,一个是接受请求,一个是返回结果。当然为测试的便利,在返回结果时可能还会有一些简单的逻辑,比如填充一些无关字段。 

对于一个标准的Stub Server,它的结构通常是: 

 

创建SOCKET,监听所需的端口
WHILE NOT STOP:
从socket中读数据
IF 数据满足[条件] THEN
返回[结果数据]
END IF
END WHILE



为了适应的不同的被测系统,Stub Server需要跟据情况实现不同的接口协议,对数据包进行解析和封装,这部分的工作量占据了很大一部分比重,且实际代码往往单调繁琐。 这个问题可以利用一些代码自动生成技术或接口定义语言来解决,这方面的话题不在本文中讨论了。

接口实现好了,接下就是跟据需要来返回相应的数据了,对应于前面的流程,其实就是替换其中的[条件]和[结果数据],以适应不同的CASE。那么如何描述 [条件]和[结果数据]自然也就是成了接下来要解决的问题,有时我们会直接将它们硬编码到程序里,或者使用配置文件来描述[条件]和[结果数据]以触发程序中不同的处理代码。如果数据的结构比较简单,这种方法是很好实现的,但如果数据结构比较复杂,那配置文件格式的设计与数据的解析加载都会是件很烦人的事情,而如果想在配置中添加一些简单的逻辑以使程序有更大的灵活性,则更是一件烦上加烦的事情。 

MockServer的结构 

MockServer的设计思想在于将接口的操作和数据的操作分离,在实现桩程序时,只考虑对各种通信接口的包装,而将[条件]和[结果数据]的构造交给使用者。这样,同样一个桩程序,只要是基于相同的通信协议,就可以模拟出任意的行为,就像mock对象可以模拟任意对象的行为一样。 

比如一个基于socket的Mock Server的可以描述为: 

 

创建SOCKET,监听所需的端口
WHILE NOT STOP:
从socket中读数据
执行[MOCK行为]
END IF
END WHILE


其中MOCK行为可能是这样描述的: 

 

ON: 
recieve('HELLO')
DO:
sendback('WORLD')
keep_alive()
ON:
recieve('QUIT')
DO:
close_link()

可见,Mock Server的核心就是如果实现执行[MOCK行为]

   目前,笔者已经用Python实现了一个基于socket接口的Mock Server并在测试中进行了一定的应用,实现中利用了一些Python的语言特性、一点RPC技术和一点DSL的技巧。

 

 一个CASE

下面先看一个实际的CASE,CASE加入了许额外的注释,以解释这段代码的意义 (CASE的格式为一种可嵌入Python代码的DSL脚本)

#定义用例集
CASE DS返回结果异常测试
BEGIN
    #定义用例的公共数据,后面的用例中都可以引用该数据,且互不干扰
 GROUP_ID=55
    #定义用例的公共入口动作,相关CASE的MAIN函数
 __exec__=BEGIN
  PYTHON mock_execute(r"""${MOCK_DATA}""".strip()) #设置MockServer的行为
  PYTHON request(host='${HOST}',port=${PORT},id=${MEDIA_ID},group=${GROUP_ID},time=${TIME},type='${TYPE}',cached=${CACHED},cache_flag='${CACHE_FLAG}') #向被测模块发送一条请求
 END
    #定义用例
 CASE 测试 1
 BEGIN
       #定义用例数据,描述MockServer的行为 
  MOCK_DATA=BEGIN
   Mock.on(
    large_than(192),     #当接收的数据长度大于192字节时(一个正常请求的最小长度)
   ).do(
    send_back(           #返回下面的数据包
     am_head_t(1,0,(c_uint32*2)(100,100),sizeof(ds_qres_head_t)+12+32),
     ds_qres_head_t(0,1,1,32,12,),
     2,4,
     "show",
     47,3,4,12,
     123,"hello,world!"
    ),
    clear_buf(),         #清除接收缓冲区
    clear_mock(),        #请除MockServer的行为
   )
  END
 END
    #定义用例
 CASE 测试 2
 BEGIN
    ……
    ……
    END
    ……
    ……
    ……
    ……
END

 

上面的代码中,request的函数的功能是将各参数拼装成一个HTTP请求发送给被测系统并接收返回的结果。而mock_execute也就是对 Mock Server的调用了。

下面就看看这个Mock Server是如让上面的CASE得以运行的

 

核心代码

首先看Mock Server中的主体代码,该代码基于Python中的ThreadingTCPServer,如下:

while not self.stop and not self.server.stop:
    buf=self.sock.recv(4096)
    if not buf:
        time.sleep(0.1)
    self.buffer+=buf
    for mock in MockRequestHandler.mocks:
        if mock(self):
            break

这段代码可以看作是程序的主循环,它不断的从socket读取代码,然后调用mock对象,触发其中定义的行为。一个mock对象定义了一个行为,程序充许一次定义的多个mock,也有使得程序可以模拟比较复杂的行为了。

下面再看看mock对象里面是如何定义的

 

if reduce(
        lambda x,y:x and y,
        map(lambda f:f(handler),self.on_list),
        True
    ):
    print 'Invoke Mock:',self.on_list,self.do_list
    map(lambda f:f(handler),self.do_list)
    return True
return False

这里用到了一点儿函数式编程的技巧,在on_list中保存了当前mock的触发条件,do_list中则保存了当前mock要执行的操作,这段代码的就意思就是当所有的触发条件都满足时,就顺序执行操作列表的中的操作,否则就退出。

上面两段代码就构成了Mock Server的核心逻辑。

条件与结果

接下来我们看看on_list和do_list里面到底是什么,以前面的CASE中用到的large_than和send_back为例,它们的原始定义如下所示:

@mock_action
def large_than(handler,size):

    return len(handler.buffer)>=size

@mock_action
def send_back(handler,data):

    return handler.sock.sendall(data)

可以看到,它们的第一参数都是handler,它代表了当前请求处理器的实例,包含以下几个基本的成员:

  • client_address 当前请求的客户端地址
  • sock 当前请求的socket对象
  • buffer 当前请求的数据缓冲区
  • stop 主程序停止标志

对照 核心代码 中的主循环,这里的handler就是循环中的self,这样就不难明白这几个成员的作用了。

在on_list和do_list中保存就是对这些函数的“间接”引用。那什么是间接引用呢?

回头看前面CAE中的代码:

Mock.on(
    large_than(192),     #当接收的数据长度大于192字节时(一个正常请求的最小长度)
).do(
    send_back(           #返回下面的数据包
        am_head_t( ……
        ……
    )
)

看以看到这里并没有给large_than和send_back传入handler参数,而且,熟悉Python语法的人也会发现,这是对函数的调用,而不是对函数对象本身的引用,最终on方法得到的参数应该是large_than执行完的结果,不是large_than这个函数对象。说了这么多,好像很混乱的样子,其实密秘就在

@mock_action

中,这个是python中的函数修饰器,它本身也是一个函数,接受一个函数对象为参数,返回一个新的函数对象,来看看mock_action的定义:

 

def mock_action(f):

    def factor(*args):

        action=lambda h:f(h,*args)
        action.__name__=f.__name__+'_action' #调试信息,暂时无用
        return action

    factor.is_mock_action=True               #暂时无用
    factor.__name__=f.__name__+'_factor'     #调试信息,暂时无用
    return factor

 

它实际上对是原始的函数进行一次封装,有点类似函数式编程中的高阶函数,简单来说就是将开始那段函数定义的代码等价于下面的代码:

 

def large_than(size):

    def large_than_func(handler):
        return len(handler.buffer)>=size
    return large_than_func

def send_back(data):

    def send_back_func(handler):
        return handler.sock.sendall(data)
    return send_back_func

 

当然也可以在定义Mock行为时写成这样:

Mock.on(
    lambda handler:large_than(handler,192)
).do(
    lambda handler:send_back(handler, …… )
)

不过这个就有点儿太难看了。

行为描述

前面说了好多mock里存什么,现看看这此东西是怎么存进去的,来一段更有代表性的代码:

Mock.on(
        any_package(),
        large_than(32),
    ).do(
        send_back('hello,world! come on ....'),
        clear_buf(),
    ).on(
        got('QUIT\n'),
    ).do(
        close_sock(),
    ).on(
        got('STOP\n'),
    ).do(
        stop_server(),
    )

每一组on|do调用都定义了一个新的mock,上面的代码中定义了三个mock,那么如何能保证on|do能成对出现,且不符合约定时能抛出异常呢?

其实上面的代码可看作是一段DSL的代码,我们在Python的语法基础上,添加了on/do两个关键字,并做了一定的语法限定。而在代码中实现on和 do时也进行相应的处理,以保证语法约定的正确性。

 

class MockRequestHandler(SocketServer.BaseRequestHandler):

    mocks=[]

    class MockObjectProxy(object):

        def __init__(self,handle,obj):

            self.__handle=handle
            self.__obj=obj

        def do(self,*funcs):

            self.__handle.mocks.append(self.__obj.do(*funcs))
            print self.__obj.on_list,self.__obj.do_list
            return self.__handle

    @staticmethod
    def on(*funcs):

        return MockRequestHandler.MockObjectProxy(
            MockRequestHandler,
            MockObject().on(*funcs)
        )

 

通过上面的代码就限制了do必须出现on后面,否则会提示Mock对象不支持do方法。同时如果只有on没有do,也不会创建新的mock。

远程调用

现在,我们的Mock Server已经可以启动运行了,但必须且只能在启动时指定mock行为,也就是说还不能动态更新配置。

接下来就该RPC登场了,RPC是远程过程调用的简称,这里的远程指的是不同的进程,可能是同一台机器上的,也可能是位于不同机器上的,它们之间可以通过某种PIC(进程间通信)协议传递信息,比如socket。而RPC就是对PIC协议的再封装,把信息发送/接收的过程变成更简单易用的函数调用过程。

本例中使用Python的第三方扩展库rpyc来实现RPC,这样就可以在CASE中动态的修改Mock Server的形为了。

 

def execute (code,service_name='MOCK_SERVER'):

    conn=get_online_connectiones(service_name)[-1]
    conn.root.execute(code)

if __name__=='__main__':

    execute(r"""Mock.on( any_package(), large_than(32), ).do( send_back('hello,world! come on ....'), clear_buf(), ).on( got('QUIT\n'), ).do( close_sock(), ).on( got('STOP\n'), ).do( stop_server(), )""")
    raw_input()

 

在最开始的CASE中的mock_exectue函数,实际上就是对这里的execute的再包装而已。

综述

总结一下我们所实现的这个Mock Server的特点:

  • 用事件驱动的方式描述行为
  • 用函数来描述[条件]和[结果]
  • 用DSL代替配置文件的解析
  • 用RPC代替配置文件的分发和加载

相比之传统定义上的Stub Server, Mock Server抛弃了死板的配置文件,将要行为描述与接口实现分离,更利于代码的复用,进一步简化桩程序的开发成本。

posted @ 2017-11-23 13:59  小学生II  阅读(755)  评论(0编辑  收藏  举报