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.

Design Patterns : Bridge Pattern

The essence of Bridge Pattern lies in the fact that it not just decouples the abstraction from implementation, but in doing so, allows both to evolve independently. Let’s consider the examples of smartphones.

The underlying OS (for sake of example, let’s assume its Android) improving every year. The Android has seen atleast 7 versions in past decade. At the same time, the hardware has also evolved, giving way to new age smartphones. The Abstraction and Implementation has evolved independently here. Let’s mock the scenario using Bridge Pattern.

public interface IAndroid
{
  void Call(string name);
  void Recieve();
  void SendMessage(string name);
  void RecieveMessage();
}
public class KitKat : IAndroid
{
  public void Call(string name) => Console.WriteLine($"Calling {name} using  KitKat");
  public void Recieve() => Console.WriteLine("Recieve using KitKat");
  public void RecieveMessage()=> Console.WriteLine("Recieve Message using  KitKat");
  public void SendMessage(string name) => Console.WriteLine($"Send {name}  Message using KitKat");
}
public class Lollipop : IAndroid
{
  public void Call(string name) => Console.WriteLine($"Calling {name} using  Lollipop");
  public void Recieve() => Console.WriteLine("Recieve using Lollipop");
  public void RecieveMessage() => Console.WriteLine("Recieve Message using  Lollipop");
  public void SendMessage(string name) => Console.WriteLine($"Send {name}  Message using Lollipop");
}
public class Nougat : IAndroid
{
  public void Call(string name) => Console.WriteLine($"Calling {name} using  Nougat");
  public void Recieve() => Console.WriteLine("Recieve using Nougat");
  public void RecieveMessage() => Console.WriteLine("Recieve Message using  Nougat");
  public void SendMessage(string name) => Console.WriteLine($"Send {name}  Message using Nougat");
}

Evolution of Abstraction which Customer/Client sees, the Phone , can be seen in following code.

public class SmartPhone
{
  protected IAndroid _android;
  public SmartPhone(IAndroid android) => _android = android;
  public virtual void Phone(string name) => _android.Call(name);
  public virtual void SendMessage(string name) =>  _android.SendMessage(name);
}
public class NewAgeSmartPhone : SmartPhone
{
  public NewAgeSmartPhone(IAndroid android) : base(android) { }
  public override void Phone(string name)
  {
    Console.WriteLine("New Age Phone Call, Optimization started");
    base._android.Call(name);
  }
  public override void SendMessage(string name)
  {
    Console.WriteLine("New Age Message");
    _android.SendMessage(name);
  }
}

 

Client Code can be shown as following

static void Main(string[] args)
{
  var phone = new SmartPhone(new KitKat());
  var newAgePhone = new NewAgeSmartPhone(new KitKat());
  phone.Phone("Wife");
  newAgePhone.Phone("Wife");
}

 

As seen in the code above, the Bridge Pattern allows the Abstraction and Implementation to vary and evolve independently. Complete Code Sample in this example can found in my GitHub