在 ASP.NET Core 中测试 gRPC 服务

作者:James Newton-King

测试是构建稳定且可维护的软件的一个重要方面。 本文介绍如何测试 ASP.NET Core gRPC 服务。

有三种常见的测试 gRPC 服务的方法:

  • 单元测试:直接从单元测试库测试 gRPC 服务。
  • 集成测试:gRPC 应用托管在 TestServerMicrosoft.AspNetCore.TestHost 包中的内存中测试服务器)中。 通过单元测试库中的 gRPC 客户端调用 gRPC 服务来对其进行测试。
  • 手动测试:使用即席调用测试 gRPC 服务器。 有关如何将命令行和 UI 工具与 gRPC 服务结合使用的信息,请参阅在 ASP.NET Core 中使用 gRPCurl 测试 gRPC 服务

在单元测试中,只涉及 gRPC 服务。 必须模拟注入到服务中的依赖项。 在集成测试中,gRPC 服务及其辅助基础结构是测试的一部分。 这包括应用启动、依赖项注入、路由和身份验证以及授权。

示例可测试服务

若要演示服务测试,请在示例应用中查看以下服务。

查看或下载示例代码如何下载

TesterService 使用 gRPC 的四种方法类型返回问候语。

public class TesterService : Tester.TesterBase
{
    private readonly IGreeter _greeter;

    public TesterService(IGreeter greeter)
    {
        _greeter = greeter;
    }

    public override Task<HelloReply> SayHelloUnary(HelloRequest request,
        ServerCallContext context)
    {
        var message = _greeter.Greet(request.Name);
        return Task.FromResult(new HelloReply { Message = message });
    }

    public override async Task SayHelloServerStreaming(HelloRequest request,
        IServerStreamWriter<HelloReply> responseStream, ServerCallContext context)
    {
        var i = 0;
        while (!context.CancellationToken.IsCancellationRequested)
        {
            var message = _greeter.Greet($"{request.Name} {++i}");
            await responseStream.WriteAsync(new HelloReply { Message = message });

            await Task.Delay(1000);
        }
    }

    public override async Task<HelloReply> SayHelloClientStreaming(
        IAsyncStreamReader<HelloRequest> requestStream, ServerCallContext context)
    {
        var names = new List<string>();

        await foreach (var request in requestStream.ReadAllAsync())
        {
            names.Add(request.Name);
        }

        var message = _greeter.Greet(string.Join(", ", names));
        return new HelloReply { Message = message };
    }

    public override async Task SayHelloBidirectionalStreaming(
        IAsyncStreamReader<HelloRequest> requestStream,
        IServerStreamWriter<HelloReply> responseStream,
        ServerCallContext context)
    {
        await foreach (var request in requestStream.ReadAllAsync())
        {
            await responseStream.WriteAsync(
                new HelloReply { Message = _greeter.Greet(request.Name) });
        }
    }
}

前面的 gRPC 服务:

单元测试 gRPC 服务

单元测试库可以通过调用其方法直接测试 gRPC 服务。 单元测试单独测试 gRPC 服务。

[Fact]
public async Task SayHelloUnaryTest()
{
    // Arrange
    var mockGreeter = new Mock<IGreeter>();
    mockGreeter.Setup(
        m => m.Greet(It.IsAny<string>())).Returns((string s) => $"Hello {s}");
    var service = new TesterService(mockGreeter.Object);

    // Act
    var response = await service.SayHelloUnary(
        new HelloRequest { Name = "Joe" }, TestServerCallContext.Create());

    // Assert
    mockGreeter.Verify(v => v.Greet("Joe"));
    Assert.Equal("Hello Joe", response.Message);
}

前面的单元测试:

  • 使用 Moq 模拟 IGreeter
  • 使用请求消息和 ServerCallContext 来执行 SayHelloUnary 方法。 所有服务方法均具有 ServerCallContext 参数。 在此测试中,使用 TestServerCallContext.Create() 帮助程序方法提供类型。 此帮助程序方法包含在示例代码中。
  • 做出断言:
    • 验证请求名称是否传递给 IGreeter
    • 服务返回预期的答复消息。

gRPC 方法中的单元测试 HttpContext

gRPC 方法可使用 ServerCallContext.GetHttpContext 扩展方法访问请求的 HttpContext。 若要对使用 HttpContext 的方法进行单元测试,必须在测试设置中配置上下文。 如果未配置 HttpContext,则 GetHttpContext 返回 null

若要在测试设置期间配置 HttpContext,请创建一个新实例并使用 __HttpContext 键将其添加到 ServerCallContext.UserState 集合中。

var httpContext = new DefaultHttpContext();

var serverCallContext = TestServerCallContext.Create();
serviceCallContext.UserState["__HttpContext"] = httpContext;

使用此调用上下文执行服务方法以使用配置的 HttpContext 实例。

集成测试 gRPC 服务

与单元测试相比,集成测试可在更广泛的级别上评估应用的组件。 gRPC 应用托管在 TestServerMicrosoft.AspNetCore.TestHost 包中的内存中测试服务器)中。

单元测试库启动 gRPC 应用,然后使用 gRPC 客户端测试 gRPC 服务。

示例代码包含可实现集成测试的基础结构:

  • GrpcTestFixture<TStartup> 类配置 ASP.NET Core 主机,并在内存中测试服务器中启动 gRPC 应用。
  • IntegrationTestBase 类是集成测试继承自的基类型。 它包含用于创建 gRPC 客户端以调用 gRPC 应用的固定例程状态和 API。
[Fact]
public async Task SayHelloUnaryTest()
{
    // Arrange
    var client = new Tester.TesterClient(Channel);

    // Act
    var response = await client.SayHelloUnaryAsync(new HelloRequest { Name = "Joe" });

    // Assert
    Assert.Equal("Hello Joe", response.Message);
}

前面的集成测试:

  • 使用 IntegrationTestBase 提供的通道创建 gRPC 客户端。 此类型包含在示例代码中。
  • 使用 gRPC 客户端调用 SayHelloUnary 方法。
  • 断言服务返回预期的答复消息。

注入模拟依赖项

在固定例程上使用 ConfigureWebHost 来重写依赖项。 当外部依赖项在测试环境中不可用时,重写依赖项很有用。 例如,使用外部支付网关的应用不应在执行测试时调用外部依赖项。 请改用模拟网关进行测试。

public MockedGreeterServiceTests(GrpcTestFixture<Startup> fixture,
    ITestOutputHelper outputHelper) : base(fixture, outputHelper)
{
    var mockGreeter = new Mock<IGreeter>();
    mockGreeter.Setup(
        m => m.Greet(It.IsAny<string>())).Returns((string s) => $"Test {s}");

    Fixture.ConfigureWebHost(builder =>
    {
        builder.ConfigureServices(
            services => services.AddSingleton(mockGreeter.Object));
    });
}

[Fact]
public async Task SayHelloUnaryTest_MockGreeter()
{
    // Arrange
    var client = new Tester.TesterClient(Channel);

    // Act
    var response = await client.SayHelloUnaryAsync(
        new HelloRequest { Name = "Joe" });

    // Assert
    Assert.Equal("Test Joe", response.Message);
}

前面的集成测试:

  • 在测试类的 (MockedGreeterServiceTests) 构造函数:
    • 使用 Moq 模拟 IGreeter
    • 重写使用 ConfigureWebHost 通过依赖项注入注册的 IGreeter
  • 使用 gRPC 客户端调用 SayHelloUnary 方法。
  • 根据模拟 IGreeter 实例断言预期的答复消息。

其他资源