以下为作者观点:
几个月前,我职业生涯中第一次接触到 gRPC 服务。在此之前,我一直在使用 SOAP,主要是使用 REST 和 GraphQL API,只听说过 gRPC。实际上,它并不难,你可以轻松地使用它。经过一段时间的尝试和使用后,我决定创建一种 gRPC 测试入门指南。
在本文中,我们将尝试了解 gRPC 的一般概念以及如何使用 gRPC 协议创建测试。为了提供广泛的示例,我决定使用以下客户端创建测试:
-
Postman——对于那些不熟悉代码的人来说很容易理解
-
C# — 我最喜欢的编程语言。适合喜欢 Java 或 C# 的老派人士。是的。对我来说入门比较容易。
-
Python — 当今非常流行的脚本编程语言。
也可能还有其他编程语言,但我决定就此打住。不再介绍。让我们跳到 gRPC 世界……
什么是 gRPC?
根据Wikipedia 的介绍,gRPC 是一个高性能、开源的远程过程调用(RPC),最初由 Google 设计。
编者注,根据百度百科介绍:gRPC是一个现代的开源高性能远程过程调用(RPC)框架,可以在任何环境中运行。它可以高效地连接数据中心内和跨数据中心的服务,支持负载平衡、跟踪、运行状况检查和身份验证。它也适用于分布式计算的最后一英里,将设备、移动应用程序和浏览器连接到后端服务。gRPC最初是由Google创建的,它使用了一个通用的RPC基础设施称为Stubby,用于连接大量微服务在其数据中心内部和之间运行了十多年。2015年3月, Google决定构建Stubby的下一个版本,并将其开源。gRPC现在在许多组织中使用, 谷歌将为从微服务到计算的“最后一英里”的用例提供动力 (移动的、Web和物联网)。
简单来说,它是关于调用远程服务器上的函数/方法以执行某些操作。让我们考虑一个具体的例子:
你想创建一个新用户。
在服务器上我们实现了可调用的特定函数/方法。
我们使用客户端向服务器发送请求并接收响应(成功或不成功)。
现在,我感觉我正在解释客户端-服务器架构的基础知识:),我们的想法实际上是调用服务器上实现的函数。
沟通实际上是如何发生的?
默认情况下,GRPC 使用协议缓冲区。我将其称为契约。这是什么意思?这意味着,服务器和任何客户端都将使用相同的模式进行通信。很酷的是,协议缓冲区是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据。
- 该模式应在以“.proto”为扩展名的文件文本内定义。
- 此文件的第一个构建块是“消息”。将其视为具有我们想要发送或接收的字段的模型。字段结构是
a) 数据类型。预期发送或接收的数据类型。例如,它可能是字符串、布尔值、数字。请阅读有关数据类型的更多信息。
b) 字段名称。你计划发送的字段的名称
c) 订单号。如 1、2、3、…n
- 第二个基本上是我们的服务与 rpc 函数签名。这些函数内部使用消息作为输入和输出参数,grpc 服务希望我们发送消息,我们期望它返回响应给我们
然后,一旦你指定了数据结构,就可以使用协议缓冲区编译器protoc从原型定义生成首选语言的数据访问类。一旦执行请求,它就会自动将其转换为字节并将其发送到服务器。服务器根据实施的逻辑返回响应。从测试的角度来看,可以使用不同的参数调用方法,服务器应该根据你的要求和实施的逻辑做出反应。理论已经足够了:)让我们开始实践吧。
测试服务器
为了简单起见,我提供了一个模拟用户管理系统的简单服务器。听起来很不可思议。它只包含三种方法:)
CreateUser — 创建用户并返回它
GetUsers — 获取所有用户
GetUsersAsArray — 获取所有用户并将其以数组形式返回。
我们想要做的是在开始创建测试之前运行此服务器
先决条件
1.下载GitHub 项目
2.按照自述文件说明运行服务器。步骤很简单
Proto 文件
Proto 文件可以在 GrpcServer →Protos 文件夹中找到
你将在其他客户端中使用此文件。因此,你可能需要复制它或记住它的位置
完成服务器后,我们就可以开始测试了。首先使用 Postman 进行简单的测试
使用 Postman 进行 GRPC 测试
最后我们来到了最有趣的部分。我们将发送请求并创建测试。至少一个:)
测试创建
-
在 Postman 中,我们可以发起一个新的请求并选择“gRPC”。就这么简单。
-
接下来,我们要指定服务器 URL 并导入 .proto 文件。
注意:还有其他与 .proto 文件通信的选项。但在此阶段,我们想简化它。
-
一旦文件导入,可能已经可以在“选择方法”部分看到可用的请求
-
让我们尝试创建一个用户创建请求。选择方法后,我们需要准备一条消息。Postman 已经包含一个非常有用的按钮用于示例生成 — “使用示例消息”。如果不喜欢某些值,可以更改这些值。
注意:本文中我们只使用“CreateUser”方法,但可以尝试其他方法。它也应该有效。
-
最后我们可以使用invoke,新创建的用户将会返回给我们。
-
让我们从请求中进行一个简单的测试。让我们转到脚本并使用代码片段进行状态代码验证。在现实生活中,这还不够,但我们正在考虑概念
-
一旦再次运行测试,将看到结果。
下一步
在上面的例子中,我们仅考虑了简单的场景和非常原始的验证步骤。至于接下来的步骤,你可以考虑以下内容:
-
添加更多断言步骤。仅状态码验证是不够的
-
添加其他案例。我们仅考虑了一种积极情景。使用测试设计技术添加更多情景
-
尝试从 CLI 运行它以运行它们继续集成。使用 Postman CLI 或 Newman(此步骤与我们的主题无关,但记住它总是好的)
接下来,我们将尝试使用 C# 来实现自动化。请稍候...
使用 C# 客户端进行 GRPC 测试
我们将把它分为项目准备和测试设计部分。
项目准备
1.首先,我们将创建一个新的 C# 项目。我使用的是 NET8 目标框架和控制台应用程序。可以使用任何 .CORE/.NET 版本。
2. 在我们的例子中,客户端和测试位于同一个项目中。因此,我们将所有 NuGet 包安装到同一个项目中。为简单起见,我们将示例分为 GRPC 相关包和测试相关包,以防想要分离项目。
GRPC 客户端相关的包有:
“Google.Protobuf”——提供在 gRPC 客户端和服务器之间发送的消息编码和解码所需的 Protobuf 序列化和反序列化功能。
“Grpc.Net.Client” — 此包提供在 .NET 中创建 gRPC 客户端所需的组件。应用程序能够使用 HTTP/2 与 gRPC 服务进行通信。
“Grpc.Tools”——在构建时用于从.proto文件生成 C# 类,定义 gRPC 应用程序中使用的服务契约和消息类型。
测试相关的包有:
“Microsoft.NET.Test.Sdk”——在 .NET 项目中运行测试所需,它与 Visual Studio 和其他 CI/CD 系统集成以促进自动化测试工作流程
“NUnit”——充当允许编写和组织测试的测试框架。它提供断言条件和验证代码正确性的必要工具。
“NUnit3TestAdapter” — 将 NUnit 框架与 Visual Studio 连接起来,允许在 IDE 中无缝发现、执行测试并报告结果。它确保可以直接从 Visual Studio 运行 NUnit 测试。
本项目中安装的附加软件包(可选):
“FluentAssertions”——FluentAssertions 是一个库,它提供了流畅的语法,用于以 TDD/BDD 风格编写断言测试。
“Bogus”——Bogus 是一个用于生成用于测试和原型设计的虚假数据的库。它允许快速轻松地创建真实的测试数据,涵盖各种数据类型和模式。
3. 接下来,我们需要将.proto文件从服务器复制到客户端。
-
创建一个Protos文件夹并添加.proto 文件。
-
验证文件包含以下配置
构建操作 = Protobuf 编译器
gRPC 存根类 = 仅限客户端
-
验证项目属性也设置为客户端
注意:在我们的示例中,我们直接复制了一个.proto文件。这不是唯一一种方式。实际上,可以.proto从某些存储(例如创建 NuGet 包)引用文件。在这种情况下,如果有新版本可用,可以避免文件实现问题。
4. 是时候构建你的项目了。完成后,你可能会发现自动生成的类。我们已准备好创建测试
测试设计
1.首先我们需要建立一个频道和客户端。
[TestFixture]
public class CreateUsersTests
{
private GrpcChannel? _channel;
private User.UserClient? _userClient;
[SetUp]
public void SetUp()
{
// Setup new channel and client for each test
_channel = GrpcChannel.ForAddress(TestContext.Parameters["server"]!);
_userClient = new User.UserClient(_channel);
}
[TearDown]
public void TearDown()
{
// Dispose the channel after each test
_channel?.Dispose();
}
- 我们将使用 NUnit [SetUp] 属性在方法内部执行此操作。这将使我们可以为每个测试单独设置,从而使我们的测试独立
- 如你所见,我们正在使用 GRPC NET 客户端的方法创建通道 -“GrpcChannel.ForAddress(<pass server url> here)”
- 客户端创建来自 GRPC.Protos。在我们的例子中,它是基于.proto文件生成的。示例:users.proto = User.UserClient(_channel);
- 最后我们要关闭一个通道来清理
2.接下来我们实现测试逻辑
[Test]
public async Task CreateUser_WithFullValidData_ShouldBeCreated()
{
//Arrange
UserRequestModel requestModel = new UserFaker().Generate();
//Act
UserResponseModel? response = await _userClient!.CreateUserAsync(requestModel).ResponseAsync;
//Assert
response.Should().NotBeNull();
using (new AssertionScope())
{
response.Email.Should().Be(requestModel.Email);
response.FirstName.Should().Be(requestModel.FirstName);
response.LastName.Should().Be(requestModel.LastName);
response.Age.Should().Be(requestModel.Age);
response.IsDiscount.Should().Be(requestModel.IsDiscount);
response.Id.Should().BeGreaterThan(0);
}
}
安排部分包含请求的准备。我使用 Bogus 库用假数据创建它。但为了简单起见,模型可能看起来像这样
var requestModel = new UserRequestModel
{
Email = "test@test.com",
FirstName = "FirstName",
LastName = "LastName",
//.....Some other properties
};
该模型是基于 proto 消息契约生成的
message UserRequestModel {
string email = 1; //Required
string firstName = 2; //Required
string lastName = 3; //Required
google.protobuf.Int32Value age = 4; //Optional
bool isDiscount = 5; //Required
UserType userType = 6; //Required
}
下一步,我们将调用“CreateUserAsync”函数,该函数也是根据我们提供的合同为你生成的。一个区别是最后添加了“Async”后缀,以表明它是异步函数/方法。
service User {
rpc CreateUser(UserRequestModel) returns (UserResponseModel);
rpc GetUsers(Empty) returns (stream UserResponseModel);
rpc GetUsersAsArray(google.protobuf.Empty) returns (UserListResponse);
}
注意:还有另一个同步方法可用 - “CreateUser”
最后一步是对我们收到的结果进行断言。查看代码示例时,我才意识到我忘了验证状态。但你可以轻松地做到这一点,但获取状态而不是 .ResponseAsync。
3. 测试已准备就绪。可以继续测试其他场景。
可能出现的负面情况
上述方法很容易适用于正面情况。不幸的是,如果尝试执行某些负面情况,GRPC 调用将发生异常并导致测试失败。那么在这种情况下该怎么办?
有几种方法可以解决此问题:
可以使用特定的断言来验证调用是否引发异常
在下面的例子中,我有一个验证年龄边界值的负面案例
var ex = Assert.ThrowsAsync<RpcException>(async () => await _userClient!.CreateUserAsync(requestModel).ResponseAsync);
断言.That(例如!.Status.StatusCode,Is.EqualTo(StatusCode.InvalidArgument));
Assert.That(ex.Status.Detail, Does.Contain(“年龄必须在 18 至 60 岁之间”));
第二种方法需要包装你的代码以便拦截“RpcException”并返回它
这是一个更复杂的方法。无论如何,让我们尝试创建一些简单的实现
首先,让我们创建一个存储结果的模型。
其次,我们需要创建一个函数/方法来捕获异常并返回它
-
简单来说,需要在 try-catch 块内进行调用并首先捕获“RpcException”。
-
无论是成功还是失败,都会返回一个带有状态代码的对象。成功的情况只会返回更多信息
最后,我们可以使用此方法来执行我们的调用
-
准备请求模型
-
使用准备好的方法并从内部调用请求
-
接收结果并执行断言
注意:此代码肯定需要重构。我建议使用“GrpcRequestBase”类作为服务包装器的基类,例如“UserServiceRequests”或类似的东西。
下一步我们将使用 Python 创建一个测试项目。
使用 python 客户端进行 GRPC 测试
与以前一样,我们将把它分为项目准备和测试设计部分。
项目准备
我将使用 Pipenv。因此,项目设置将如下所示
1.如果尚未安装 Python,请先安装
2.安装 pipenv
pip install --upgrade pip
pip install pipenv
3.创建项目目录
mkdir my_grpc_project
cd my_grpc_project
4. 使用已安装的 Python 版本初始化 Pipenv 环境
pipenv -- python 3.8.2
5.激活Pipenv shell
pipenv shell
6.安装GRPC相关包(grpc客户端)
pipenv install grpcio grpcio-tools
7.安装测试相关包
pipenv install pytest pytest-asyncio --dev
8. 应创建 Pip 文件。它应类似于以下内容
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
grpcio = "*"
grpcio-tools = "*"
[dev-packages]
pytest = "*"
pytest-asyncio = "*"
[requires]
python_version = "3.8"
python_full_version = "3.8.2"
9. 定义项目结构。例如:
“protos”——我们将放置 .proto 文件的文件夹
“src”——所有客户端代码都将位于其中
“tests” — 包含测试的文件夹
“utils”——不同的测试助手
生成 Python GRPC 文件并创建客户端
-
要从 .proto 文件生成 Python gRPC 文件,可以使用protoc带有 Python gRPC 插件的编译器。以下是生成它的命令
python -m grpc_tools.protoc -I./protos --python_out=./src/generated --grpc_python_out=./src/generated ./protos/users.proto
此命令将在“src/generated”文件夹中生成 GRPC 客户端文件
-
接下来我们将创建一个非常简单的客户端
- 让我们创建一个文件“grpc_client.py”
- 让我们在里面放一个简单的实现
import grpc
import os
import sys
# Assuming the grpc_client.py is in src/client and the generated files are in src/generated
generated_files_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'generated'))
if generated_files_directory not in sys.path:
sys.path.append(generated_files_directory)
from users_pb2_grpc import UserStub
async def create_grpc_channel(address='localhost:5000'):
"""Create and return a gRPC channel."""
return grpc.aio.insecure_channel(address)
def get_user_client(channel):
"""Create and return a user service client."""
return UserStub(channel)
-
是时候创建测试了
测试设计
1.首先,我们将创建一个测试配置文件,在其中定义频道和用户客户端
导入系统导入操作系统
import sys
import os
import pytest
base_directory = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
client_directory = os.path.join(base_directory, 'src', 'client')
generated_directory = os.path.join(base_directory, 'src', 'generated')
sys.path.append(client_directory)
sys.path.append(generated_directory)
from grpc_client import create_grpc_channel, get_user_client
@pytest.fixture(scope="function")
async def grpc_test_client():
# Create the channel
channel = await create_grpc_channel('localhost:5000') # I'm lazy, so I'm hardcoding the port for this example. It should be read from a config file
# Create the client
client = get_user_client(channel)
# Provide the client to the test and ensure it is awaited properly
try:
yield client
finally:
# Close the channel after the test
await channel.close()
2. 最后我们可以创建测试。
我将添加正面和负面的案例
import pytest
from users_pb2 import UserRequestModel
from google.protobuf.wrappers_pb2 import Int32Value
from utils.randomizers.random_string import generate_random_string
from grpc import StatusCode, RpcError
@pytest.mark.asyncio
async def test_create_user_with_full_valid_data_should_be_created(grpc_test_client):
# Arrange
user_client = grpc_test_client
request_model = UserRequestModel(
email="john@example.com",
firstName="John",
lastName="Doe",
age=Int32Value(value=30),
isDiscount=True,
userType=1
)
# Act
response = await user_client.CreateUser(request_model)
# Assert
assert response is not None
assert response.email == "john@example.com"
assert response.firstName == "John"
assert response.lastName == "Doe"
assert response.age.value == 30
assert response.isDiscount is True
assert response.id > 0
@pytest.mark.asyncio
@pytest.mark.parametrize("chars_number, is_positive", [
(3, True),
(2, False),
(100, True),
(101, False)
])
async def test_create_user_last_name_validation(grpc_test_client, chars_number, is_positive):
# Arrange
user_client = grpc_test_client
request_model = UserRequestModel(
email="test@test.com",
firstName="FirstName",
lastName=generate_random_string(chars_number)
)
# Act and Assert
if is_positive:
# Expect no exception, and user creation should be successful.
response = await user_client.CreateUser(request_model)
assert response is not None # Ensure some response logic if needed
else:
# Expect an exception due to invalid last name length.
with pytest.raises(RpcError) as exc_info:
await user_client.CreateUser(request_model)
assert exc_info.value.code() == StatusCode.INVALID_ARGUMENT
if chars_number == 101:
expected_message = "Last name max length is 100"
else:
expected_message = "Last name min length is 3"
assert expected_message in exc_info.value.details()
注意:我知道这是偷懒的代码。我只是想和你分享一个基本的想法
3.当然,让我们运行测试
-
使用以下命令激活虚拟环境:
pipenv shell
-
使用命令运行测试:
pytest
结语
在本文中,我们简要回顾了如何使用 Postman、NET 和 Python 客户端测试 GRPC 服务。我想指出的是,有些部分没有包括在内,这些部分与发送和接收流作为参数有关。此类流有助于发送和接收任何数据类型的消息。也许这在未来会是一个很好的话题。
行动吧,在路上总比一直观望的要好,未来的你肯定会感 谢现在拼搏的自己!如果想学习提升找不到资料,没人答疑解惑时,请及时加入群: 759968159,里面有各种测试开发资料和技术可以一起交流哦。
最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取 【保证100%免费】
软件测试面试文档
我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。