MockServer的测试思想与实现(下篇)

MockServer的技术实现

目前,笔者已经用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抛弃了死板的配置文件,将要行为描述与接口实现分离,更利于代码的复用,进一步简化桩程序的开发成本。

作者:qabloger

(全文完)

 【本文首发于:百度测试技术空间http://hi.baidu.com/baiduqa/blog/item/450f19dc77915954ccbf1a4d.html

关注百度技术沙龙

你可能感兴趣的:(思想,测试,实现,休闲,MockServer)