Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Модульное тестирование является важной частью процесса устойчивого развития, который может улучшить качество кода и предотвратить регрессию или ошибки в приложениях. Однако модульное тестирование вызывает проблемы, когда тестируемый код выполняет сетевые вызовы, такие как те, которые были сделаны в ресурсы Azure. Тесты, выполняемые с использованием активных сервисов, могут испытывать проблемы, такие как задержка, замедляющая выполнение теста, зависимости от кода за пределами изолированного теста, а также проблемы с управлением состоянием и затратами на сервис при каждом запуске теста. Вместо тестирования в динамических службах Azure замените клиенты служб макетами или реализацией в памяти. Это позволяет разработчикам избегать указанных выше проблем и сосредоточиться на тестировании логики приложения независимо от сети и службы.
В этой статье вы узнаете, как создавать модульные тесты для пакета SDK Azure для .NET, которые изолируют зависимости, чтобы сделать тесты более надежными. Вы также узнаете, как заменить ключевые компоненты реализацией тестов в памяти для создания быстрых и надежных модульных тестов и узнать, как создавать собственные классы для повышения поддержки модульного тестирования. В этой статье приведены примеры, использующие Moq и NSubstitute, которые являются популярными библиотеками макетирования для .NET.
Общие сведения о клиентах служб
Клиентский класс службы — это основная точка входа для разработчиков в библиотеке пакета SDK Azure и реализует большую часть логики для взаимодействия со службой Azure. При использовании клиентских классов службы модульного тестирования важно иметь возможность создавать экземпляр клиента, который ведет себя должным образом без каких-либо сетевых вызовов.
Каждый из клиентов SDK Azure следует руководствам по имитации, которые позволяют переопределить их поведение.
- Каждый клиент предлагает по крайней мере один защищенный конструктор, чтобы разрешить наследование для тестирования.
- Все общедоступные члены клиента являются виртуальными, чтобы разрешить переопределение.
Замечание
Примеры кода в этой статье используют типы из библиотеки Azure.Security.KeyVault.Secret для службы Azure Key Vault. Основные понятия, описанные в этой статье, также применяются к клиентам служб из многих других служб Azure, таких как служба хранилища Azure или служебная шина Azure.
Чтобы создать клиент тестовой службы, можно использовать макетную библиотеку или стандартные функции C#, такие как наследование. Макетные платформы позволяют упростить код, который необходимо написать для переопределения поведения члена. (Эти платформы также имеют другие полезные функции, которые выходят за рамки этой статьи.)
Чтобы создать тестовый экземпляр клиента с помощью C# без использования имитационной библиотеки, наследуйте от типа клиента и переопределите методы, вызываемые в вашем коде, с реализацией, которая возвращает набор тестовых объектов. Большинство клиентов содержат синхронные и асинхронные методы для операций. Переопределяйте только тот, который вызывает ваш код приложения.
Замечание
Это может быть сложно, чтобы вручную определить классы тестов, особенно если необходимо настроить поведение по-разному для каждого теста. Рекомендуется использовать библиотеку, например Moq или NSubstitute, чтобы упростить тестирование.
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;
}
Модели ввода и вывода клиента службы
Типы моделей содержат данные, отправляемые и полученные из служб Azure. Существует три типа моделей:
- Входные модели предназначены для создания и передаче в качестве параметров в методы обслуживания разработчиками. У них есть один или несколько открытых конструкторов и доступных для записи свойств.
- Выходные модели возвращаются только службой и не имеют открытых конструкторов или записываемых свойств.
- Модели кругового пути менее распространены, но возвращаются службой, изменяются и используются в качестве входных данных.
Чтобы создать тестовый экземпляр входной модели, используйте один из доступных общедоступных конструкторов и задайте необходимые дополнительные свойства.
var secretProperties = new SecretProperties("secret")
{
NotBefore = DateTimeOffset.Now
};
Для создания экземпляров выходных моделей используется фабрика моделей. Клиентские библиотеки Пакета SDK Azure предоставляют класс фабрики статических моделей с суффиксом ModelFactory в его имени. Класс содержит набор статических методов для инициализации типов выходной модели библиотеки. Например, фабрика по созданию моделей для SecretClient является SecretModelFactory:
KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret(
new SecretProperties("secret"), "secretValue");
Замечание
Некоторые входные модели имеют свойства только для чтения, которые заполняются только при возврате модели службой. В этом случае будет доступен метод фабрики моделей, позволяющий задать эти свойства. Например: 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);
Изучение типов ответов
Класс Response является абстрактным классом, который представляет http-ответ и возвращается большинством методов клиента службы. Вы можете создавать тестовые Response экземпляры, используя библиотеку для создания имитации или стандартное наследование C#.
Класс Response является абстрактным, что означает, что существует множество элементов для переопределения. Рассмотрите возможность использования библиотеки для упрощения подхода.
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();
}
Некоторые службы также поддерживают использование Response<T> типа, который является классом, который содержит модель и ответ HTTP, возвращаемый им. Чтобы создать тестовый экземпляр Response<T>, используйте статический Response.FromValue метод:
KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret(
new SecretProperties("secret"), "secretValue");
Response<KeyVaultSecret> response = Response.FromValue(keyVaultSecret, new MockResponse());
Изучите разбиение на страницы
Класс Page<T> используется в качестве стандартного блока в методах службы, которые вызывают операции, возвращающие результаты на нескольких страницах. Редко Page<T> возвращается из API напрямую, но они полезны для создания экземпляров AsyncPageable<T> и Pageable<T> в следующем разделе. Чтобы создать Page<T> экземпляр, используйте метод Page<T>.FromValues, передав список элементов, маркер продолжения и Response.
Параметр continuationToken используется для получения следующей страницы из службы. В целях модульного тестирования оно должно быть установлено на null для последней страницы и должно быть непустым для других страниц.
Page<SecretProperties> responsePage = Page<SecretProperties>.FromValues(
new[] {
new SecretProperties("secret1"),
new SecretProperties("secret2")
},
continuationToken: null,
new MockResponse());
AsyncPageable<T> и Pageable<T> являются классами, представляющими коллекции моделей, возвращаемых службой на страницах. Единственное различие между ними заключается в том, что один используется с синхронными методами, а другой используется с асинхронными методами.
Чтобы создать тестовый экземпляр Pageable или AsyncPageableиспользуйте статический FromPages метод:
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 });
Создание макетного модульного теста
Предположим, что приложение содержит класс, который находит имена ключей, срок действия которого истекает в течение определенного периода времени.
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();
}
}
Вы хотите проверить следующее поведение AboutToExpireSecretFinder , чтобы убедиться, что они продолжают работать должным образом:
- Секреты без установленного срока действия не возвращаются.
- Секреты с датой истечения срока действия ближе к текущей дате, чем пороговое значение, возвращаются.
При модульном тестировании требуется только модульные тесты, чтобы проверить логику приложения, а не правильность работы службы или библиотеки Azure. В следующем примере проверяется поведение ключей с помощью популярной библиотеки xUnit :
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);
}
}
Перефакторизуйте ваши типы для упрощения тестирования
Классы, которые необходимо протестировать, должны быть разработаны для внедрения зависимостей, что позволяет классу получать свои зависимости вместо их внутреннего создания. Процесс замены SecretClient в примере из предыдущего раздела прошёл без труда, так как эта реализация была одним из параметров конструктора. Однако в коде могут быть классы, которые создают собственные зависимости и не легко тестируются, например следующий класс:
public class AboutToExpireSecretFinder
{
public AboutToExpireSecretFinder(TimeSpan threshold)
{
_threshold = threshold;
_client = new SecretClient(
new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")),
new DefaultAzureCredential());
}
}
Самый простой рефакторинг, который вы можете сделать для включения тестирования с внедрением зависимостей, заключается в предоставлении клиента в качестве параметра и выполнении кода создания по умолчанию, если значение не предоставлено. Этот подход позволяет сделать класс тестируемым, при этом сохраняя гибкость использования типа без лишних формальностей.
public class AboutToExpireSecretFinder
{
public AboutToExpireSecretFinder(TimeSpan threshold, SecretClient client = null)
{
_threshold = threshold;
_client = client ?? new SecretClient(
new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")),
new DefaultAzureCredential());
}
}
Другим вариантом является перемещение создания зависимостей полностью в вызывающий код:
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);
Этот подход полезен при консолидации создания зависимостей и совместного использования клиента между несколькими используемыми классами.
Общие сведения о клиентах Azure Resource Manager (ARM)
В библиотеках ARM клиенты были призваны подчеркнуть их связь друг с другом, зеркально отражая иерархию служб. Для достижения этой цели методы расширения широко используются для добавления дополнительных функций клиентам.
Например, виртуальная машина Azure существует в группе ресурсов Azure. Пространство имен Azure.ResourceManager.Compute моделирует виртуальную машину Azure в виде VirtualMachineResource. Пространство Azure.ResourceManager имен моделирует группу ресурсов Azure как ResourceGroupResource. Чтобы запросить виртуальные машины для группы ресурсов, необходимо написать:
VirtualMachineCollection virtualMachineCollection = resourceGroup.GetVirtualMachines();
Так как функциональные возможности, связанные с виртуальной машиной, такие как GetVirtualMachinesResourceGroupResource, реализованы как методы расширения, невозможно просто создать макет типа и переопределить метод. Вместо этого вам также придется создать класс макета для "имитируемого ресурса" и связать их.
Тип мокируемого ресурса всегда находится в Mocking подпространстве имен метода расширения. В предыдущем примере тип ресурса для макета находится в Azure.ResourceManager.Compute.Mocking пространстве имен. Тип ресурса, который можно имитировать, всегда называется по типу ресурса, с добавлением "Mockable" и имени библиотеки в качестве префикса. В предыдущем примере тип ресурса, который можно макетировать, называется MockableComputeResourceGroupResource, где ResourceGroupResource — это тип ресурса для метода расширения, а Compute — это имя библиотеки.
Еще одно требование перед запуском модульного теста заключается в том, чтобы имитировать GetCachedClient метод в типе ресурса метода расширения. Завершение этого шага связывает метод расширения и метод в мокаемом типе ресурса.
Вот как это работает:
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)
{
// Initialize with an empty mock to satisfy non-null contract
_mockableComputeResourceGroupResource =
new MockMockableComputeResourceGroupResource(new MockVirtualMachineCollection(client, id));
}
public override T GetCachedClient<T>(Func<ArmClient, T> factory) where T : class
{
if (typeof(T) == typeof(MockableComputeResourceGroupResource))
return (T)(object)_mockableComputeResourceGroupResource;
return base.GetCachedClient(factory);
}
}
public sealed class MockVirtualMachineCollection : VirtualMachineCollection
{
public MockVirtualMachineCollection()
{}
internal MockVirtualMachineCollection(ArmClient client, ResourceIdentifier id) : base(client, id)
{}
}