在服务端测试(一)中详细了介绍了服务端测试中的策略和方法论,以及针对服务端测试中稳定性的测试,以及高并发下对服务
的并发请求。不过在企业的应用中,除了REST API的接口,还有gRPC的协议,主要应用于金融以及货币交易等领域,当然不仅仅如此
,gRPC协议是一个高性能,开源和通用的框架,主流的开发语言都提供了对应的API,如Java,Go,Python等语言。gRPC的协议是基
于HTTP/2标准设计(REST API 基本是基于HTTP/1.1设计),可以处理双向流,单路复用等,这些特性在移动端的领域表现更加高效,
而且更加节省空间,背后的技术团队是Google。
在服务端的测试中,不管是什么协议,首先都是客户端与服务端的交互,只是这中间会涉及到很多的协议,如HTTP,WebSocket,以
及今天主要要介绍的gRPC协议,把握住这点后,那么中间的交互我们可以更多的理解为同步交互或者是异步的交互,或者更加精准的说:请
求/响应模式和异步/响应模式,不管那种模式,它最后的本质思想是客户端与服务端之间建立TCP的连接之后,客户端与服务端就会进行交互
(请求/响应模式和异步/响应模式),在测试的角度上而言,客户端与服务端交互后,客户端拿到服务端返回的响应数据,然后针对这些响应
数据进行断言和结果准确性的验证。
在gRPC的协议中,客户端应用程序可以直接调用其他计算机上的服务器应用程序上的方法,就好像它是本地对象一样,从而更容易创建分
布式应用程序和服务。与许多 RPC系统一样,gRPC 基于定义服务的想法,指定可以远程称为其参数和返回类型的方法。在服务器方面,服务
器实现此界面并运行 gRPC 服务器来处理客户端呼叫,如下图所示:
在如上截图中我们看到,使用gRPC的协议,主流的开发语言之间都是可以进行交互。
本文章主要使用Python语言来演示gRPC的应用和针对gRPC协议的接口测试应用。在gRPC的协议中,默认使用protocol buffers作为接口
定义语言,来描述服务端和客户端之间的消息结构信息,如下所示,文件名称为helloworld.proto:
syntax = "proto3"; service Greeter { rpc SayHello(HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
在如上描述了客户端和服务端之间的交互信息,当然这个接口是单向的,除了单向的,还有请求流,应答流,双向流,本文章主要介绍单向的应用
(后续逐步介绍流式的应用)。在如上的文件中,我们可以看到在请求中需要的请求参数是name,请求成功后服务端返回的message,name和message
的字段数据类型都是string。
在Python中,我们首先需要安装第三方的库来操作gRPC的协议,具体安装的库如下:
pip3 install grpcio
pip3 install grpcio-tools
安装成功后,执行如下命令生成pb2文件和pb2_grpc的文件,执行命令为:
python3 -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. helloworld.proto
执行成功后,在当前的目录下就会生成名称为:helloworld_pb2.py和helloworld_pb2_grpc.py的文件,helloworld_pb2_grpc.py文件主要是服务端的内容,
文件内容如下:
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc import helloworld_pb2 as helloworld__pb2 class GreeterStub(object): """Missing associated documentation comment in .proto file.""" def __init__(self, channel): """Constructor. Args: channel: A grpc.Channel. """ self.SayHello = channel.unary_unary( '/Greeter/SayHello', request_serializer=helloworld__pb2.HelloRequest.SerializeToString, response_deserializer=helloworld__pb2.HelloReply.FromString, ) class GreeterServicer(object): """Missing associated documentation comment in .proto file.""" def SayHello(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') def add_GreeterServicer_to_server(servicer, server): rpc_method_handlers = { 'SayHello': grpc.unary_unary_rpc_method_handler( servicer.SayHello, request_deserializer=helloworld__pb2.HelloRequest.FromString, response_serializer=helloworld__pb2.HelloReply.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( 'Greeter', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) # This class is part of an EXPERIMENTAL API. class Greeter(object): """Missing associated documentation comment in .proto file.""" @staticmethod def SayHello(request, target, options=(), channel_credentials=None, call_credentials=None, insecure=False, compression=None, wait_for_ready=None, timeout=None, metadata=None): return grpc.experimental.unary_unary(request, target, '/Greeter/SayHello', helloworld__pb2.HelloRequest.SerializeToString, helloworld__pb2.HelloReply.FromString, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
helloworld_pb2.py的文件内容为:
# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: helloworld.proto """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor.FileDescriptor( name='helloworld.proto', package='', syntax='proto3', serialized_options=None, create_key=_descriptor._internal_create_key, serialized_pb=b' x10helloworld.proto"x1c x0cHelloRequestx12x0c x04namex18x01 x01( "x1d HelloReplyx12x0f x07messagex18x01 x01( 23 x07Greeterx12( x08SayHellox12 .HelloRequestx1ax0b.HelloReply"x00x62x06proto3' ) _HELLOREQUEST = _descriptor.Descriptor( name='HelloRequest', full_name='HelloRequest', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='HelloRequest.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], serialized_start=20, serialized_end=48, ) _HELLOREPLY = _descriptor.Descriptor( name='HelloReply', full_name='HelloReply', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='message', full_name='HelloReply.message', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], serialized_start=50, serialized_end=79, ) DESCRIPTOR.message_types_by_name['HelloRequest'] = _HELLOREQUEST DESCRIPTOR.message_types_by_name['HelloReply'] = _HELLOREPLY _sym_db.RegisterFileDescriptor(DESCRIPTOR) HelloRequest = _reflection.GeneratedProtocolMessageType('HelloRequest', (_message.Message,), { 'DESCRIPTOR' : _HELLOREQUEST, '__module__' : 'helloworld_pb2' # @@protoc_insertion_point(class_scope:HelloRequest) }) _sym_db.RegisterMessage(HelloRequest) HelloReply = _reflection.GeneratedProtocolMessageType('HelloReply', (_message.Message,), { 'DESCRIPTOR' : _HELLOREPLY, '__module__' : 'helloworld_pb2' # @@protoc_insertion_point(class_scope:HelloReply) }) _sym_db.RegisterMessage(HelloReply) _GREETER = _descriptor.ServiceDescriptor( name='Greeter', full_name='Greeter', file=DESCRIPTOR, index=0, serialized_options=None, create_key=_descriptor._internal_create_key, serialized_start=81, serialized_end=132, methods=[ _descriptor.MethodDescriptor( name='SayHello', full_name='Greeter.SayHello', index=0, containing_service=None, input_type=_HELLOREQUEST, output_type=_HELLOREPLY, serialized_options=None, create_key=_descriptor._internal_create_key, ), ]) _sym_db.RegisterServiceDescriptor(_GREETER) DESCRIPTOR.services_by_name['Greeter'] = _GREETER # @@protoc_insertion_point(module_scope)
下面我们来编写服务端的代码,具体实现代码如下:
#!/usr/bin/env python #!coding:utf-8 import grpc import helloworld_pb2 import helloworld_pb2_grpc from concurrent import futures import time import os class HelloWorldServicer(helloworld_pb2_grpc.GreeterServicer): def SayHello(self,request,context): '''客户端与服务端之间进行交互''' return helloworld_pb2.HelloReply(message='Hello {0}'.format(request.name)) def serve(): server=grpc.server(futures.ThreadPoolExecutor(max_workers=os.cpu_count())) helloworld_pb2_grpc.add_GreeterServicer_to_server(HelloWorldServicer(),server) #指定服务端的测试地址信息 server.add_insecure_port('[::]:50051') server.start() try: while True: time.sleep(60*60*24) except KeyboardInterrupt: server.stop(0) if __name__ == '__main__': serve()
不管是客户端的代码还是服务端的代码,都需要导入helloworld_pb2和helloworld_pb2_grpc。
我们在上面说到,不顾任何协议之间的通信,都是客户端与服务端之间的通信,那么对我们而言,在测试的时候需要清楚是什么协议,清楚协议
后就模拟具体的协议编写客户端的代码与服务端之间进行通信,如本案例是gRPC的协议,下来在进行测试的时候,我们编写gRPC的协议模拟客户
端来发送请求,客户端实现的源码如下:
#!/usr/bin/env python #!coding:utf-8 import grpc import helloworld_pb2 import helloworld_pb2_grpc import requests def getData(): r=requests.get(url='https://api.muxiaoguo.cn/api/QqInfo?qq=2839168630') return r.text def run(): #连接服务端的程序 channel=grpc.insecure_channel('localhost:50051') #调用rcp的服务 stub=helloworld_pb2_grpc.GreeterStub(channel) r=stub.SayHello(helloworld_pb2.HelloRequest(name=getData())) print('server response data:{0}'.format(r.message)) if __name__ == '__main__': run()
当然我们在进行数据交互的时候,更多的是拿到目标的数据,再由客户端发送给服务端,如本案例拿到QQ的信息后,再通过参数name发送给服务
端,下来启动服务端的程序和客户端
的程序,客户端就会接收到服务端返回的数据信息,如下所示:
如上可以看到客户端发送请求后,服务端通过message把数据返回给了客户端。
感谢您的阅读。第十期服务端测试开发招生中,欢迎加我微信私聊!