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,它代表了当前请求处理器的实例,包含以下几个基本的成员:
对照 核心代码 中的主循环,这里的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的特点:
相比之传统定义上的Stub Server, Mock Server抛弃了死板的配置文件,将要行为描述与接口实现分离,更利于代码的复用,进一步简化桩程序的开发成本。
作者:qabloger
(全文完)
【本文首发于:百度测试技术空间】http://hi.baidu.com/baiduqa/blog/item/450f19dc77915954ccbf1a4d.html