Unit testing and mocking with the Azure SDK for .NET
Unit testing is an important part of a sustainable development process that can improve code quality and prevent regressions or bugs in your apps. However, unit testing presents challenges when the code you're testing performs network calls, such as those made to Azure resources. Tests that run against live services can experience issues, such as latency that slows down test execution, dependencies on code outside of the isolated test, and issues with managing service state and costs every time the test is run. Instead of testing against live Azure services, 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, you learn how to write unit tests for the Azure SDK for .NET that isolate your dependencies to make your tests more reliable. You also learn how to replace key components with in-memory test implementations to create fast and reliable unit tests, and see how to design your own classes to better support unit testing. This article includes examples that use Moq and NSubstitute, which are popular mocking libraries for .NET.
Understand service clients
A service client class is the main entry point for developers in an Azure SDK library and implements most of the logic to communicate with the Azure service. When unit testing service client classes, it's important to be able to create an instance of the 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:
- Each client offers at least one protected constructor to allow inheritance for testing.
- All public client members are virtual to allow overriding.
Note
The code examples in this article use types from the Azure.Security.KeyVault.Secrets library for the Azure Key Vault service. The concepts demonstrated in this article also apply to service clients from many other Azure services, such as Azure Storage or Azure Service Bus.
To create a test service client, you can either use a mocking library or standard C# features such as inheritance. Mocking frameworks allow you to simplify the code that you must write to override member behavior. (These frameworks also have other useful features that are beyond the scope of this article.)
To create a test client instance using C# without a mocking library, inherit from the client type and override methods you're 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.
Note
It can be cumbersome to manually define test classes, especially if you need to customize behavior differently for each test. Consider using a library like Moq or NSubstitute to streamline your testing.
using Azure.Security.KeyVault.Secrets;
using Azure;
using NSubstitute.Routing.Handlers;
namespace UnitTestingSampleApp.NonLibrary;
public sealed class MockSecretClient : SecretClient
{
AsyncPageable<SecretProperties> _pageable;
// Allow a pageable to be passed in for mocking different responses
public MockSecretClient(AsyncPageable<SecretProperties> pageable)
{
_pageable = pageable;
}
public override Response<KeyVaultSecret> GetSecret(
string name,
string version = null,
CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public override Task<Response<KeyVaultSecret>> GetSecretAsync(
string name,
string version = null,
CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
// Return the pageable that was passed in
public override AsyncPageable<SecretProperties> GetPropertiesOfSecretsAsync
(CancellationToken cancellationToken = default)
=> _pageable;
}
Service client input and output models
Model types hold the data being sent and received from Azure services. There are three types of models:
- 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.
- Output models are only returned by the service and have no public constructors or writeable properties.
- Round-trip models are less common, but are returned by the service, modified, and used as an input.
To create a test instance of an input model, use one of the available public constructors and set the additional properties you need.
var secretProperties = new SecretProperties("secret")
{
NotBefore = DateTimeOffset.Now
};
To create instances of output models, a model factory is used. Azure SDK client libraries provide a static model factory class with a ModelFactory
suffix in its name. The class contains a set of static methods to initialize the library's output model types. For example, the model factory for SecretClient
is SecretModelFactory
:
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. For example, SecretProperties.
// CreatedOn is a read-only property and can only be
// set via the model factory's SecretProperties method.
secretPropertiesWithCreatedOn = SecretModelFactory.SecretProperties(
name: "secret", createdOn: DateTimeOffset.Now);
Explore response types
The Response class is an abstract class that represents an HTTP response and is returned by most service client methods. You can create test Response
instances using either a mocking library or standard C# inheritance.
The Response
class is abstract, which means there are many members to override. Consider using a library to streamline your approach.
using Azure.Core;
using Azure;
using System.Diagnostics.CodeAnalysis;
namespace UnitTestingSampleApp.NonLibrary;
public sealed class MockResponse : Response
{
public override int Status => throw new NotImplementedException();
public override string ReasonPhrase => throw new NotImplementedException();
public override Stream? ContentStream
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override string ClientRequestId
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
public override void Dispose() =>
throw new NotImplementedException();
protected override bool ContainsHeader(string name) =>
throw new NotImplementedException();
protected override IEnumerable<HttpHeader> EnumerateHeaders() =>
throw new NotImplementedException();
protected override bool TryGetHeader(
string name,
[NotNullWhen(true)] out string? value) =>
throw new NotImplementedException();
protected override bool TryGetHeaderValues(
string name,
[NotNullWhen(true)] out IEnumerable<string>? values) =>
throw new NotImplementedException();
}
Some services also support using the Response<T> type, which 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<KeyVaultSecret> response = Response.FromValue(keyVaultSecret, new MockResponse());
Explore paging
The Page<T> class 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 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 nonempty for other pages.
Page<SecretProperties> responsePage = Page<SecretProperties>.FromValues(
new[] {
new SecretProperties("secret1"),
new SecretProperties("secret2")
},
continuationToken: null,
new MockResponse());
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<SecretProperties> page1 = Page<SecretProperties>.FromValues(
new[]
{
new SecretProperties("secret1"),
new SecretProperties("secret2")
},
"continuationToken",
new MockResponse());
Page<SecretProperties> page2 = Page<SecretProperties>.FromValues(
new[]
{
new SecretProperties("secret3"),
new SecretProperties("secret4")
},
"continuationToken2",
new MockResponse());
Page<SecretProperties> lastPage = Page<SecretProperties>.FromValues(
new[]
{
new SecretProperties("secret5"),
new SecretProperties("secret6")
},
continuationToken: null,
new MockResponse());
Pageable<SecretProperties> pageable = Pageable<SecretProperties>
.FromPages(new[] { page1, page2, lastPage });
AsyncPageable<SecretProperties> asyncPageable = AsyncPageable<SecretProperties>
.FromPages(new[] { page1, page2, lastPage });
Write a mocked unit test
Suppose your app contains a class that finds the names of keys that will expire within a given amount of time.
using Azure.Security.KeyVault.Secrets;
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[]> GetAboutToExpireSecretsAsync()
{
List<string> secretsAboutToExpire = new();
await foreach (var secret in _client.GetPropertiesOfSecretsAsync())
{
if (secret.ExpiresOn.HasValue &&
secret.ExpiresOn.Value - DateTimeOffset.Now <= _threshold)
{
secretsAboutToExpire.Add(secret.Name);
}
}
return secretsAboutToExpire.ToArray();
}
}
You want to test the following behaviors of the AboutToExpireSecretFinder
to ensure they continue working as expected:
- Secrets without an expiry date set aren't returned.
- Secrets with an expiry date closer to the current date than the threshold are returned.
When unit testing you only want the unit tests to verify the application logic and not whether the Azure service or library works correctly. The following example tests the key behaviors using the popular xUnit library:
using Azure;
using Azure.Security.KeyVault.Secrets;
namespace UnitTestingSampleApp.NonLibrary;
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, new MockResponse());
// Create a pageable that consists of a single page
AsyncPageable<SecretProperties> pageable =
AsyncPageable<SecretProperties>.FromPages(new[] { page });
var clientMock = new MockSecretClient(pageable);
// Create an instance of a class to test passing in the mock client
var finder = new AboutToExpireSecretFinder(TimeSpan.FromDays(2), clientMock);
// Act
string[] soonToExpire = await finder.GetAboutToExpireSecretsAsync();
// 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, new MockResponse());
// Create a pageable that consists of a single page
AsyncPageable<SecretProperties> pageable =
AsyncPageable<SecretProperties>.FromPages(new[] { page });
// Create a client mock object
var clientMock = new MockSecretClient(pageable);
// Create an instance of a class to test passing in the mock client
var finder = new AboutToExpireSecretFinder(TimeSpan.FromDays(2), clientMock);
// Act
string[] soonToExpire = await finder.GetAboutToExpireSecretsAsync();
// Assert
Assert.Equal(new[] { "secret1", "secret2" }, soonToExpire);
}
}
Refactor your types for testability
Classes that need to be tested should be designed for dependency injection, which allows the class to receive its dependencies instead of creating them internally. It was a seamless process to replace the SecretClient
implementation in the example from the previous section because it was one of the constructor parameters. However, there might be classes in your code that create their own dependencies and aren't easily testable, such as the following class:
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. This approach allows you to make the class testable while still retaining the flexibility of using the type without much ceremony.
public class AboutToExpireSecretFinder
{
public AboutToExpireSecretFinder(TimeSpan threshold, SecretClient client = null)
{
_threshold = threshold;
_client = client ?? new SecretClient(
new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")),
new DefaultAzureCredential());
}
}
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.
Understand Azure Resource Manager (ARM) clients
In ARM libraries, the clients were designed to emphasize their relationship to one another, mirroring the service hierarchy. To achieve that goal, extension methods are widely used to add additional features to clients.
For example, an Azure virtual machine exists in an Azure resource group. The Azure.ResourceManager.Compute
namespace models the Azure virtual machine as VirtualMachineResource
. The Azure.ResourceManager
namespace models the Azure resource group as ResourceGroupResource
. To query the virtual machines for a resource group, you would write:
VirtualMachineCollection virtualMachineCollection = resourceGroup.GetVirtualMachines();
Because the virtual machine-related functionality such as GetVirtualMachines
on ResourceGroupResource
, is implemented as extension methods, it's impossible to just create a mock of the type and override the method. Instead, you'll also have to create a mock class for the "mockable resource" and wire them together.
The mockable resource type is always in the Mocking
sub-namespace of the extension method. In the preceding example, the mockable resource type is in the Azure.ResourceManager.Compute.Mocking
namespace. The mockable resource type is always named after the resource type with "Mockable" and the library name as prefixes. In the preceding example, the mockable resource type is named MockableComputeResourceGroupResource
, where ResourceGroupResource
is the resource type of the extension method, and Compute
is the library name.
One more requirement before you get the unit test running is to mock the GetCachedClient
method on the resource type of the extension method. Completing this step hooks up the extension method and the method on the mockable resource type.
Here's how it works:
using Azure.Core;
namespace UnitTestingSampleApp.ResourceManager.NonLibrary;
public sealed class MockMockableComputeResourceGroupResource : MockableComputeResourceGroupResource
{
private VirtualMachineCollection _virtualMachineCollection;
public MockMockableComputeResourceGroupResource(VirtualMachineCollection virtualMachineCollection)
{
_virtualMachineCollection = virtualMachineCollection;
}
public override VirtualMachineCollection GetVirtualMachines()
{
return _virtualMachineCollection;
}
}
public sealed class MockResourceGroupResource : ResourceGroupResource
{
private readonly MockableComputeResourceGroupResource _mockableComputeResourceGroupResource;
public MockResourceGroupResource(VirtualMachineCollection virtualMachineCollection)
{
_mockableComputeResourceGroupResource =
new MockMockableComputeResourceGroupResource(virtualMachineCollection);
}
internal MockResourceGroupResource(ArmClient client, ResourceIdentifier id) : base(client, id)
{}
public override T GetCachedClient<T>(Func<ArmClient, T> factory) where T : class
{
if (typeof(T) == typeof(MockableComputeResourceGroupResource))
return _mockableComputeResourceGroupResource as T;
return base.GetCachedClient(factory);
}
}
public sealed class MockVirtualMachineCollection : VirtualMachineCollection
{
public MockVirtualMachineCollection()
{}
internal MockVirtualMachineCollection(ArmClient client, ResourceIdentifier id) : base(client, id)
{}
}