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.

To download the source code for this article, you can visit our GitHub repository.

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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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:

Test results of running unit tests for gRPC service

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:

Test results of running unit and integration tests for gRPC service

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!