Testing ASP.NET Core GRPC applications with WebApplicationFactory
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 WebApplicationFactory
s 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.
Support this blog
If you liked this article, consider supporting this blog by buying me a pizza!