gRPC is a popular way to create fast and scalable API’s in the .NET world. In this article, we explore how to test gRPC Services in .NET applications, and set up both unit and integration tests to ensure the reliability and performance of gRPC services.
So let’s start.
What is gRPC?
In general, gRPC is a framework designed for communication between parts of an application, regardless of their location or the languages they’re coded in. Under the hood, it relies on HTTP/2 for fast and efficient data transfer. It uses a Protocol Buffer for message serialization. This is a method similar to XML or JSON, but smaller in size which makes it more efficient. To learn more about gRPC, with emphasis on ASP.NET Core implementation, check out our existing article.
When talking about testing, we have various testing methodologies that serve different purposes. For example, we have performance tests that check the overall performance of our application Also, we have tests that check the logic and behavior of our application and one of the most common ones are unit and integration tests. We use unit tests to test one isolated unit in our application, and integration tests to test the whole flow. In this article, we will focus on these two types of tests, unit and integration tests, but if you want to learn more about testing itself, see Testing ASP.NET Core Applications.
Now that we know a bit more about gRPC itself, and why we would want to test it, we are ready to set up a new gRPC project and see it in action!
Project Setup
First, we create a new solution, and inside of it a new gRPC application using the .NET CLI:
dotnet new sln -n TestingGrpcService dotnet new grpc -n GrpcService dotnet sln TestingGrpcService.sln add GrpcService/GrpcService.csproj
Inside the newly created application, we check the GreeterService
class:
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 }); } }
The SayHello()
method takes a HelloRequest
and returns an HelloReply
object. It constructs the reply by greeting the name from the HelloRequest
. The method utilizes Task.FromResult()
to wrap the HelloReply
in a task, making the method asynchronous without using any asynchronous operations. This is done to make the method compatible with the expected task-based async pattern of gRPC methods.
Also, we see the method accepts another parameter, ServerCallContext
context. When talking about gRPC, ServerCallContext
provides us with server-side information about a gRPC call. It can include metadata, deadlines, cancellation signals, and client details. Also, using ServerCallContext
enables servers to access and respond to the specific context of each incoming gRPC request. For now, we don’t use this parameter in the method body, later we will.
One thing that also gets generated in a gRPC project is the greet.proto
file. This file defines the structure of the gRPC service and it includes the service methods, their request parameters, and return types. In our case, inside of it, we find the definition of the SayHello()
method. This file is important to mention when talking about testing since we use it to generate code for building both the client and server sides of the service, but more about this later on.
After taking a look at the service we want to test, the next thing is to create a new testing project inside the existing solution:
dotnet new xunit GrpcServiceTests dotnet sln TestingGrpcService.sln add GrpcServiceTests/GrpcServiceTests.csproj
Inside the test project, we install NSubstitute:
dotnet add package NSubstitute
Also, in the same project, we add a reference to the GrpcService
project so we can access its methods for testing:
dotnet add reference GrpcService.csproj
Now that we have a quick primer about the gRPC structure and set everything up, let’s start writing tests!
Test gRPC Services With Unit Testing
First, we open our newly created test project and add a new GreeterServiceUnitTests.cs
class. Inside it, we write a test for the SayHello()
method:
private readonly ILogger<GreeterService> _logger; public GreeterServiceUnitTests() { _logger = Substitute.For<ILogger<GreeterService>>(); } private GreeterService CreateGreeterService() => new(_logger.Object); [Fact] public async Task WhenSayHelloAsyncIsCalled_ThenItShouldReturnCorrectName() { // Arrange var service = CreateGreeterService(); var testName = "Test Name"; var expectedOutput = $"Hello {testName}"; var request = new HelloRequest { Name = testName }; // Act var response = await service.SayHello(request, null); // Assert Assert.Equal(expectedOutput, response.Message); }
We define a field ILogger<GreeterService> _logger
, a logger which we will use later on to initialize GreeterService
. Within the class constructor, using NSubstitute, we initialize _logger
as a new mocked instance of ILogger<GreeterService>
. This approach enables us to test them GreeterService
in isolation, without any external dependencies.
Also, we create a small helper method CreateGreeterService()
to have a centralized place where we create GreeterService
class.
Next, we define the WhenSayHelloAsyncIsCalled_ThenItShouldReturnCorrectName
test in which we test the SayHello()
method. We prepare a new HelloRequest
one with the Name
property set to “Test Name”. To verify the output later on, we also define the expectedOutput
variable to hold the expected response value after calling the method.
In the Act portion of the test, we invoke the SayHello()
method with a predefined request body and a null ServerCallContext
. In this case, we set ServerCallContext
to null
since we are not utilizing it.
Lastly, in the Assert part, we check that the method response equals the expected output.
Now, with dotnet test
.NET CLI command inside our test project we run the tests and check if everything works as expected:
We can see that one test was found and that it ran successfully.
With this example, we took a look at the simplest case when writing unit tests. Next, we will take a look at how to mock and use the ServerCallContext
data.
Mocking ServerCallContext
First, we go back to the GrpcService
project. Inside of GreeterService
class, we update SayHello()
method to utilize ServerCallContext
parameter:
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { var peer = context.Peer; _logger.LogInformation("Request from: {peer}", peer); return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); }
Now, inside the SayHello()
method we use the ServerCallContext
Peer
property to get the caller’s address or its identifier. Then, we use this information to log where the request came from. The rest of the method behaves as before.
Under the hood, ServerCallContext is an abstract class. When setting up methods and properties of such classes, we can only set up the ones marked with an abstract or virtual keyword. In our case, we want to mock the Peer
property, which isn’t abstract or virtual. In the case of setting up ServerCallContext
, we can use the Grpc.Core.Testing library. This library gives us the possibility to create a test instance of ServerCallContext
. We go back to our test project, and through .NET CLI install Grpc.Core.Testing
:
dotnet add package Grpc.Core.Testing
Then, we modify our existing test to include setting up a ServerCallContext:
[Fact] public async Task WhenSayHelloAsyncIsCalled_ThenItShouldReturnCorrectName() { // Arrange var service = CreateGreeterService(); var testName = "Test Name"; var expectedOutput = $"Hello {testName}"; var request = new HelloRequest { Name = testName }; var peer = "localhost"; var mockContext = TestServerCallContext.Create( method: "", host: "", deadline: DateTime.UtcNow.AddMinutes(30), requestHeaders: [], cancellationToken: CancellationToken.None, peer: peer, authContext: null, contextPropagationToken: null, writeHeadersFunc: null, writeOptionsGetter: null, writeOptionsSetter: null); // Act var response = await service.SayHello(request, mockContext); // Assert Assert.Equal(expectedOutput, response.Message); _logger.Received(1).Log( LogLevel.Information, Arg.Any<EventId>(), Arg.Is<object>(o => o.ToString()!.Contains("Request from: localhost")), null, Arg.Any<Func<object, Exception, string>>()); }
We include TestServerCallContext
from the Grpc.Core.Testing
library, which allows us to create a test instance of ServerCallContext
. We call its Create()
method and define all required constructor parameters and set the peer value to “localhost”. Now, in the assert section, we verify that our logger correctly logs the desired message.
Again, we run a dotnet test
command to check if our tests run successfully.
By using the TestServerCallContext.Create()
method we can define and test all the ServerCallContext
constructor parameters. Besides ServerCallContext
properties, we also might need to use its extension methods, so let’s take a quick look at it.
Setting up ServerCallContext Extension Methods
When working with ServerCallContext
there are cases where we might also use its extension method GetHttpContext()
. This extension method is used to get access to HttpContext
, which can be useful to check for HTTP headers, IP addresses, or similar information. If we want to setup this method in our test scenarios, we need to do a couple of things:
var serverCallContextMock = TestServerCallContext.Create(); var httpContext = new DefaultHttpContext(); serverCallContextMock.UserState["__HttpContext"] = httpContext;
Same as before, we first create a new ServerCallContext
class by using Grpc.Core.Testing
library.
Then, we define httpContext
variable that represents an HttpContext
object that GetHttpContext()
method will return. After that, we alter the UserState
property of the created ServerCallContext
object. UserState
property is defined as a <key, value>
pair value.
Under the hood, when we call GetHttpContext()
, within UserState
it searches for a value with the key __HttpContex
. For everything to work smoothly in the setup, with line 4, serverCallContextMock.UserState["__HttpContext"] = httpContext
we assign the value of the key __HttpContex
to our mocked HttpContext
named httpContext. And with this, we are set to test the GetHttpContext()
extension method,
Now that we have seen how we can unit test our gRPC service in different scenarios, let’s look at what setting up an integration test would look like.
Test gRPC Services With Integration Testing
To start with integration testing of the gRPC service, first, we need to check if we are creating both client and server from our greeter.proto
file. One way we can check is to open the .csproj
file of the GrpcService
project where the greet.proto
file is located in:
<ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Server" /> </ItemGroup>
By default, we see that we have <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
. This directive states that the gRPC server code is generated from the greet.proto
file. For integration tests, it’s also necessary to have a client-side to simulate real client interactions with the server:
<ItemGroup> <Protobuf Include="Protos\greet.proto" GrpcServices="Both" /> </ItemGroup>
We update the GrpcServices setting from “Server” to “Both”. This way, we instruct our development environment to generate server and client stubs from our proto file. To apply these changes and generate the necessary code for both client and server, we need to rebuild the project.
After we set this up, let’s see what else we are missing to start testing.
Setting up Boilerplate Code to Test gRPC Services
When writing integration tests for gRPC we have a lot of similar code to write as in integration testing any other client in .NET. One of the things we commonly use in those cases is WebApplicationFactory<TEntryPoint> Class. This is a fixture that allows us to write integration tests without the need for a real web server. To configure and run the application it uses the TEntryPoint
which is most often the “Program” or “Startup” class and in our case it is Program.cs
class.
Since in this article, we want to focus on specifics when testing gRPC services, we will quickly explain how to set up WebApplicationFactory<Program>
and use it in our integration test setup. If you want to go a little bit more in-depth about it, check out Integration Testing in ASP.NET Core.
The first thing we need to do is go to our GrpcService project and add public and partial modifiers to the Program.cs
class:
public partial class Program { }
This makes the Program.cs accessible inside our testing project and flexible for further extensions:
Next, we go back to our test project and create a CustomWebApplicationFactory.cs
the class that inherits from WebApplicationFactory<Program>
:
public class CustomWebApplicationFactory : WebApplicationFactory<Program> { }
With this set, we have a centralized place where we can modify application settings. This is especially useful if using an in-memory database or wanting to customize how the application configures.
After this, we create a new class, named GreeterServiceIntegrationTests.cs
, which we will use to write our integration tests. Inside it, we use xunit’s IClassFixture, to add CustomWebApplicationFactory
:
public class GreeterServiceIntegrationTests : IClassFixture<CustomWebApplicationFactory> { private readonly WebApplicationFactory<Program> _factory; public GreeterServiceIntegrationTests(CustomWebApplicationFactory factory) { _factory = factory; } }
We inject CustomWebApplicationFactory in our constructor and assign it to the WebApplicationFactory<Program> _factory
so it is available to us later on.
With all of this set, we are ready to focus on gRPC specifics in integration testing. The next thing that we need to do is to create a new gRPC client that we use to make our requests.
Setting Up a gRPC Client
Remaining in our test project, we want to create a new GreeterClient
class. This client refers to a specific client stub that was generated from the greet.proto definition.
To create a client we first need to create a new channel. In gRPC, a channel represents a long-lived connection to a gRPC server. Clients use this connection to send requests and receive responses from the server:
[Fact] public async Task WhenSayHelloAsyncIsCalled_ThenItShouldReturnCorrectName() { // Arrange GrpcChannelOptions options = new() { HttpHandler = _factory.Server.CreateHandler() }; GrpcChannel channel = GrpcChannel.ForAddress(_factory.Server.BaseAddress, options); GreeterClient client = new(channel); var testName = "Test Name"; var expectedResponse = $"Hello {testName}"; var request = new HelloRequest { Name = testName }; // Act var response = await client.SayHelloAsync(request); // Assert Assert.Equal(expectedResponse, response.Message); }
To create a new channel, we need to define where it should send the requests, which is where the ForAddress() method, BaseAddress, and HttpHandler come into play.
We use ForAddress()
method to create a new channel that connects to a gRPC service at a specified address. In our case, we want to connect to the test server created from our WebApplicationFactory
class, so we use _factory.Server.BaseAddress
property to get its base address.
Next, we use GrpcChannelOptions to instantiate a new HttpHandler
. An HttpHandler
is a component that handles the HTTP transport layer of a request. By using _factory.Server.CreateHandler()
to define our handler we bypass the network stack and directly invoke the test server’s processing pipeline. This allows the gRPC client in our test to communicate with the test server without making actual network calls, making the tests faster and more reliable since they’re not affected by network issues.
Now that we have our channel ready, by using new Greeter.GreeterClient(channel)
we create a new GreeterClient
. With the client set, we create a new request boy with a sample name and prepare the expected output variable. We invoke the SayHelloAsync() method, and after the request is made check if the response contains the expected message.
Again, we use .NET CLI dotnet test
command to run the tests:
After running the tests, we see that both our previously written unit test and newly added integration test are found and work as expected.
Conclusion
In this article, we learned how to test gRPC services in ASP.NET Core apps. We covered both the unit and the integration testing and showed how to use web factory options to set up a gRPC client which we then used for testing.