Unit testing and mocking with Azure SDK .NET

Pavel Krymets

Pavel

Unit testing is an important part of a sustainable development process because it enables developers to prevent regressions.

Things get complicated when the code you are testing communicates with an Azure service over a network. How can you reliably test code with external dependencies?

One option is to provision required Azure resources and run tests using them.

This is expensive because every developer needs their own set of resources. It’s slow because having every test communicate over a network makes them take longer. It’s hard to manage because resources have to be in a specific state before starting a test.

A better option is to replace the service clients with mocked or in-memory implementations. This avoids the above issues and lets developers focus on testing their application logic, independent from the network and service.

In this article, we’ll show you how to use the Azure SDK to write great unit tests that isolate your dependencies to make your tests more reliable.

First, we’ll show you how many of the Azure SDK building blocks have been designed to be replaced with an in-memory test implementation. Then, we’ll show you how the pieces come together into a fast and reliable unit test. Finally, we’ll provide some design tips to help you design your own classes to better support unit testing.

Service clients

A service client is the main entry point for developers in an Azure SDK library. Because a client type implements most of the “live” logic that communicates with an Azure service, it’s important to be able to create an instance of a client that behaves as expected without making any network calls.

Each of the Azure SDK clients follows mocking guidelines that allow their behavior to be overridden:

  1. Each client offers at least one protected constructor to allow inheritance for testing.
  2. All public client members are virtual to allow overriding.

NOTE: Demonstrations in the article are using types from the Azure.Security.KeyVault.Secrets library for illustrations. It is a client library for the Azure Key Vault service. You can find the reference documentation for it at docs.microsoft.com.

To create a test client instance, inherit from the client type and override methods you are calling in your code with an implementation that returns a set of test objects. Most clients contain both synchronous and asynchronous methods for operations; override only the one your application code is calling.

public class MockSecretClient : SecretClient 
{ 
    public MockSecretClient() {}

    public override Response<KeyVaultSecret> GetSecret(string name, string version = null, CancellationToken cancellationToken = default) => ...;
    public override Task<Response<KeyVaultSecret>> GetSecretAsync(string name, string version = null, CancellationToken cancellationToken = default) => ...;

}

SecretClient secretClient = new MockSecretClient();

It can be cumbersome to define a test instance of the class, especially if you need to customize behavior differently for each test. Mocking frameworks allow you to simplify the code that you must write to override member behavior (as well as other useful features that are beyond the scope of this article). We’ll illustrate this set of examples using a popular .NET mocking framework, Moq.

To create a test client instance using Moq:

