Testing ASP.NET Core GRPC applications with WebApplicationFactory

5 minute read

In the previous post we used the WebApplicationFactory to test web applications built with ASP.NET Core.
In this post we will use it to test GRPC applications.

GRPC is a framework to create language agnostic, high-performance Remote Procedure Call (RPC) applications. Support to GRPC services was added to ASP.NET Core in version 3.0 and it has been evolving since then.

Being built on top of the ASP.NET Core pipeline, GRPC services leverage the many components that take care of cross-cutting concerns like authentication, authorization, dependency injection, logging and more.

In this post we will assume a basic knowledge of developing GRPC services with the ASP.NET Core framework. Anyway, a clear introduction post is available here.

The service under test

Since GRPC is a contract-first framework, it seems only fitting to start this post introducing the contract behind our service under test.

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

From this simple contract, we can create a GRPC service that inherits from the base class generated by the tooling

public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;

    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }

    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply
        {
            Message = "Hello " + request.Name
        });
    }
}

Finally, here is a peak at the relevant parts of the project file

<ItemGroup>
    <Protobuf Include="..\..\proto\greet.proto" GrpcServices="Server" Link="Protos\greet.proto" />
</ItemGroup>

<ItemGroup>
    <PackageReference Include="Grpc.AspNetCore" Version="2.40.0" />
</ItemGroup>

Notice how we use the <Protobuf /> element to import the protobuf definition of the contract. The GRPC tooling will take care of generating the classes needed to host the service. These classes include the GreaterBase class we inherited from earlier and all the classes representing the strong-typed messages the service is going to receive and return.

You can see the source code of the service, including the Program and Startup here.

Setting up the test project

Because it’s built on ASP.NET Core and it’s mainly based on HTTP/2 requests, we can leverage the WebApplicationFactory to create an instance of our application and use it for our tests.

Theoretically, we could use the HttpClient returned by the WebApplicationFactory.CreateClient method to manually forge the HTTP requests to be sent to the service.

Instead, we will leverage the typed client that GRPC can generate from a protobuf contract.

First of all, let’s create a basic test project based on the NUnit template.

$ dotnet new nunit -o Tests

After the project is created, we can add a reference to the application.

$ dotnet add reference path/to/grpc/application

Then, we add the Microsoft.AspNetCore.Mvc.Testing package:

$ dotnet add package Microsoft.AspNetCore.Mvc.Testing

Then, we modify the project file to include the protobuf contract and the packages needed for the GRPC tooling.

<ItemGroup>
    <Protobuf Include="..\..\proto\greet.proto"
              GrpcServices="Client"
              Link="Protos\greet.proto" />
</ItemGroup>

<ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.19.1" />
    <PackageReference Include="Grpc.Net.Client" Version="2.41.0" />
    <PackageReference Include="Grpc.Tools" Version="2.42.0" PrivateAssets="All" />
</ItemGroup>

In the snippet above, it’s important to notice the value passed to the GrpcServices attribute: Client instructs the tooling to generate the client class and all the classes representing the strong-typed messages the client is going to send to the service and receive.

If we were to try to build the test project now, the build would fail. This is because the project contains both the messages generated by the server and the client. To avoid this ambiguity, we can use aliasing to distinguish the classes coming from the application project from those coming from the test project.

<ItemGroup>
    <ProjectReference Include="..\..\src\grpc\grpc.csproj">
        <Aliases>SUT</Aliases>
    </ProjectReference>
</ItemGroup>

Setting an alias for a project reference forces us to import it in every file we intend to use the classes exposed by that library. This can be done with extern alias.

Writing the first test

Once we have added all the needed dependencies to the test project, it’s time to put everything together and write some actual test.

First of all, we need to instantiate a WebApplicationFactory for the application under test.

Then, we create a GRPC channel that connects to the fake application.

Finally, we can instantiate a typed client that use the channel we just created.

Once we have an instance of the client, we can use it to fire requests to the service as if we were performing a remote call.

[Test]
public async Task ShouldReturnMessageWithName()
{
    var factory = new WebApplicationFactory<SUT::Sample0004.Startup>();

    var options = new GrpcChannelOptions { HttpHandler = factory.Server.CreateHandler() };
    var channel = GrpcChannel.ForAddress(factory.Server.BaseAddress, options);

    var client = new Greeter.GreeterClient(channel);

    var request = new HelloRequest { Name = "Renato" };

    var response = await client.SayHelloAsync(request);

    Assert.That(response.Message, Does.Contain("Renato"));
}

Notice that the we use the alias identifier to identify the Startup class that belongs to the assembly under test.

Reusing the resources

Since both the WebApplicationFactory and the GrpcChannel are very expensive to create and both are meant to be used concurrently, we can reuse them across multiple tests by leveraging NUnit’s tests lifecycle.

extern alias SUT;

// omitted for brevity

[TestFixture]
public class Tests
{
    private GrpcChannel _channel;
    private Greeter.GreeterClient _client;

    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        var factory = new WebApplicationFactory<SUT::Sample0004.Startup>();

        var options = new GrpcChannelOptions { HttpHandler = factory.Server.CreateHandler() };

        _channel = GrpcChannel.ForAddress(factory.Server.BaseAddress, options);
    }

    [SetUp]
    public void Setup()
    {
        _client = new Greeter.GreeterClient(_channel);
    }

    [Test]
    public async Task houldReturnMessageWithName()
    {
        var request = new HelloRequest { Name = "Renato" };

        var response = await _client.SayHelloAsync(request);

        Assert.That(response.Message, Does.Contain("Renato"));
    }

    [Test]
    public async Task ShouldReturnMessageWithHello()
    {
        var request = new HelloRequest { Name = "Renato" };

        var response = await _client.SayHelloAsync(request);

        Assert.That(response.Message, Does.StartWith("Hello"));
    }
}

Notice the external alias directive at the head of the file.

We can, but should we?

Let’s be honest, GRPC services don’t require the same amount of plumbing that classic web applications need.
So it’s just natural to ask what advantage this approach based on the WebApplicationFactory gives us.

After all, there is an officially maintained NuGet package called Grpc.Core.Testing that offers fakes needed to invoke calls on a service instantiated in the classic way.

A test would look like something like the snippet below

[Test]
public async Task ShouldReturnMessageWithName()
{
    var context = new TestServerCallContext();

    var sut = new GreeterService(Mock.Of<ILogger>());

    var request = new HelloRequest { Name = "Renato" };

    var response = await sut.SayHello(request, context);

    Assert.That(response.Message, Does.StartWith("Hello"));
}

Personally, I find that WebApplicationFactory-based tests are not a replacement of classic unit-tests but rather a complement to them.

Being able to expand the scope of the tests so that they include serialization and deserialization, general integration with the framwork and more.

Finally, in a future post I’ll show how multiple WebApplicationFactorys can be used together to create an in-memory representation of the platform to test.

Recap

In this post we’ve explored how the WebApplicationFactory can help testing GRPC services without removing them from their hosting environment.

In the next post, I’ll show how it in unit tests based on Moq and AutoFixture.