Mock RestClient.ExecuteAsync

RestSharp is not a stranger to developers, so is the usage. What this posts aims to explore is how to do Unit Test a method, involving a RestClient.ExecuteAsync call. To be more precise, the post explores how to mock the RestClient for the method.

Let’s consider the following code, taken from VSPostman, one of the side projects I am working on.

public async Task Get(string url)
{
if (string.IsNullOrWhiteSpace(url) || _parameterDictionary.Values.Contains(null)) throw new ArgumentNullException();

_restClient.BaseUrl = new Uri(url); // _restClient is an instance of RestClient
_restRequest.Method = Method.GET;
var _returnValue = new ResponseObject();
if(_parameterDictionary?.Count==0)
_restRequest.Parameters.AddRange(_parameterDictionary?.Select(x => new Parameter() {Name = x.Key, Value = x.Value }));
var tcs = new TaskCompletionSource();
var watch = Stopwatch.StartNew();
_restClient.ExecuteAsync(_restRequest, response =>
{
_returnValue.ContendType = response.ContentType;
_returnValue.ResponseString = response.Content;
_returnValue.StatusCode = response.StatusCode;
_returnValue.StatusDescription = response.StatusDescription;
_returnValue.ResponseTime = watch.Elapsed;
_returnValue.Length = response.ContentLength;
if (response.Headers != null)
_returnValue.Headers = response.Headers.ToDictionary(x => x.Name, y => y.Value as string);

tcs.SetResult(_returnValue);
});
return await tcs.Task;
}

As observed, the code raises a GET Request using the ReshSharp library. If I was using TDD and writing a Unit Test case for this method, how would I approach it ? How would I mock the RestSharp library calls ?

Of course, it is easy to understand that we need to create the mock object of IRestClient. But how do we create a response ? For this we attach a call back.

var expected = "response content";
var expectedBytes = Encoding.UTF8.GetBytes(expected);
Mock restClient = new Mock();
restClient.Setup(x => x.ExecuteAsync(
It.IsAny(),
It.IsAny<Action<IRestResponse, RestRequestAsyncHandle>>()))
.Callback<IRestRequest, Action<IRestResponse, RestRequestAsyncHandle>>((request, callback) =>
{
callback(new RestResponse { Content= expected }, null);
});
// act
var clientService = new ClientServiceUsingRestSharp(restClient.Object);
var actualResponse = await clientService.Get($"http://www.google.com");

That would be all you need. The callback would ensure you get the response from the mock object.

Unit Testing Asynchronous Code

Unit Testing Asynchronous methods in older days were pretty tough, but thanks to the advances made in the Unit Testing Frameworks, this is now all the more easier.

We will begin by writing an async method, which we could use for writing our Unit Tests against.

public async Task ReadFileAsync(string fileName)
{
  using(var file = File.OpenText(fileName))
     return await file.ReadToEndAsync();
}

The advancements in Unit Testing Framework has made it possible for us to write Unit Tests against async methods easier.

private string _ValidPath = "DummyText.txt";
private string _InvalidPath = "";
private FileReaderAsync _fileReaderAsync;

[TestInitialize]
public void Init()
{
  _ValidPath = @"DemoFile/LargeText.txt";
  _InvalidPath = @"DemoFile/DoNotExist.txt";
  _fileReaderAsync = new FileReaderAsync();
}

[TestMethod]
public async Task FileRead_ValidPath_NoErrors()
{
  var data = await _fileReaderAsync.ReadFileAsync(_ValidPath);
  Assert.IsNotNull(data);
  Assert.IsTrue(data.Length > 0);
}

Handling Exceptions

Let’s add one more Test Case, that would ensure a FileNotFoundException is raised when an invalid path is passed to the method.

[TestMethod]
[ExpectedException(typeof(FileNotFoundException))]
public async Task FileRead_InvalidPath_FileNotFoundException()
{
  var data = await _fileReaderAsync.ReadFileAsync(_InvalidPath);
  Assert.IsNotNull(data);
  Assert.IsTrue(data.Length > 0);
}

