Compartilhar via


Testar os serviços do gRPC no ASP.NET Core

Por: James Newton-King

O teste é um aspecto importante da criação de software estável e de manutenção. Este artigo discute como testar os serviços gRPC do ASP.NET Core.

Há três abordagens comuns para testar os serviços gRPC:

  • Teste de unidade: teste os serviços gRPC diretamente de uma biblioteca de testes de unidade.
  • Teste de integração: o aplicativo gRPC é hospedado no TestServer, um servidor de teste na memória do pacote Microsoft.AspNetCore.TestHost. Os serviços gRPC são testados chamando-os usando um cliente gRPC de uma biblioteca de teste de unidade.
  • Teste manual: teste servidores gRPC com chamadas ad hoc. Para obter informações sobre como usar ferramentas de linha de comando e UI com serviços gRPC, veja Testar serviços gRPC com gRPCurl e gRPCui no ASP.NET Core.

No teste de unidade, somente o serviço gRPC está envolvido. As dependências injetadas no serviço devem ser simuladas. No teste de integração, o serviço gRPC e sua infraestrutura auxiliar fazem parte do teste. Isso inclui a inicialização de aplicativo, injeção de dependência, roteamento e autenticação e autorização.

Exemplo de serviço testável

Para demonstrar testes de serviço, examine o serviço a seguir no aplicativo de exemplo.

Exibir ou baixar código de exemplo (como baixar)

O TesterService retorna saudações usando os quatro tipos de método do 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) });
        }
    }
}

O serviço gRPC anterior:

Serviços gRPC de teste de unidade

Uma biblioteca de teste de unidade pode testar diretamente os serviços gRPC chamando seus métodos. Os testes de unidade testam um serviço gRPC isoladamente.

[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);
}

O teste de unidade anterior:

  • Simula IGreeter usando Moq.
  • Executa o método SayHelloUnary com uma mensagem de solicitação e um ServerCallContext. Todos os métodos de serviço têm um argumento ServerCallContext. Neste teste, o tipo é fornecido usando o método auxiliar TestServerCallContext.Create(). Esse método auxiliar está incluído no código de exemplo.
  • Faz asserções:
    • Verifica se o nome da solicitação é passado para IGreeter.
    • O serviço retorna a mensagem de resposta esperada.

Teste de unidade HttpContext em métodos gRPC

Os métodos gRPC podem acessar um HttpContext da solicitação usando o método de extensão ServerCallContext.GetHttpContext. Para testar uma unidade de um método que usa HttpContext, o contexto deve ser definido na configuração de teste. Se HttpContext não estiver configurado, GetHttpContext retornará null.

Para configurar um HttpContext durante a instalação do teste, crie uma nova instância e adicione-a à coleção ServerCallContext.UserState usando a chave __HttpContext.

var httpContext = new DefaultHttpContext();

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

Execute os métodos de serviço com esse contexto de chamada para usar a instância HttpContext configurada.

Serviços gRPC de teste de integração

Os testes de integração avaliam os componentes de um aplicativo em um nível mais amplo do que os testes de unidade. O aplicativo gRPC é hospedado no TestServer, um servidor de teste na memória do pacote Microsoft.AspNetCore.TestHost.

Uma biblioteca de teste de unidade inicia o aplicativo gRPC e, em seguida, os serviços gRPC são testados usando o cliente gRPC.

O código de exemplo contém a infraestrutura para possibilitar o teste de integração:

  • A classe GrpcTestFixture<TStartup> configura o host ASP.NET Core e inicia o aplicativo gRPC em um servidor de teste na memória.
  • A classe IntegrationTestBase é o tipo base do qual os testes de integração herdam. Ela contém o estado do acessório e as APIs para criar um cliente gRPC para chamar o aplicativo gRPC.
[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);
}

O teste de integração anterior:

  • Cria um cliente gRPC usando o canal fornecido por IntegrationTestBase. Esse tipo está incluído no código de exemplo.
  • Chama o método SayHelloUnary usando o cliente gRPC.
  • Declara o serviço que retorna a mensagem de resposta esperada.

Injetar dependências fictícias

Use ConfigureWebHost no acessório para substituir dependências. A substituição de dependências é útil quando uma dependência externa não está disponível no ambiente de teste. Por exemplo, um aplicativo que usa um gateway de pagamento externo não deve chamar a dependência externa ao executar testes. Em vez disso, use um gateway fictício para o teste.

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);
}

O teste de integração anterior:

  • No construtor (MockedGreeterServiceTests) da classe de teste:
    • Simula IGreeter usando Moq.
    • Substitui o IGreeter registrado com injeção de dependência usando ConfigureWebHost.
  • Chama o método SayHelloUnary usando o cliente gRPC.
  • Declara a mensagem de resposta esperada com base na instância fictícia IGreeter.

Recursos adicionais