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.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s