在 ASP.NET Core 中测试 gRPC 服务

作者:James Newton-King

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

有三种常见的测试 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();
serverCallContext.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) =>
        {
            if (string.IsNullOrEmpty(s))
            {
                throw new ArgumentException("Name not provided.");
            }
            return $"Test {s}";
        });

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

[Fact]
public async Task SayHelloUnaryTest_MockGreeter_Success()
{
    // 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 实例断言预期的答复消息。

其他资源