Mock<SecretClient> clientMock = new Mock<SecretClient>();
clientMock.Setup(c => c.GetSecret(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
        .Returns(...);

clientMock.Setup(c => c.GetSecretAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
        .ReturnsAsync(...);

SecretClient secretClient = clientMock.Object;

NOTE: When created using a parameterless constructor, the client is not fully initialized leaving client behavior undefined. In practice, this means that most will throw an exception if called without an overridden implementation.

Model types

Model types hold the data being sent and received from Azure services. There are two main kinds of models: input and output.

Input models are intended to be created and passed as parameters to service methods by developers. They have one or more public constructors and writeable properties.

To create a test instance of an input model use one of the available public constructors and set additional properties you need.

SecretProperties secretProperties = new SecretProperties("secret"); secretProperties.NotBefore = DateTimeOffset.Now;

Output models are only returned by the service and have neither public constructors nor writeable properties.

To create instances of output models, a model factory is used. For most Azure SDK client libraries, the model factory is a static class that ends in ModelFactory and contains a set of static methods to create and initialize the library’s output model types.

Creating an output model using a model factory:

KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret(new SecretProperties("secret"), "secretValue");

NOTE: Some input models have read-only properties that are only populated when the model is returned by the service. In this case, a model factory method will be available that allows setting these properties.

// CreatedOn is a read-only property and can only be set via a model factory SecretProperties        
secretPropertiesWithCreatedOn = SecretModelFactory.SecretProperties(name: "secret", createdOn: DateTimeOffset.Now);

Response

The Response class is an abstract class that represents an HTTP response and is a part of almost all types returned by client methods.

To create a test Response instance, one approach is to manually subclass Response and override the abstract members with test implementations.

public class TestResponse : Response
{
    protected override bool TryGetHeader(string name, out string value) => ...;
    protected override bool TryGetHeaderValues(string name, out IEnumerable<string> values) => ...;
    protected override bool ContainsHeader(string name) => ...;
    protected override IEnumerable<HttpHeader> EnumerateHeaders() => ...;

    public override int Status => ...;
    public override string ReasonPhrase => ...;
    public override Stream ContentStream => ...;
    public override string ClientRequestId => ...;

    public override void Dispose() => ...;
}

This approach has the disadvantage that because the Response is abstract there are a lot of members to override. Using the Moq version is simpler and more concise:

Mock responseMock = new Mock(); responseMock.SetupGet(r => r.Status).Returns(200);

Response response = responseMock.Object;

To create an instance of Response without defining any behaviors:

Response response = Mock.Of<Response>();

Response<T>

The Response<T> is a class that contains a model and the HTTP response that returned it.

To create a test instance of Response<T> use the static Response.FromValue method:

KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret(new SecretProperties("secret"), "secretValue"); Response response = Response.FromValue(keyVaultSecret, new TestResponse());

Or using Moq:

KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret(new SecretProperties("secret"), "secretValue");
Response<KeyVaultSecret> response = Response.FromValue(keyVaultSecret, Mock.Of<Response>());

Page<T>

The Page<T> is used as a building block in service methods that invoke operations returning results in multiple pages. The Page<T> is rarely returned from APIs directly but is useful to create the AsyncPageable<T> and Pageable<T> instances we’ll discuss in the next section. To create a Page<T> instance, use the Page<T>.FromValues method, passing a list of items, a continuation token, and the Response.

The continuationToken parameter is used to retrieve the next page from the service. For unit testing purposes, it should be set to null for the last page and should be non-empty for other pages.

Page responsePage = Page.FromValues( new[] { new SecretProperties("secret1"), new SecretProperties("secret2") }, continuationToken: null, new TestResponse());

Or using a Moq:

Page<SecretProperties> responsePage = Page<SecretProperties>.FromValues(
    new[] {
        new SecretProperties("secret1"),
        new SecretProperties("secret2")
    },
    continuationToken: null,
    Mock.Of<Response>());

AsyncPageable<T> and Pageable<T>

AsyncPageable<T> and Pageable<T> are classes that represent collections of models returned by the service in pages. The only difference between them is that one is used with synchronous methods while the other is used with asynchronous methods.

To create a test instance of Pageable or AsyncPageable, use the FromPages static method:

Page page1 = Page.FromValues(new[] { new SecretProperties("secret1"), new SecretProperties("secret2") }, "continuationToken", Mock.Of<Response>());

Page page2 = Page.FromValues(new[] { new SecretProperties("secret3"), new SecretProperties("secret4") }, "continuationToken2", Mock.Of<Response>());

Page lastPage = Page.FromValues(new[] { new SecretProperties("secret5"), new SecretProperties("secret6") }, continuationToken: null, Mock.Of<Response>());

Pageable pageable = Pageable.FromPages(new[] { page1, page2, lastPage });

AsyncPageable asyncPageable = AsyncPageable.FromPages(new[] { page1, page2, lastPage }); )

Writing a Mocked Test

Now that we’ve learned about the building blocks, let’s put them together and write a test for some example code that uses an Azure client.

The class we will test finds the names of keys that will expire within a given amount of time.

public class AboutToExpireSecretFinder
{
    private readonly TimeSpan _threshold;
    private readonly SecretClient _client;

    public AboutToExpireSecretFinder(TimeSpan threshold, SecretClient client)
    {
        _threshold = threshold;
        _client = client;
    }

    public async Task<string[]> GetAboutToExpireSecrets()
    {
        List<string> secretsAboutToExpire = new List<string>();

        await foreach (var secret in _client.GetPropertiesOfSecretsAsync())
        {
            if (secret.ExpiresOn != null &&
                secret.ExpiresOn.Value - DateTimeOffset.Now <= _threshold)
            {
                secretsAboutToExpire.Add(secret.Name);
            }
        }

        return secretsAboutToExpire.ToArray();
    }
}

Since, in general, we only want our unit tests to test the application logic and not whether the Azure service or SDK works correctly we are going to implement tests for the following scenarios:

  1. Secrets that don’t have an expiry date set are not returned.
  2. Secrets with an expiry date closer to the current date than the threshold are returned.

This is the test class that uses Moq and a popular unit testing framework xUnit:

public class AboutToExpireSecretFinderTests { [Fact] public async Task DoesNotReturnNonExpiringSecrets() 
{ // Arrange

    // Create a page of enumeration results
    Page<SecretProperties> page = Page<SecretProperties>.FromValues(new[]
    {
        new SecretProperties("secret1") { ExpiresOn = null },
        new SecretProperties("secret2") { ExpiresOn = null }
    }, null, Mock.Of<Response>());

    // Create a pageable that consists of a single page
    AsyncPageable<SecretProperties> pageable = AsyncPageable<SecretProperties>.FromPages(new [] { page });

    // Setup a client mock object to return the pageable when GetPropertiesOfSecretsAsync is called
    var clientMock = new Mock<SecretClient>();
    clientMock.Setup(c => c.GetPropertiesOfSecretsAsync(It.IsAny<CancellationToken>()))
        .Returns(pageable);

    // Create an instance of a class to test passing in the mock client
    var finder = new AboutToExpireSecretFinder(TimeSpan.FromDays(2), clientMock.Object);

    // Act
    var soonToExpire = await finder.GetAboutToExpireSecrets();

    // Assert
    Assert.Empty(soonToExpire);
}


[Fact]
public async Task ReturnsSecretsThatExpireSoon()
{
    // Arrange

    // Create a page of enumeration results
    DateTimeOffset now = DateTimeOffset.Now;
    Page<SecretProperties> page = Page<SecretProperties>.FromValues(new[]
    {
        new SecretProperties("secret1") { ExpiresOn = now.AddDays(1) },
        new SecretProperties("secret2") { ExpiresOn = now.AddDays(2) },
        new SecretProperties("secret3") { ExpiresOn = now.AddDays(3) }
    }, null, Mock.Of<Response>());

    // Create a pageable that consists of a single page
    AsyncPageable<SecretProperties> pageable = AsyncPageable<SecretProperties>.FromPages(new [] { page });

    // Setup a client mock object to return the pageable when GetPropertiesOfSecretsAsync is called
    var clientMock = new Mock<SecretClient>();
    clientMock.Setup(c => c.GetPropertiesOfSecretsAsync(It.IsAny<CancellationToken>()))
        .Returns(pageable);

    // Create an instance of a class to test passing in the mock client
    var finder = new AboutToExpireSecretFinder(TimeSpan.FromDays(2), clientMock.Object);

    // Act
    var soonToExpire = await finder.GetAboutToExpireSecrets();

    // Assert
    Assert.Equal(new[] {"secret1", "secret2"}, soonToExpire);
}

Designing your types for testability

We’ve shown you how Azure SDK types are designed for easy mocking. Now let’s talk about how you can use the same principles to design your own easily mockable types.

To make testing your classes easier, they should be designed with the ability to replace their dependency implementations.

It was easy to replace the SecretClient implementation in the example from the previous section because it was one of the constructor parameters. But what if you find yourself working with a class that creates its own dependencies that you can’t modify?

public class AboutToExpireSecretFinder
{
    public AboutToExpireSecretFinder(TimeSpan threshold)
    {
        _threshold = threshold;
        _client = new SecretClient(new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")), new DefaultAzureCredential());
    }
}

The simplest refactoring you can do to enable testing with dependency injection would be to expose the client as a parameter and run default creation code when no value is provided:

public class AboutToExpireSecretFinder
{
    public AboutToExpireSecretFinder(TimeSpan threshold, SecretClient client = null) 
    { 
        \_threshold = threshold; 
        \_client = client ?? new SecretClient(new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")), new DefaultAzureCredential());
    }
}

This approach allows you to make the class testable while still retaining the flexibility of using the type without much ceremony.

Another option is to move the dependency creation entirely into the calling code:

public class AboutToExpireSecretFinder
{
    public AboutToExpireSecretFinder(TimeSpan threshold, SecretClient client)
    {
        _threshold = threshold;
        _client = client;
    }
}

var secretClient = new SecretClient(new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")), new DefaultAzureCredential());
var finder = new AboutToExpireSecretFinder(TimeSpan.FromDays(2), secretClient);

This approach is useful when you would like to consolidate the dependency creation and share the client between multiple consuming classes.

NOTE: This technique where the class receives its dependencies instead of creating them internally is called dependency injection.

Conclusion

As we’ve discussed in this article, the Azure SDKs are designed to work great with unit testing and make developers productive when writing tests. Each building block of an Azure SDK has a way to create a test instance with a customized behavior required for a test. Combining these building blocks with well-structured application code will allow you to write fast and repeatable unit tests and prevent regressions in your applications.

Azure SDK Blog Contributions

Thank you for reading this Azure SDK blog post! We hope that you learned something new and welcome you to share this post. We are open to Azure SDK blog contributions. Please contact us at azsdkblog@microsoft.com with your topic and we’ll get you setup as a guest blogger.

0 comments

Comments are closed. Login to edit/delete your existing comments