- 背景
- 什么是MOCK
- Mock的定义
- Mock框架简介
- Mock在单测中的应用
- Define Interface
- Define a Model Class
- Create a Mock Object
- Adding Behavior
- Specifying Return Values
- 什么是MockServer
- MockServer的工作原理
- 一般桩程序的结构
- MockServer的结构
- MockServer的技术实现
- 一个CASE
- 核心代码
- 条件与结果
- 行为描述
- 远程调用
- MockServer的工作原理
- 综述
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);
}
创建SOCKET,监听所需的端口
WHILE NOT STOP:
从socket中读数据
IF 数据满足[条件] THEN
返回[结果数据]
END IF
END WHILE
创建SOCKET,监听所需的端口
WHILE NOT STOP:
从socket中读数据
执行[MOCK行为]
END IF
END WHILE
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抛弃了死板的配置文件,将要行为描述与接口实现分离,更利于代码的复用,进一步简化桩程序的开发成本。