Unit Testing AWS SDKs in .NET core

Automated unit testing is essential for production systems, you probably don't need to be told that! If you're programming in C# you're probably familiar with Dependency Injection and the Inversion of Control (IoC) pattern.

Sometimes you come across third-party libraries that don't provide interfaces, such as AWS SDKs. How would you unit test a part of your system that relied on these libraries? Easy! By combining concrete dependency and a little re-write of your system under test.

Let's look at the AWS IoT Core SDKs. There are AWS SDKs for .NET. The documentation will feel light-on if you don't have AWS IoT experience already, and sometimes there aren't even C# examples in the developer guides.

Dependencies

To run these examples I'm using a .NET core 3.1 project with the following package:

  • AWSSDK.IoT 3.7.2.19

I'm using a separate class-library project with the following packages for unit-testing:

Using AWS Iot Core SDKs

AWS provides various IoT Core endpoints depending on what you want to do. For these examples I will be using the Control Plane. The general steps for using these AWS SDKs are as follows:

  1. instantiate a client
  2. prepare the request for one of the client's supported methods
  3. call the method on the client
  4. interpret the response

For example, to get information on an IoT "thing", you might do the following:

// my function for querying things
public async Task<string> GetThingType(string deviceKey, CancellationToken cancellationToken)
{
    using var client = new AmazonIoTClient(); // STEP 1
    var request = new DescribeThingRequest {ThingName = deviceKey}; // STEP 2
    DescribeThingResponse response = await client.DescribeThingAsync(request, cancellationToken); // STEP 3
    if (response.HttpStatusCode == HttpStatusCode.OK)
    {
        return response.ThingTypeName; // STEP 4
    }
    // log error, throw exception, etc.
    return "";
}

The Problem

Now how do you unit test this? Even if you put GetThingType() behind an interface, you can't really mock out the AmazonIoTClient. Your test would attempt to connect to AWS IoT Core. The pointer to the fact that something might be wrong with this design ("code smell") is the new keyword. Don't get me wrong, there's nothing wrong with using new, but in this case, AmazonIoTClient is an infrastructure style of service/dependency, not a POCO, and dependencies (using IoC) should be provided to our system, not instantiated by our system.

The Solution

So let's redesign this by setting up AmazonIoTClient as a dependency that can be injected. In Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
   // ...
   services.AddSingleton<AmazonIoTClient>();
   services.AddScoped<IMyAwsIotService, MyAwsIotService>();
}

Now setup your service class for constructor injection:

public class MyAwsIotService : IMyAwsIotService
{
    private readonly AmazonIoTClient _iotClient;

    public MyAwsIotService(AmazonIoTClient amazonIoTClient)
    {
        _iotClient = amazonIoTClient
    }

    async Task<string> IMyAwsIotService.GetThingType(string deviceKey, CancellationToken cancellationToken)
    {
        // ...
    }
}

And finally, in the GetThingType function, remove using var client = new AmazonIoTClient(); (Step 1) and replace client with our injected _iotClient. Your final function will look something like this:

// my function for querying things
async Task<string> IMyAwsIotService.GetThingType(string deviceKey, CancellationToken cancellationToken)
{
    var request = new DescribeThingRequest {ThingName = deviceKey}; // STEP 2
    DescribeThingResponse response = await _iotClient.DescribeThingAsync(request, cancellationToken); // STEP 3
    if (response.HttpStatusCode == HttpStatusCode.OK)
    {
        return response.ThingTypeName; // STEP 4
    }
    // log error, throw exception, etc.
    return "";
}

Unit Testing

Now comes the cool bit. In another class-library project, install the Mock and nunit dependencies above. Create a class for testing (I call it MyAwsIotServiceTests):

[TestFixture]
public class MyAwsIotServiceTests
{
    // System under tests
    private IMyAwsIotService _service;
    // services
    private Mock<AmazonIoTClient> _iotClient;

    [SetUp]
    public void Setup()
    {
        _iotClient = new Mock<AmazonIoTClient>();
        AWSConfigs.AWSRegion = "ap-southeast-2"; // required for unit tests on build server which doesn't have AWS credentials/profile

        _service = new MyAwsIotService(_iotClient.Object);
    }

    [Test]
    public async Task HandlerExecutes()
    {
        // Arrange
        var token = new CancellationToken();

        _iotClient.Setup(s => s.DescribeThingAsync(It.IsAny<DescribeThingRequest>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new DescribeThingResponse
            {
                ThingTypeName = "someThingType",
                HttpStatusCode = HttpStatusCode.OK
            });

        // Act
        string type = await _service.GetThingType("abcd1234", token);

        // Assert
        Assert.AreEqual("someThingType", type);
    }
}

This will test your service, and ensure that the ThingTypeName is returned. In practice you would have lots of tests for various HttpStatusCode responses and other things, such as when an exception is thrown. In this case you may want to ensure your service catches the error and returns the empty string:

[Test]
public async Task NoSuchThing()
{
    // Arrange
    var token = new CancellationToken();

    _iotClient.Setup(s => s.DescribeThingAsync(It.IsAny<DescribeThingRequest>(), It.IsAny<CancellationToken>()))
        .Throws(new Amazon.IoT.Model.ResourceNotFoundException("uh-oh"));

    // Act
    string type = await _service.GetThingType("abcd1234", token);

    // Assert
    Assert.AreEqual("", type);
}

Conclusion

I hope this has been helpful in both connecting to IoT Core (as there are few examples for .NET core) and for mocking your services that may call AWS SDKs.

Let me know what SDKs you use and how you might test differently!