Welcome to today’s post.
In today’s post I will be explaining how to setup and implement unit testing for gRPC services.
I will be showing how to use dependency injection to make gRPC services testable, and how to use the test mocking framework Mock, to implement unit tests.
I will be focusing on testing each of the four different gRPC call types:
- Unary (one-way)
- Server streaming
- Client streaming
- Bi-directional (duplex) streaming
I have explained in previous posts what each of the above RPC call types is, how to implement each as a gRPC service method within a gRPC library, how to create a gRPC client to connect to and make requests to the gRPC call method.
I followed up with the above in another post by showing how to test the above calls by using the popular Web API testing tool, Postman.
In another previous post I showed how to setup and use the Mock testing framework to unit test .NET Core applications.
In each unit test method, I will employ the unit test pattern sequence:
- Arrange
- Act
- Assert
I will first show how to prepare our gRPC service library for unit testing by refactoring any business logic within the library out to interfaces and common classes that are then injected into the service library using.NET Core dependency injection and service collections.
Making a gRPC Library Testable
To make a gRPC library testable we refactor the class library using the basic SOLID principles such as the single-responsibility rule, interface segregation, and dependency inversion.
Below is the original gRPC service library with a unary RPC call method:
public override Task<LibraryReply> GetLibrary(LibraryRequest request, ServerCallContext context)
It contains the essence of the business logic, along with logging functionality. When run, this will allow a client (a .NET console or even the Postman tester app) to connect and run requests.
using Grpc.Core;
using GrpcBookLoanService;
namespace GrpcBookLoanService.Services
{
public class LibraryService : Library.LibraryBase
{
private readonly ILogger<LibraryService> _logger;
public LibraryService(ILogger<LibraryService> logger)
{
_logger = logger;
}
public override Task<LibraryReply> GetLibrary(LibraryRequest request, ServerCallContext context)
{
string slibrary_name = String.Empty;
string slibrary_location = String.Empty;
switch (request.LibraryId)
{
case 1:
slibrary_name = "NSW State Library";
slibrary_location = "Sydney";
break;
case 2:
slibrary_name = "Eastwood Library";
slibrary_location = "Eastwood";
break;
case 3:
slibrary_name = "Chatswood Library";
slibrary_location = "Chatswood";
break;
default:
slibrary_name = "N/A";
slibrary_location = "N/A";
break;
}
return Task.FromResult(new LibraryReply
{
LibraryName = slibrary_name,
LibraryLocation = slibrary_location
});
}
}
}
I will now apply the interface segregation principle to separate the logic out into an interface contract that includes the method call prototype. The definition is shown below:
using Grpc.Core;
namespace GrpcBookLoanService.Interfaces
{
public interface ILibraryCommon
{
public LibraryReply GetLibraryDetails(int id);
}
}
The business logic within the original class library can be re-implemented into a common library that contains the implementation of the GetLibraryDetails() helper method. The helper method contains the case switch logic code that was in the original class library. I have also swept up the logging interface into this common class in keeping with the single-responsibility principle.
The concrete implementation of the common class that enforces the interface contract is below:
using Grpc.Core;
using GrpcBookLoanService.Interfaces;
using GrpcBookLoanService.Services;
namespace GrpcBookLoanService.Common
{
public class LibraryCommon : ILibraryCommon
{
private readonly ILogger<LibraryService> _logger;
public LibraryCommon(ILogger<LibraryService> logger)
{
_logger = logger;
}
public LibraryReply GetLibraryDetails(int id)
{
string slibrary_name = String.Empty;
string slibrary_location = String.Empty;
switch (id)
{
case 1:
slibrary_name = "NSW State Library";
slibrary_location = "Sydney";
break;
case 2:
slibrary_name = "Eastwood Library";
slibrary_location = "Eastwood";
break;
case 3:
slibrary_name = "Chatswood Library";
slibrary_location = "Chatswood";
break;
default:
slibrary_name = "N/A";
slibrary_location = "N/A";
break;
}
return new LibraryReply
{
LibraryName = slibrary_name,
LibraryLocation = slibrary_location
};
}
}
When we setup our application configuration in Program.cs, we add the interface and implementation class to the service collection as follows:
builder.Services.AddSingleton<ILibraryCommon, LibraryCommon>();
Once we have bound the interface to the concrete common class, we are one step closer to making our gRPC service class testable.
The resulting gRPC service library that contains the library interface, injects the common library implementation into the service class with the property declaration:
private readonly ILibraryCommon _library;
The re-factored, testable gRPC service library is below:
using Grpc.Core;
using GrpcBookLoanService;
using GrpcBookLoanService.Interfaces;
namespace GrpcBookLoanService.Services
{
public class LibraryService : Library.LibraryBase
{
private readonly ILibraryCommon _library;
public LibraryService(ILibraryCommon library)
{
_library = library;
}
// Example of a unary gRPC call.
public override Task<LibraryReply> GetLibrary(LibraryRequest request, ServerCallContext context)
{
LibraryReply reply = _library.GetLibraryDetails(request.LibraryId);
return Task.FromResult(new LibraryReply
{
LibraryName = reply.LibraryName,
LibraryLocation = reply.LibraryLocation
});
}
..
}
}
Once our library is testable, we can then start to create a unit test project and implement some useful tests of out gRPC library.
Implementation of a gRPC Unary Call Unit Test
With the unit test project, I am using the NUnit project template. You can use any other unit test project type available such as xUnit or MS Test to achieve the same outcome.
To use the Mock testing framework, also known as Moq, it must be imported through the NuGet package manager.
Each test in the Unit test runner framework can consist of a Setup, Teardown and at least one Test method.
When implementing we arrange part of a test, we are setting up the mock test libraries and expected result(s) of our tests.
Setting up the mock object for the common library is shown below:
// Arrange
var mockLibraryCommon = new Mock<ILibraryCommon>();
Setting up the expected result for the GetLibraryDetails() commonclass method is shown below:
mockLibraryCommon.Setup(m => m.GetLibraryDetails(1))
.Returns(new LibraryReply()
{
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
Setting up the mock object for the gRPC service library is shown below:
var service = new LibraryService(mockLibraryCommon.Object);
We then run the test execution of the unary RPC call method:
public override Task<LibraryReply> GetLibrary(LibraryRequest request, ServerCallContext context)
I gave a comprehensive overview of the implementation of a unary (one-way) gRPC call and how to execute client requests in a previous post.
The unit test Act step (the test action or execution) is done in the code excerpt below:
// Act
var rpc_call = await service.GetLibrary(new LibraryRequest()
{
LibraryId = 1
}, new TestServerCallContext(new Metadata(), default)
);
In our unary RPC call, the final parameter that is in the function specification is the server call context of type ServerCallContext. In the unit test environment, we do not have an existing server context that runs in the real production environment, so we are required to implement a testable server call context class. Essentially, to make unit testing easier for us, we implement a test double. A test double is an object that is a simplified version of what in implemented in the real version. In our case we require the implementation of a fake object that mimics the behaviour of an RPC server call context.
We do this by implementing from the ServerCallContext class. By applying the class implementation helper in the IDE, we get the following stubs generated:
using Grpc.Core;
namespace GrpcBookLoanService.Helpers
{
public class TestServerCallContext : ServerCallContext
{
protected override string MethodCore => throw new NotImplementedException();
protected override string HostCore => throw new NotImplementedException();
protected override string PeerCore => throw new NotImplementedException();
protected override DateTime DeadlineCore => throw new NotImplementedException();
protected override Metadata RequestHeadersCore => throw new NotImplementedException();
protected override CancellationToken CancellationTokenCore => throw new NotImplementedException();
protected override Metadata ResponseTrailersCore => throw new NotImplementedException();
protected override Status StatusCore { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
protected override WriteOptions? WriteOptionsCore { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
protected override AuthContext AuthContextCore => throw new NotImplementedException();
protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions? options)
{
throw new NotImplementedException();
}
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)
{
throw new NotImplementedException();
}
}
}
We then implement each protected property getters, setters, and methods to arrive at the following implementation for the server call context test double:
using Grpc.Core;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace GrpcBookLoanService.Helpers
{
public class TestServerCallContext : ServerCallContext
{
private readonly Metadata _requestHeaders;
private readonly CancellationToken _cancellationToken;
private readonly Metadata _responseTrailers;
private readonly AuthContext _authContext;
private readonly DateTime _deadlineCore;
private readonly string _methodCore;
private readonly IDictionary<object, object> _userState;
private readonly string _hostCore;
private readonly string _peerCore;
private WriteOptions? _writeOptions;
private Status _statusCore;
protected override string MethodCore => _methodCore;
protected override string HostCore => _hostCore;
protected override string PeerCore => _peerCore;
protected override DateTime DeadlineCore => _deadlineCore;
protected override Metadata RequestHeadersCore => _requestHeaders;
protected override CancellationToken CancellationTokenCore => _cancellationToken;
protected override Metadata ResponseTrailersCore => _responseTrailers;
protected override Status StatusCore { get => _statusCore; set { _statusCore = value; } }
protected override WriteOptions? WriteOptionsCore { get => _writeOptions; set { _writeOptions = value; } }
protected override AuthContext AuthContextCore => _authContext;
protected override IDictionary<object, object> UserStateCore => _userState;
public TestServerCallContext(Metadata requestHeaders, CancellationToken cancellationToken)
{
_requestHeaders = requestHeaders;
_cancellationToken = cancellationToken;
_responseTrailers = new Metadata();
_authContext = new AuthContext(string.Empty, new Dictionary<string, List<AuthProperty>>());
_userState = new Dictionary<object, object>();
_methodCore = "AnyMethod";
_hostCore = "AnyHostCore";
_peerCore = "AnyPeerCore";
}
public static TestServerCallContext Create(Metadata? requestHeaders = null, CancellationToken cancellationToken = default)
{
return new TestServerCallContext(requestHeaders ?? new Metadata(), cancellationToken);
}
protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions? options)
{
throw new NotImplementedException();
}
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)
{
responseHeaders = responseHeaders ?? new Metadata();
return Task.CompletedTask;
}
}
}
The testable server call context includes a static constructor that can be instantiated within the context of the unit test environment.
Given the above helper, its purpose is served in the creation of an RPC call method that has a testable call context.
The Assert part of our unit test involves ensuring the call to the library method was completed in the Act part of the unit test. This is done with the Verify() method:
// Assert
mockLibraryCommon.Verify(v => v.GetLibraryDetails(1));
We can then run assertions that determine the truth or falsity of any property from the call response. Below we test that the LibraryName property within the response matches a particular value:
Assert.That(rpc_call.LibraryName, Is.EqualTo("NSW State Library"));
Below is the unit test implementation for the unary RPC call:
using Grpc.Core;
using GrpcBookLoanService;
using GrpcBookLoanService.Helpers;
using GrpcBookLoanService.Interfaces;
using GrpcBookLoanService.Services;
using Microsoft.AspNetCore.TestHost;
using Moq;
namespace GrpcBookLoanUnitTest
{
public class Tests
{
[SetUp]
public async Task Setup()
{
}
[Test]
public async Task TestUnaryLibraryCall()
{
// Arrange
var mockLibraryCommon = new Mock<ILibraryCommon>();
mockLibraryCommon.Setup(m => m.GetLibraryDetails(It.IsAny<int>()))
.Returns(It.IsAny<LibraryReply>());
mockLibraryCommon.Setup(m => m.GetLibraryDetails(1))
.Returns(new LibraryReply()
{
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
var service = new LibraryService(mockLibraryCommon.Object);
// Act
var rpc_call = await service.GetLibrary(
new LibraryRequest()
{
LibraryId = 1
},
new TestServerCallContext(new Metadata(), default)
);
// Assert
mockLibraryCommon.Verify(v => v.GetLibraryDetails(1));
Assert.That(rpc_call.LibraryName, Is.EqualTo("NSW State Library"));
Assert.Pass();
}
..
}
}
Running the above unit tests can be done through the Test Explorer in the Visual Studio IDE.
The above is the starting point for creating unit tests for gRPC services. The next level of complexity is implementing unit tests for RPC server, client, and duplex stream calls. These involve the implementation of additional testable classes which I will explore.
Implementation of a gRPC Server Stream Call Unit Test
The next gRPC call type to unit test is for the server stream RPC call.
I gave a comprehensive overview of the implementation of a server stream gRPC call and how to execute client requests in a previous post.
The server stream gRPC call method we wish to test has the following functional prototype:
// Example of a server streaming gRPC call.
public override async Task StreamLibraryStatusFromServer(
LibraryRequest request,
IServerStreamWriter<LibraryStatusReply> responseStream,
ServerCallContext context
)
The initial call has a request of type LibraryRequest, which supplies the RPC server method with an initial input parameter. The RPC server then returns a stream of messages from the response parameter responseStream, which is of type IServerStreamWriter<LibraryStatusReply>.
A unit test for RPC server streaming will require us to implement a test double of type IServerStreamWriter<T>, where T is any type.
Before I show how the test double is implemented, I will start by explaining how we implement the unit test.
Much like we did for the unary call unit test, we do likewise for the server streaming call.
The Arrange step is shown below:
// Arrange
var mockLibraryCommon = new Mock<ILibraryCommon>();
mockLibraryCommon.Setup(m => m.GetLibraryDetails(1))
.Returns(new LibraryReply()
{
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
mockLibraryCommon.Setup(m => m.GetLibraryStatusDetails(1))
.Returns(new LibraryStatusReply()
{
Status = "Normal",
IsOpen = true,
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
We have setup responses for the common library to return mocked results for the GetLibraryDetails() and GetLibraryStatusDetails() methods.
Our common library has an additional method GetLibraryStatusDetails() that returns the library opening status (Open or Closed) depending on the time of day and a random Status for how busy the library is.
public LibraryStatusReply GetLibraryStatusDetails(int id)
{
string slibrary_name = String.Empty;
string slibrary_location = String.Empty;
LibraryReply reply = GetLibraryDetails(id);
bool openStatus = false;
DateTime dt = DateTime.Now;
if ((dt.Hour >= 9) && (dt.Hour <= 23))
openStatus = ((dt.Hour >= 9) && (dt.Hour <= 23)) ? true : false;
Random random = new Random();
int statusValue = random.Next(1, 4);
string status = String.Empty;
switch (statusValue)
{
case 1:
status = "Quiet";
break;
case 2:
status = "Normal";
break;
case 3:
status = "Busy";
break;
default:
status = "Normal";
break;
}
return new LibraryStatusReply
{
LibraryName = reply.LibraryName,
LibraryLocation = reply.LibraryLocation,
IsOpen = openStatus,
Status = (openStatus == true ? status : "Quiet")
};
}
Our gRPC service library has the equivalent method implemented as shown:
public LibraryStatusReply GetLibraryStatusDetails(int id)
{
return _library.GetLibraryStatusDetails(id);
}
Before we can run the Act steps, we require setting up objects for the mocks and test doubles.
var cancellationTokenSource = new CancellationTokenSource();
var service = new LibraryService(mockLibraryCommon.Object);
var testserverCallContext = new TestServerCallContext(new Metadata(),
cancellationTokenSource.Token);
var teststreamWriter = new TestServerStreamWriter<LibraryStatusReply>(testserverCallContext);
// Act
var rpc_call = service.StreamLibraryStatusFromServer(
new LibraryRequest()
{
LibraryId = 1
}, teststreamWriter, testserverCallContext
);
In a call to the RPC server stream method, we require instances of the TestServerCallContext test double and an additional instance TestServerStreamWriter, which isa test double from an instance of the typed interface IServerStreamWriter<T>. In the RPC call to the server stream method, we will be required to pass in new instances of the typed TestServerStreamWriter class initialised with an instance of the TestServerCallContext class.
Below is an implementation of the TestServerStreamWriter class:
using Grpc.Core;
using System.Threading.Channels;
namespace GrpcBookLoanUnitTest
{
public class TestServerStreamWriter<T> : IServerStreamWriter<T> where T : class
{
public WriteOptions? WriteOptions { get; set; }
private readonly Channel<T> _channel;
private readonly ServerCallContext _serverCallContext;
public TestServerStreamWriter(ServerCallContext serverCallContext)
{
_serverCallContext = serverCallContext;
_channel = Channel.CreateUnbounded<T>();
}
public void Complete()
{
_channel.Writer.Complete();
}
public IAsyncEnumerable<T> ReadAllAsync()
{
return _channel.Reader.ReadAllAsync();
}
public async Task<T?> ReadNextAsync()
{
if (await _channel.Reader.WaitToReadAsync())
{
_channel.Reader.TryRead(out var message);
return message;
}
return null;
}
public Task WriteAsync(T message)
{
if (_serverCallContext.CancellationToken.IsCancellationRequested)
{
return Task.FromCanceled(_serverCallContext.CancellationToken);
}
_channel.Writer.TryWrite(message);
return Task.CompletedTask;
}
}
}
The above class uses channels to implement a producer/consumer model to allow messages to be written to and read from the channel in a thread-safe asynchronous manner.
A new channel that allows an indefinite number of messages is created:
Channel.CreateUnbounded<T>();
Messages are written to the channel with the command:
_channel.Writer.TryWrite();
Messages are then read from the channel with the command:
_channel.Reader.TryRead();
Or they can be read asynchronously from the channel with the command:
_channel.Reader.ReadAllAsync();
When writes are complete, the channel can be closed to prevent further writes with the command:
_channel.Writer.Complete();
In the unit test, in the Assert step we test if the call is not cancelled or completed:
// Assert
Assert.IsFalse(rpc_call.IsCanceled, "RPC method has not been cancelled!");
Assert.IsFalse(rpc_call.IsCompleted, "RPC method is not completed!");
Assert.IsFalse(rpc_call.IsCompletedSuccessfully, "RPC method is not completed!");
As the server stream continues to run until it is cancelled, we execute a cancellation of the token:
// Cancel the token.
cancellationTokenSource.Cancel();
We then close the channel within the stream to prevent further writes:
// Complete the response stream.
teststreamWriter.Complete();
We then read messages from the channel until there are no further messages to read:
// Read messages from server stream.
var serverMessages = new List<LibraryStatusReply>();
var message = new LibraryStatusReply();
while (message != null)
{
message = await teststreamWriter.ReadNextAsync();
if (message != null)
serverMessages.Add(message);
}
Then we apply an assertion to determine if there were messages in the stream:
// Test if we received at least one message response.
Assert.IsTrue(serverMessages.Count() > 0);
The complete unit test for the RPC server stream is below:
[Test]
public async Task TestServerStreamLibraryCall()
{
// Arrange
var mockLibraryCommon = new Mock<ILibraryCommon>();
mockLibraryCommon.Setup(m => m.GetLibraryDetails(1))
.Returns(new LibraryReply()
{
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
mockLibraryCommon.Setup(m => m.GetLibraryStatusDetails(1))
.Returns(new LibraryStatusReply()
{
Status = "Normal",
IsOpen = true,
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
var cancellationTokenSource = new CancellationTokenSource();
var service = new LibraryService(mockLibraryCommon.Object);
var testserverCallContext = new TestServerCallContext(
new Metadata(),
cancellationTokenSource.Token
);
var teststreamWriter = new TestServerStreamWriter<LibraryStatusReply>(testserverCallContext);
// Act
var rpc_call = service.StreamLibraryStatusFromServer(
new LibraryRequest()
{
LibraryId = 1
},
teststreamWriter,
testserverCallContext
);
// Assert
Assert.IsFalse(rpc_call.IsCanceled, "RPC method has not been cancelled!");
Assert.IsFalse(rpc_call.IsCompleted, "RPC method is not completed!");
Assert.IsFalse(rpc_call.IsCompletedSuccessfully, "RPC method is not completed!");
// Cancel the token.
cancellationTokenSource.Cancel();
// Wait for RC call completion.
//await response;
// Complete the response stream.
teststreamWriter.Complete();
// Read messages from server stream.
var serverMessages = new List<LibraryStatusReply>();
var message = new LibraryStatusReply();
while (message != null)
{
message = await teststreamWriter.ReadNextAsync();
if (message != null)
serverMessages.Add(message);
}
// Test if we received at least one message response.
Assert.IsTrue(serverMessages.Count() > 0);
}
In the next section, I will show how to implement a unit test for a gRPC client steam call.
Implementation of a gRPC Client Stream Call Unit Test
The next gRPC call type to unit test is for the client stream RPC call.
I gave a comprehensive overview of the implementation of a client stream gRPC call and how to execute client requests in a previous post.
The client stream gRPC call method we wish to test has the following functional prototype:
public override async Task<LibraryTransferReply> StreamingTransferActionFromClient(
IAsyncStreamReader<LibraryBookTransferRequest> requestStream,
ServerCallContext context
)
The response from a streamed client call is of type LibraryTransferReply. The client initially pushes a stream of messages through the requestStream parameter, which is of type IAsyncStreamReader<LibraryBookTransferRequest>.
A unit test for RPC client streaming will require us to implement a test double of type IAsyncStreamReader<T>, where T is any type.
Before I show how the test double is implemented, I will start by explaining how we implement the unit test.
Much like we did for the server call unit test, we do likewise for the client streaming call.
The Arrange step is shown below:
// Arrange
var mockLibraryCommon = new Mock<ILibraryCommon>();
mockLibraryCommon.Setup(m => m.GetLibraryDetails(1))
.Returns(new LibraryReply()
{
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
mockLibraryCommon.Setup(m => m.GetLibraryStatusDetails(1))
.Returns(new LibraryStatusReply()
{
Status = "Normal",
IsOpen = true,
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
Before we can run the Act steps, we require setting up objects for the mocks and test doubles.
var cancellationTokenSource = new CancellationTokenSource();
var service = new LibraryService(mockLibraryCommon.Object);
var testserverCallContext = new TestServerCallContext(
new Metadata(),
cancellationTokenSource.Token
);
var teststreamReader = new TestServerAsyncStreamReader<LibraryBookTransferRequest>(
testserverCallContext
);
// Act
var rpc_call = service.StreamingTransferActionFromClient(
teststreamReader,
testserverCallContext
);
In a call to the RPC client stream method, we require instances of the TestServerCallContext test double and an additional instance TestAsyncStreamReader, which isa test double from an instance of the typed interface IAsyncStreamReader<T>. In the RPC call to the server stream method, we will be required to pass in new instances of the typed TestAsyncStreamReader class initialised with an instance of the TestServerCallContext class.
Below is an implementation of the TestAsyncStreamReader class:
using Grpc.Core;
using System.Threading.Channels;
namespace GrpcBookLoanUnitTest
{
public class TestServerAsyncStreamReader<T> : IAsyncStreamReader<T>
where T : class
{
private readonly Channel<T> _channel;
private readonly ServerCallContext _serverCallContext;
private T _current;
public T Current
{
get { return _current; }
set { value = _current; }
}
public TestServerAsyncStreamReader(ServerCallContext serverCallContext)
{
_current = null!;
_serverCallContext = serverCallContext;
_channel = Channel.CreateUnbounded<T>();
}
public async Task<bool> MoveNext(CancellationToken cancellationToken)
{
_serverCallContext.CancellationToken.ThrowIfCancellationRequested();
_current = null!;
if (await _channel.Reader.WaitToReadAsync(cancellationToken))
{
if (_channel.Reader.TryRead(out var message))
{
_current = message;
return true;
}
return false;
}
return false;
}
public void AddMessage(T message)
{
_channel.Writer.TryWrite(message);
}
public void Complete()
{
_channel.Writer.Complete();
}
}
}
As we did for the TestServerStreamWriter test double, the above class uses channels to allow messages to be written to and read from the stream in a thread-safe asynchronous manner.
In the unit test, before we can read in the results of the response from the client streaming call, we must push some messages into the reader stream and complete the stream.
This is done below:
DateTime currentServerDateTime = DateTime.UtcNow;
teststreamReader.AddMessage(new LibraryBookTransferRequest()
{
BookId = "1",
LibraryId = 1,
TransferAction = "Loan",
DateAction = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(currentServerDateTime)
});
teststreamReader.AddMessage(new LibraryBookTransferRequest()
{
BookId = "2",
LibraryId = 1,
TransferAction = "Return",
DateAction = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(currentServerDateTime)
});
// Complete the async stream.
teststreamReader.Complete();
in the Assert step we wait for the client streaming call to complete processing of the messages in the request stream.
// Assert
var rpc_response = await rpc_call;
The client streaming call sums up the number of books borrowed, and the number of books returned, returning the result in the response through the LibraryTransferReply object. Applying assertions is the final part of our unit test.
Assert.IsTrue(rpc_response.NumberBorrowed == 1);
Assert.IsTrue(rpc_response.NumberReturned == 1);
The complete unit test for the RPC client stream is below:
[Test]
public async Task TestClientStreamLibraryCall()
{
// Arrange
var mockLibraryCommon = new Mock<ILibraryCommon>();
mockLibraryCommon.Setup(m => m.GetLibraryDetails(1))
.Returns(new LibraryReply()
{
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
mockLibraryCommon.Setup(m => m.GetLibraryStatusDetails(1))
.Returns(new LibraryStatusReply()
{
Status = "Normal",
IsOpen = true,
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
var cancellationTokenSource = new CancellationTokenSource();
var service = new LibraryService(mockLibraryCommon.Object);
var testserverCallContext = new TestServerCallContext(
new Metadata(),
cancellationTokenSource.Token
);
var teststreamReader = new TestServerAsyncStreamReader<LibraryBookTransferRequest>
(
testserverCallContext
);
// Act
var rpc_call = service.StreamingTransferActionFromClient(
teststreamReader,
testserverCallContext
);
DateTime currentServerDateTime = DateTime.UtcNow;
teststreamReader.AddMessage(new LibraryBookTransferRequest()
{
BookId = "1",
LibraryId = 1,
TransferAction = "Loan",
DateAction = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(currentServerDateTime)
});
teststreamReader.AddMessage(new LibraryBookTransferRequest()
{
BookId = "2",
LibraryId = 1,
TransferAction = "Return",
DateAction = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(currentServerDateTime)
});
// Complete the async stream.
teststreamReader.Complete();
// Assert
var rpc_response = await rpc_call;
Assert.IsTrue(rpc_response.NumberBorrowed == 1);
Assert.IsTrue(rpc_response.NumberReturned == 1);
}
In the next section, I will show how to implement a unit test for a gRPC duplex stream call.
Implementation of a gRPC Duplex Stream Call Unit Test
The next gRPC call type to unit test is for the duplex (bidirectional) stream RPC call.
I gave a comprehensive overview of the implementation of a duplex stream gRPC call and how to execute bidirectional requests in a previous post.
The bidirectional stream gRPC call method we wish to test has the following functional prototype:
// Example of a duplex streaming gRPC call.
public override async Task StreamingTransferActionDuplex(
IAsyncStreamReader<LibraryBookTransferRequest> requestStream,
IServerStreamWriter<LibraryBookTransferResponse> responseStream,
ServerCallContext context)
A duplex streaming call has two streams running concurrently: a response stream of type IAsyncStreamReader<T> andrequest stream of type IServerStreamWriter<T>. The object type passed in both directions is of type LibraryBookTransferRequest.
A unit test for an RPC duplex streaming call will require us to use the test double classes TestServerAsyncStreamReader and TestServerStreamWriter for the request and response streams.
The purpose behind testing RPC duplex streams is to test the writing of messages into the reader stream while reading messages from the writer stream. While we read messages from the writer stream, we are ensuring the order of messages pushed into the channel for reader stream has been respected by checking the properties of the messages being read. This is done while at least one of the streams is still open.
As we did for the client and server call unit tests, the setup for the duplex streaming call is similar.
The mock object creation section of the Arrange step is shown below:
// Arrange
var mockLibraryCommon = new Mock<ILibraryCommon>();
mockLibraryCommon.Setup(m => m.GetLibraryDetails(1))
.Returns(new LibraryReply()
{
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
mockLibraryCommon.Setup(m => m.GetLibraryStatusDetails(1))
.Returns(new LibraryStatusReply()
{
Status = "Normal",
IsOpen = true,
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
The creation of mock objects for server context and cancellation token is shown below:
var cancellationTokenSource = new CancellationTokenSource();
var service = new LibraryService(mockLibraryCommon.Object);
var testserverCallContext = new TestServerCallContext(
new Metadata(),
cancellationTokenSource.Token
);
The creation of instances of the test doubles for the request (reader) and response (writer) stream objects are shown below:
var teststreamReader = new TestServerAsyncStreamReader<LibraryBookTransferRequest>(
testserverCallContext
);
var teststreamWriter = new TestServerStreamWriter<LibraryBookTransferResponse>(
testserverCallContext
);
In the Act section we first create of an instance of the duplex RPC streaming call, which is shown below:
// Act
var rpc_call = service.StreamingTransferActionDuplex(
teststreamReader,
teststreamWriter,
testserverCallContext
);
Then, the test involves pushing two message records though the reader (request) stream.
DateTime currentServerDateTime = DateTime.UtcNow;
teststreamReader.AddMessage(new LibraryBookTransferRequest()
{
LibraryId = 1,
BookId = "1",
TransferAction = "Loan",
DateAction = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(currentServerDateTime)
});
teststreamReader.AddMessage(new LibraryBookTransferRequest()
{
LibraryId = 1,
BookId = "2",
TransferAction = "Loan",
DateAction = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(currentServerDateTime)
});
We then read a message from the writer (response) stream, which should be the first record we pushed into the reader stream:
var response_1 = await teststreamWriter.ReadNextAsync();
Assert.IsTrue(
response_1!.MessageResponse ==
"Library 1 Loan desk has processed book 1."
);
We then push a further messing into the reader stream and then close off the stream:
teststreamReader.AddMessage(new LibraryBookTransferRequest()
{
LibraryId = 1,
BookId = "3",
TransferAction = "Return",
DateAction = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(currentServerDateTime)
});
teststreamReader.Complete();
At this point, there should still be two messages in the reader stream. If we read both messages from the writer stream, then the second and third records would be retrieved (in that order):
var response_2 = await teststreamWriter.ReadNextAsync();
Assert.IsTrue(
response_2!.MessageResponse ==
"Library 1 Loan desk has processed book 2."
);
var response_3 = await teststreamWriter.ReadNextAsync();
Assert.IsTrue(
response_3!.MessageResponse ==
"Library 1 Loan desk has processed book 3."
);
As there are no further messages in the writer stream, and no further records can be pushed through the reader stream, we close off the writer stream:
teststreamWriter.Complete();
The unit test implementation is shown below:
[Test]
public async Task TestDuplexStreamLibraryCall()
{
// Arrange
var mockLibraryCommon = new Mock<ILibraryCommon>();
mockLibraryCommon.Setup(m => m.GetLibraryDetails(1))
.Returns(new LibraryReply()
{
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
mockLibraryCommon.Setup(m => m.GetLibraryStatusDetails(1))
.Returns(new LibraryStatusReply()
{
Status = "Normal",
IsOpen = true,
LibraryLocation = "Sydney",
LibraryName = "NSW State Library"
});
var cancellationTokenSource = new CancellationTokenSource();
var service = new LibraryService(mockLibraryCommon.Object);
var testserverCallContext = new TestServerCallContext(
new Metadata(),
cancellationTokenSource.Token
);
var teststreamReader = new TestServerAsyncStreamReader<LibraryBookTransferRequest>
(
testserverCallContext
);
var teststreamWriter = new TestServerStreamWriter<LibraryBookTransferResponse>
(
testserverCallContext
);
// Act
var rpc_call = service.StreamingTransferActionDuplex(
teststreamReader,
teststreamWriter,
testserverCallContext);
DateTime currentServerDateTime = DateTime.UtcNow;
teststreamReader.AddMessage(new LibraryBookTransferRequest()
{
LibraryId = 1,
BookId = "1",
TransferAction = "Loan",
DateAction = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(currentServerDateTime)
});
teststreamReader.AddMessage(new LibraryBookTransferRequest()
{
LibraryId = 1,
BookId = "2",
TransferAction = "Loan",
DateAction = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(currentServerDateTime)
});
var response_1 = await teststreamWriter.ReadNextAsync();
Assert.IsTrue(
response_1!.MessageResponse ==
"Library 1 Loan desk has processed book 1."
);
teststreamReader.AddMessage(new LibraryBookTransferRequest()
{
LibraryId = 1,
BookId = "3",
TransferAction = "Return",
DateAction = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(currentServerDateTime)
});
teststreamReader.Complete();
var response_2 = await teststreamWriter.ReadNextAsync();
Assert.IsTrue(
response_2!.MessageResponse ==
"Library 1 Loan desk has processed book 2."
);
var response_3 = await teststreamWriter.ReadNextAsync();
Assert.IsTrue(
response_3!.MessageResponse ==
"Library 1 Return desk has processed book 3."
);
teststreamWriter.Complete();
}
The output on the server debug console for the five dispatched requests messages and response messages would be as shown:
All request stream messages processed.
Server request stream received 1 loan messages.
Server request stream received 0 return messages.
Start writing message to response stream...
Finished writing message to response stream.
Server request stream received 2 loan messages.
Server request stream received 0 return messages.
Start writing message to response stream...
Finished writing message to response stream.
Server request stream received 2 loan messages.
Server request stream received 1 return messages.
Start writing message to response stream...
Finished writing message to response stream.
With a gRPC duplex streaming call, we would expect response stream messages to follow request stream messages. The way we test the RPC server call is determined by the order of response messages, which depends on how we implement the logic for writing response stream messages.
Do we use a reader stream in a background thread then wait for it to process all request stream messages before running a separate loop or thread that writes the messages through the writer stream, or do we use a reader stream in a background thread that spawns off threads that writes the messages through the writer stream?
The above is an overview of unit testing the four different gRPC call types using the Mock unit testing framework within an NUnit test runner.
That is all for today’s post.
I hope you have found this post useful and informative.

Andrew Halil is a blogger, author and software developer with expertise of many areas in the information technology industry including full-stack web and native cloud based development, test driven development and Devops.