This method would, as expected, would succeed with ExpectedException specified. So far, so good. That’s because we expected the exception to be raised from ReadFileAsync method and it did raise an exception when an invalid path was passed.

But what if we had an exception raised from the Arrange Part. For example, we had an exception raised by the Constructor of the class. Let us rewrite Constructor of our Class and the Test Method to demonstrate the issue.

Class under Test

public class FileReaderAsync
{
  public FileReaderAsync(bool throwError=false)
  {
    if (throwError)
       throw new InvalidOperationException();
  }
  public async Task ReadFileAsync(string fileName)
  {
    using(var file = File.OpenText(fileName))
      return await file.ReadToEndAsync();
  }}

Test Method

[TestMethod]
[ExpectedException(typeof(FileNotFoundException))]
public async Task  FileRead_ConstructorOperationNotValidAndFileNotFoundException_NotFound()
{
  _fileReaderAsync = new FileReaderAsync(true);
  var data = await _fileReaderAsync.ReadFileAsync(_InvalidPath);
  Assert.IsNotNull(data);
  Assert.IsTrue(data.Length > 0);
}

This Test method would fail as one would expect. The Expected Exception specified is FileNotFoundException, however, the call to the class Constructor would raise an InvalidOperationException.

Handling Exceptions : Better Approach

What we need is mechanism to specify the expected exception for each specified line of code. NUnit was probably the first framework to make that possible, but MS Test was quick to catch up. Let’s write the Test Method with the new approach.

[TestMethod]
public async Task  FileRead_ConstructorOperationNotValidAndFileNotFoundException_Found()
{
  Assert.ThrowsException(()=>_fileReaderAsync  = new FileReaderAsync(true));
  // Reinitialize as constructor has failed
  _fileReaderAsync = new FileReaderAsync();
  await Assert.ThrowsExceptionAsync(async () =>  await _fileReaderAsync.ReadFileAsync(_InvalidPath));
}

This approach would ensure that the Test Method would pass. As you might have already noticed the Assert.ThrowException has a async counterpart, to take care of the async methods.

This approach is far better than the ExpectedException method as we have already seen. Code samples described in this post is available in my Github.

Mutation Testing

Even with TDD, the quality or coverage of the existing Unit Tests are always a concern for the developer. “Have I done enough ?” That’s one question that haunts him throughout.

Mutation Testing is a structural testing method that attempts to add more value to Unit Tests by aiding in identifying the ‘misses’ in Unit Tests. The central idea behind is to write Mutant of existing code, each of the mutant varying each other by a single change.

For example, Let’s think about the following code.

int TestMethod(int CurrentValue)
{
if(CurrentValue<=3)
return 100;
else
return 200;
}

One of the mutants of the method would be replacing the decision parameter “”.  The Mutant would look like following.

int TestMethod(int CurrentValue)
{
if(CurrentValue>3)
return 100;
else
return 200;
}

Idea behind the Mutation Testing Approach is to ensure that the Testing Data set is near complete and adequate to identify all possibles that the code could have.  The focus would then shift to create Test Cases that would kill as much as mutants as possible.

The mutants can be broadly categorized under 3 categories.
  • Decision Mutations  – Conditions are changed for checking design errors, typically relational, logical and arithmetic operators.
  • Value Mutations  – Typically focused on changing the values (especially constants) to detect the impact/error
  • Statement Mutations – Statements are removed/duplicated.
Thinking about it, Mutation Testing looks a must for any developer if one was turn blind on the extensive resources (mutants) that could be generated. However, from the perspective of a .Net Developer (especially if you are using Visual Studio 2017), there is definite lack of tools and related documentation. Some of the tools that are available for .Net developers are
While Visual Mutator looks the most user friendly, it doesn’t seem to support Visual Studio 2017. Ninja Turtles, on other hand, suffers from lack of documentation and guidance for usage.