diff --git a/docs/azure/TOC.yml b/docs/azure/TOC.yml index 5141db1a48edb..c32d50341c3eb 100644 --- a/docs/azure/TOC.yml +++ b/docs/azure/TOC.yml @@ -61,6 +61,8 @@ href: ./sdk/logging.md - name: Pagination href: ./sdk/pagination.md + - name: Unit testing and mocking + href: ./sdk/unit-testing-mocking.md - name: Configure a proxy server href: ./sdk/azure-sdk-configure-proxy.md - name: Packages list diff --git a/docs/azure/sdk/snippets/unit-testing/AboutToExpireSecretsFinder.cs b/docs/azure/sdk/snippets/unit-testing/AboutToExpireSecretsFinder.cs new file mode 100644 index 0000000000000..072f714272ec7 --- /dev/null +++ b/docs/azure/sdk/snippets/unit-testing/AboutToExpireSecretsFinder.cs @@ -0,0 +1,29 @@ +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 GetAboutToExpireSecretsAsync() + { + List 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(); + } +} \ No newline at end of file diff --git a/docs/azure/sdk/snippets/unit-testing/Moq/AboutToExpireSecretsFinderTests_Moq.cs b/docs/azure/sdk/snippets/unit-testing/Moq/AboutToExpireSecretsFinderTests_Moq.cs new file mode 100644 index 0000000000000..ec9b1e662959e --- /dev/null +++ b/docs/azure/sdk/snippets/unit-testing/Moq/AboutToExpireSecretsFinderTests_Moq.cs @@ -0,0 +1,71 @@ +using Azure; +using Azure.Security.KeyVault.Secrets; +using Moq; + +namespace UnitTestingSampleApp.Moq; + +public class AboutToExpireSecretFinderTests_Moq +{ + [Fact] + public async Task DoesNotReturnNonExpiringSecrets() + { + // Arrange + // Create a page of enumeration results + Page page = Page.FromValues(new[] + { + new SecretProperties("secret1") { ExpiresOn = null }, + new SecretProperties("secret2") { ExpiresOn = null } + }, null, Mock.Of()); + + // Create a pageable that consists of a single page + AsyncPageable pageable = + AsyncPageable.FromPages(new[] { page }); + + // Setup a client mock object to return the pageable + var clientMock = new Mock(); + clientMock.Setup(c => c.GetPropertiesOfSecretsAsync(It.IsAny())) + .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 + 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 page = Page.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()); + + // Create a pageable that consists of a single page + AsyncPageable pageable = + AsyncPageable.FromPages(new[] { page }); + + // Setup a client mock object to return the pageable + var clientMock = new Mock(); + clientMock.Setup(c => c.GetPropertiesOfSecretsAsync(It.IsAny())) + .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 + string[] soonToExpire = await finder.GetAboutToExpireSecretsAsync(); + + // Assert + Assert.Equal(new[] { "secret1", "secret2" }, soonToExpire); + } +} diff --git a/docs/azure/sdk/snippets/unit-testing/Moq/TestSnippets_Moq.cs b/docs/azure/sdk/snippets/unit-testing/Moq/TestSnippets_Moq.cs new file mode 100644 index 0000000000000..11fe22aa7a747 --- /dev/null +++ b/docs/azure/sdk/snippets/unit-testing/Moq/TestSnippets_Moq.cs @@ -0,0 +1,100 @@ +using Azure.Security.KeyVault.Secrets; +using Azure; +using Moq; + +namespace UnitTestingSampleApp.Moq; + +public class TestSnippets_Moq +{ + public void ServiceClientSnippets() + { + // + KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret( + new SecretProperties("secret"), "secretValue"); + + Mock clientMock = new Mock(); + clientMock.Setup(c => c.GetSecret( + It.IsAny(), + It.IsAny(), + It.IsAny()) + ) + .Returns(Response.FromValue(keyVaultSecret, Mock.Of())); + + clientMock.Setup(c => c.GetSecretAsync( + It.IsAny(), + It.IsAny(), + It.IsAny()) + ) + .ReturnsAsync(Response.FromValue(keyVaultSecret, Mock.Of())); + + SecretClient secretClient = clientMock.Object; + // + } + + public void ResponseTypeSnippets() + { + // + Mock responseMock = new Mock(); + responseMock.SetupGet(r => r.Status).Returns(200); + + Response response = responseMock.Object; + // + } + + public void ResponseTypeTSnippets() + { + // + KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret( + new SecretProperties("secret"), "secretValue"); + Response response = Response.FromValue(keyVaultSecret, Mock.Of()); + // + } + + public void PaggingSnippets() + { + // + Page responsePage = Page.FromValues( + new[] { + new SecretProperties("secret1"), + new SecretProperties("secret2") + }, + continuationToken: null, + Mock.Of()); + // + + // + Page page1 = Page.FromValues( + new[] + { + new SecretProperties("secret1"), + new SecretProperties("secret2") + }, + "continuationToken", + Mock.Of()); + + Page page2 = Page.FromValues( + new[] + { + new SecretProperties("secret3"), + new SecretProperties("secret4") + }, + "continuationToken2", + Mock.Of()); + + Page lastPage = Page.FromValues( + new[] + { + new SecretProperties("secret5"), + new SecretProperties("secret6") + }, + continuationToken: null, + Mock.Of()); + + Pageable pageable = Pageable + .FromPages(new[] { page1, page2, lastPage }); + + AsyncPageable asyncPageable = AsyncPageable + .FromPages(new[] { page1, page2, lastPage }); + // + } +} diff --git a/docs/azure/sdk/snippets/unit-testing/NSubstitute/AboutToExpireSecretsFinderTests_NSubstitute.cs b/docs/azure/sdk/snippets/unit-testing/NSubstitute/AboutToExpireSecretsFinderTests_NSubstitute.cs new file mode 100644 index 0000000000000..31bc66a35a3f7 --- /dev/null +++ b/docs/azure/sdk/snippets/unit-testing/NSubstitute/AboutToExpireSecretsFinderTests_NSubstitute.cs @@ -0,0 +1,71 @@ +using Azure; +using Azure.Security.KeyVault.Secrets; +using NSubstitute; + +namespace UnitTestingSampleApp.NSubstitute; + +public class AboutToExpireSecretFinderTests_NSubstitute +{ + [Fact] + public async Task DoesNotReturnNonExpiringSecrets() + { + // Arrange + // Create a page of enumeration results + Page page = Page.FromValues(new[] + { + new SecretProperties("secret1") { ExpiresOn = null }, + new SecretProperties("secret2") { ExpiresOn = null } + }, null, Substitute.For()); + + // Create a pageable that consists of a single page + AsyncPageable pageable = + AsyncPageable.FromPages(new[] { page }); + + // Setup a client mock object to return the pageable + SecretClient clientMock = Substitute.For(); + clientMock.GetPropertiesOfSecretsAsync(Arg.Any()) + .Returns(pageable); + + // Create an instance of a class to test passing in the mock client + var finder = new AboutToExpireSecretFinder(TimeSpan.FromDays(2), clientMock); + + // Act + var 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 page = Page.FromValues(new[] + { + new SecretProperties("secret1") { ExpiresOn = now.AddDays(1) }, + new SecretProperties("secret2") { ExpiresOn = now.AddDays(2) }, + new SecretProperties("secret3") { ExpiresOn = now.AddDays(3) } + }, null,Substitute.For()); + + // Create a pageable that consists of a single page + AsyncPageable pageable = + AsyncPageable.FromPages(new[] { page }); + + // Setup a client mock object to return the pageable + SecretClient clientMock = Substitute.For(); + clientMock.GetPropertiesOfSecretsAsync(Arg.Any()) + .Returns(pageable); + + // Create an instance of a class to test passing in the mock client + var finder = new AboutToExpireSecretFinder(TimeSpan.FromDays(2), clientMock); + + // Act + var soonToExpire = await finder.GetAboutToExpireSecretsAsync(); + + // Assert + Assert.Equal(new[] { "secret1", "secret2" }, soonToExpire); + } +} diff --git a/docs/azure/sdk/snippets/unit-testing/NSubstitute/TestSnippets_NSubstitute.cs b/docs/azure/sdk/snippets/unit-testing/NSubstitute/TestSnippets_NSubstitute.cs new file mode 100644 index 0000000000000..5d4d059f6225a --- /dev/null +++ b/docs/azure/sdk/snippets/unit-testing/NSubstitute/TestSnippets_NSubstitute.cs @@ -0,0 +1,102 @@ +using Azure.Security.KeyVault.Secrets; +using Azure; +using NSubstitute; + +namespace UnitTestingSampleApp.NSubstitute; + +public class TestSnippets_NSubstitute +{ + public void ServiceClientSnippets() + { + // + KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret( + new SecretProperties("secret"), "secretValue"); + + SecretClient clientMock = Substitute.For(); + clientMock.GetSecret( + Arg.Any(), + Arg.Any(), + Arg.Any() + ) + .Returns(Response.FromValue(keyVaultSecret, Substitute.For())); + + clientMock.GetSecretAsync( + Arg.Any(), + Arg.Any(), + Arg.Any() + ) + .Returns(Response.FromValue(keyVaultSecret, Substitute.For())); + + SecretClient secretClient = clientMock; + // + } + + public void ResponseTypeSnippets() + { + // + Response responseMock = Substitute.For(); + responseMock.Status.Returns(200); + + Response response = responseMock; + // + } + + public void ResponseTypeTSnippets() + { + // + KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret( + new SecretProperties("secret"), "secretValue"); + Response response = Response.FromValue(keyVaultSecret, Substitute.For()); + // + } + + + public void PaggingSnippets() + { + // + Page responsePage = Page.FromValues( + new[] { + new SecretProperties("secret1"), + new SecretProperties("secret2") + }, + continuationToken: null, + Substitute.For()); + // + + // + Page page1 = Page.FromValues( + new[] + { + new SecretProperties("secret1"), + new SecretProperties("secret2") + }, + "continuationToken", + Substitute.For()); + + Page page2 = Page.FromValues( + new[] + { + new SecretProperties("secret3"), + new SecretProperties("secret4") + }, + "continuationToken2", + Substitute.For()); + + Page lastPage = Page.FromValues( + new[] + { + new SecretProperties("secret5"), + new SecretProperties("secret6") + }, + continuationToken: null, + Substitute.For()); + + Pageable pageable = Pageable + .FromPages(new[] { page1, page2, lastPage }); + + AsyncPageable asyncPageable = AsyncPageable + .FromPages(new[] { page1, page2, lastPage }); + // + } + +} diff --git a/docs/azure/sdk/snippets/unit-testing/NonLibrary/AboutToExpireSecretsFinderTests.cs b/docs/azure/sdk/snippets/unit-testing/NonLibrary/AboutToExpireSecretsFinderTests.cs new file mode 100644 index 0000000000000..d1c8570e3fc21 --- /dev/null +++ b/docs/azure/sdk/snippets/unit-testing/NonLibrary/AboutToExpireSecretsFinderTests.cs @@ -0,0 +1,66 @@ +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 page = Page.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 pageable = + AsyncPageable.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 page = Page.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 pageable = + AsyncPageable.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); + } +} diff --git a/docs/azure/sdk/snippets/unit-testing/NonLibrary/MockResponse.cs b/docs/azure/sdk/snippets/unit-testing/NonLibrary/MockResponse.cs new file mode 100644 index 0000000000000..099c8e63e1478 --- /dev/null +++ b/docs/azure/sdk/snippets/unit-testing/NonLibrary/MockResponse.cs @@ -0,0 +1,38 @@ +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 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? values) => + throw new NotImplementedException(); +} diff --git a/docs/azure/sdk/snippets/unit-testing/NonLibrary/MockSecretClient.cs b/docs/azure/sdk/snippets/unit-testing/NonLibrary/MockSecretClient.cs new file mode 100644 index 0000000000000..1960b2e59268a --- /dev/null +++ b/docs/azure/sdk/snippets/unit-testing/NonLibrary/MockSecretClient.cs @@ -0,0 +1,33 @@ +using Azure.Security.KeyVault.Secrets; +using Azure; +using NSubstitute.Routing.Handlers; + +namespace UnitTestingSampleApp.NonLibrary; + +public sealed class MockSecretClient : SecretClient +{ + AsyncPageable _pageable; + + // Allow a pageable to be passed in for mocking different responses + public MockSecretClient(AsyncPageable pageable) + { + _pageable = pageable; + } + + public override Response GetSecret( + string name, + string version = null, + CancellationToken cancellationToken = default) + => throw new NotImplementedException(); + + public override Task> GetSecretAsync( + string name, + string version = null, + CancellationToken cancellationToken = default) + => throw new NotImplementedException(); + + // Return the pageable that was passed in + public override AsyncPageable GetPropertiesOfSecretsAsync + (CancellationToken cancellationToken = default) + => _pageable; +} diff --git a/docs/azure/sdk/snippets/unit-testing/NonLibrary/TestSnippets.cs b/docs/azure/sdk/snippets/unit-testing/NonLibrary/TestSnippets.cs new file mode 100644 index 0000000000000..39113b2943259 --- /dev/null +++ b/docs/azure/sdk/snippets/unit-testing/NonLibrary/TestSnippets.cs @@ -0,0 +1,65 @@ +using Azure; +using Azure.Security.KeyVault.Secrets; + +namespace UnitTestingSampleApp.NonLibrary; + +public class TestSnippets +{ + public void ResponseTypeTSnippets() + { + // + KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret( + new SecretProperties("secret"), "secretValue"); + + Response response = Response.FromValue(keyVaultSecret, new MockResponse()); + // + } + + public void PagingSnippets() + { + // + Page responsePage = Page.FromValues( + new[] { + new SecretProperties("secret1"), + new SecretProperties("secret2") + }, + continuationToken: null, + new MockResponse()); + // + + // + Page page1 = Page.FromValues( + new[] + { + new SecretProperties("secret1"), + new SecretProperties("secret2") + }, + "continuationToken", + new MockResponse()); + + Page page2 = Page.FromValues( + new[] + { + new SecretProperties("secret3"), + new SecretProperties("secret4") + }, + "continuationToken2", + new MockResponse()); + + Page lastPage = Page.FromValues( + new[] + { + new SecretProperties("secret5"), + new SecretProperties("secret6") + }, + continuationToken: null, + new MockResponse()); + + Pageable pageable = Pageable + .FromPages(new[] { page1, page2, lastPage }); + + AsyncPageable asyncPageable = AsyncPageable + .FromPages(new[] { page1, page2, lastPage }); + // + } +} diff --git a/docs/azure/sdk/snippets/unit-testing/UnitTestingSampleApp.csproj b/docs/azure/sdk/snippets/unit-testing/UnitTestingSampleApp.csproj new file mode 100644 index 0000000000000..c8611a5428938 --- /dev/null +++ b/docs/azure/sdk/snippets/unit-testing/UnitTestingSampleApp.csproj @@ -0,0 +1,29 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/docs/azure/sdk/snippets/unit-testing/Usings.cs b/docs/azure/sdk/snippets/unit-testing/Usings.cs new file mode 100644 index 0000000000000..8c927eb747a6a --- /dev/null +++ b/docs/azure/sdk/snippets/unit-testing/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/docs/azure/sdk/unit-testing-mocking.md b/docs/azure/sdk/unit-testing-mocking.md new file mode 100644 index 0000000000000..07efa981494df --- /dev/null +++ b/docs/azure/sdk/unit-testing-mocking.md @@ -0,0 +1,238 @@ +--- +title: Unit testing and mocking with the Azure SDK for .NET +description: Learn techniques and tools for unit testing and mocking the Azure SDK for .NET +ms.custom: devx-track-dotnet, engagement-fy23 +ms.date: 07/05/2023 +--- + +# 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'll learn how to write unit tests for the Azure SDK for .NET that isolate your dependencies to make your tests more reliable. You'll 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](https://www.nuget.org/packages/moq/) and [NSubstitute](https://www.nuget.org/packages/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](https://azure.github.io/azure-sdk/dotnet_introduction.html#dotnet-mocking) 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](https://www.nuget.org/packages/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 (as well as other useful features that are beyond the scope of this article). + +# [Non-library](#tab/csharp) + +To create a test client instance using C# without a mocking library, 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. + +> [!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. + +:::code language="csharp" source="snippets/unit-testing/NonLibrary/MockSecretClient.cs" ::: + +# [Moq](#tab/moq) + +:::code language="csharp" source="snippets/unit-testing/Moq/TestSnippets_Moq.cs" id="MockSecretClient" ::: + +# [NSubstitute](#tab/nsubstitute) + +:::code language="csharp" source="snippets/unit-testing/NSubstitute/TestSnippets_NSubstitute.cs" id="MockSecretClient" ::: + +--- + +### 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 neither public constructors nor 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. + +```csharp +var secretProperties = new SecretProperties("secret") +{ + NotBefore = DateTimeOffset.Now +}; +``` + +To create instances of output models, a model factory is used. Most Azure SDK client libraries provide a static model factory class that ends in `ModelFactory` and contains a set of static methods to initialize the library's output model types. + +```csharp +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, . + +```csharp +// 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 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. + +## [Non-library](#tab/csharp) + +The `Response` class is abstract, which means there are a lot of members to override. Consider using a library to streamline your approach. + +:::code language="csharp" source="snippets/unit-testing/NonLibrary/MockResponse.cs" ::: + +## [Moq](#tab/moq) + +:::code language="csharp" source="snippets/unit-testing/Moq/TestSnippets_Moq.cs" id="MockResponse" ::: + +# [NSubstitute](#tab/nsubstitute) + +:::code language="csharp" source="snippets/unit-testing/NSubstitute/TestSnippets_NSubstitute.cs" id="MockResponse" ::: + +--- + +Some services also support using the type, which is a class that contains a model and the HTTP response that returned it. To create a test instance of `Response`, use the static `Response.FromValue` method: + +## [Non-library](#tab/csharp) + +:::code language="csharp" source="snippets/unit-testing/NonLibrary/TestSnippets.cs" id="MockResponseT" ::: + +## [Moq](#tab/moq) + +:::code language="csharp" source="snippets/unit-testing/Moq/TestSnippets_Moq.cs" id="MockResponseT" ::: + +# [NSubstitute](#tab/nsubstitute) + +:::code language="csharp" source="snippets/unit-testing/NSubstitute/TestSnippets_NSubstitute.cs" id="MockResponseT" ::: + +--- + +### Explore paging + +The class is used as a building block in service methods that invoke operations returning results in multiple pages. The `Page` is rarely returned from APIs directly but is useful to create the `AsyncPageable` and `Pageable` instances in the next section. To create a `Page` instance, use the `Page.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. + +## [Non-library](#tab/csharp) + +:::code language="csharp" source="snippets/unit-testing/NonLibrary/TestSnippets.cs" id="SingleResponsePage" ::: + +## [Moq](#tab/moq) + +:::code language="csharp" source="snippets/unit-testing/Moq/TestSnippets_Moq.cs" id="SingleResponsePage" ::: + +# [NSubstitute](#tab/nsubstitute) + +:::code language="csharp" source="snippets/unit-testing/NSubstitute/TestSnippets_NSubstitute.cs" id="SingleResponsePage" ::: + +--- + + and 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: + +## [Non-library](#tab/csharp) + +:::code language="csharp" source="snippets/unit-testing/NonLibrary/TestSnippets.cs" id="MultipleResponsePage" ::: + +## [Moq](#tab/moq) + +:::code language="csharp" source="snippets/unit-testing/Moq/TestSnippets_Moq.cs" id="MultipleResponsePage" ::: + +# [NSubstitute](#tab/nsubstitute) + +:::code language="csharp" source="snippets/unit-testing/NSubstitute/TestSnippets_NSubstitute.cs" id="MultipleResponsePage" ::: + +--- + +## 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. + +:::code language="csharp" source="snippets/unit-testing/AboutToExpireSecretsFinder.cs" ::: + +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](https://www.nuget.org/packages/xunit) library: + +## [Non-library](#tab/csharp) + +:::code language="csharp" source="snippets/unit-testing/NonLibrary/AboutToExpireSecretsFinderTests.cs" ::: + +## [Moq](#tab/moq) + +:::code language="csharp" source="snippets/unit-testing/Moq/AboutToExpireSecretsFinderTests_Moq.cs" ::: + +# [NSubstitute](#tab/nsubstitute) + +:::code language="csharp" source="snippets/unit-testing/NSubstitute/AboutToExpireSecretsFinderTests_NSubstitute.cs" ::: + +--- + +## Refactor your types for testability + +Classes that need to be tested should be designed for [dependency injection](/dotnet/azure/sdk/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 may be classes in your code that create their own dependencies and are not easily testable, such as the following: + +```csharp +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. + +```csharp +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: + +```csharp +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. + +## See also + +* [Dependency injection in .NET](/dotnet/core/extensions/dependency-injection) +* [Unit testing best practices](/dotnet/core/testing/unit-testing-best-practices) +* [Unit testing C# in .NET using dotnet test and xUnit](/dotnet/core/testing/unit-testing-with-dotnet-test)