Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
O teste de unidade é uma parte importante de um processo de desenvolvimento sustentável que pode melhorar a qualidade do código e evitar regressões ou bugs em seus aplicativos. No entanto, o teste de unidade apresenta desafios quando o código que você está testando executa chamadas de rede, como aquelas feitas para recursos do Azure. Os testes executados em serviços ativos podem enfrentar problemas, como latência que retarda a execução do teste, dependências de código fora do teste isolado e problemas com o gerenciamento do estado e dos custos do serviço sempre que o teste é executado. Em vez de testar em serviços ativos do Azure, substitua os clientes de serviço por implementações simuladas ou em memória. Isso evita os problemas acima e permite que os desenvolvedores se concentrem em testar a lógica do aplicativo, independentemente da rede e do serviço.
Neste artigo, você aprenderá a escrever testes de unidade para o SDK do Azure para .NET que isolam suas dependências para tornar seus testes mais confiáveis. Você também aprende como substituir componentes-chave por implementações de teste na memória para criar testes de unidade rápidos e confiáveis e veja como projetar suas próprias classes para oferecer melhor suporte ao teste de unidade. Este artigo inclui exemplos que usam Moq e NSubstitute, que são bibliotecas de simulação populares para .NET.
Compreender os clientes de serviços
Uma classe de cliente de serviço é o principal ponto de entrada para desenvolvedores em uma biblioteca do SDK do Azure e implementa a maior parte da lógica para se comunicar com o serviço do Azure. Ao testar classes de clientes de serviço em testes unitários, é importante ser capaz de criar uma instância do cliente que se comporte conforme o esperado, sem realizar quaisquer chamadas de rede.
Cada um dos clientes do SDK do Azure segue diretrizes simuladas que permitem que seu comportamento seja substituído:
- Cada cliente oferece pelo menos um construtor protegido para permitir a herança para testes.
- Todos os membros do cliente público são virtuais para permitir a substituição.
Observação
Os exemplos de código neste artigo usam tipos da biblioteca Azure.Security.KeyVault.Secrets para o serviço Azure Key Vault. Os conceitos demonstrados neste artigo também se aplicam a clientes de serviço de muitos outros serviços do Azure, como o Armazenamento do Azure ou o Barramento de Serviço do Azure.
Para criar um cliente de serviço de teste, pode-se usar uma biblioteca de simulação ou funcionalidades padrão do C#, como herança. Estruturas simuladas permitem simplificar o código que você deve escrever para substituir o comportamento do membro. (Essas estruturas também têm outros recursos úteis que estão além do escopo deste artigo.)
Para criar uma instância de cliente de teste usando C# sem uma biblioteca simulada, herde do tipo de cliente e substitua os métodos que você está chamando em seu código com uma implementação que retorna um conjunto de objetos de teste. A maioria dos clientes contém métodos síncronos e assíncronos para operações; substitua apenas aquele que o código da aplicação está chamando.
Observação
Pode ser complicado definir manualmente as classes de teste, especialmente se você precisar personalizar o comportamento de forma diferente para cada teste. Considere usar uma biblioteca como Moq ou NSubstitute para simplificar seus testes.
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;
}
Modelos de entrada e saída do cliente de serviço
Os tipos de modelo contêm os dados que estão sendo enviados e recebidos dos serviços do Azure. Existem três tipos de modelos:
- Os modelos de entrada destinam-se a ser criados e passados como parâmetros para métodos de serviço pelos desenvolvedores. Eles têm um ou mais construtores públicos e propriedades editáveis.
- Os modelos de saída são retornados apenas pelo serviço e não têm construtores públicos ou propriedades graváveis.
- Os modelos de ida e volta são menos comuns, mas são retornados pelo serviço, modificados e usados como entrada.
Para criar uma instância de teste de um modelo de entrada, use um dos construtores públicos disponíveis e defina as propriedades adicionais necessárias.
var secretProperties = new SecretProperties("secret")
{
NotBefore = DateTimeOffset.Now
};
Para criar instâncias de modelos de saída, uma fábrica de modelos é usada. As bibliotecas de cliente do SDK do Azure fornecem uma classe de fábrica de modelo estático com um ModelFactory sufixo em seu nome. A classe contém um conjunto de métodos estáticos para inicializar os tipos de modelo de saída da biblioteca. Por exemplo, a fábrica do modelo para SecretClient é SecretModelFactory:
KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret(
new SecretProperties("secret"), "secretValue");
Observação
Alguns modelos de input têm propriedades de leitura única que só são preenchidas quando o modelo é retornado pelo serviço. Nesse caso, um método de fábrica modelo estará disponível que permite definir essas propriedades. Por exemplo, 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 os tipos de resposta
A Response classe é uma classe abstrata que representa uma resposta HTTP e é retornada pela maioria dos métodos de cliente de serviço. Você pode criar instâncias de teste Response usando uma biblioteca simulada ou herança C# padrão.
A Response classe é abstrata, o que significa que há muitos membros para substituir. Considere o uso de uma biblioteca para simplificar sua abordagem.
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();
}
Alguns serviços também suportam o uso do Response<T> tipo, que é uma classe que contém um modelo e a resposta HTTP que o retornou. Para criar uma instância de teste do Response<T>, use o método estático Response.FromValue :
KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret(
new SecretProperties("secret"), "secretValue");
Response<KeyVaultSecret> response = Response.FromValue(keyVaultSecret, new MockResponse());
Explorar paginação
A Page<T> classe é usada como um bloco de construção em métodos de serviço que invocam operações que retornam resultados em várias páginas. O Page<T> é raramente retornado diretamente das APIs, mas é útil para criar as instâncias AsyncPageable<T> e Pageable<T> na seção seguinte. Para criar uma Page<T> instância, use o Page<T>.FromValues método, passando uma lista de itens, um token de continuação e o Response.
O continuationToken parâmetro é usado para recuperar a próxima página do serviço. Para fins de teste de unidade, ele deve ser definido como null para a última página e não deve estar vazio para outras páginas.
Page<SecretProperties> responsePage = Page<SecretProperties>.FromValues(
new[] {
new SecretProperties("secret1"),
new SecretProperties("secret2")
},
continuationToken: null,
new MockResponse());
AsyncPageable<T> e Pageable<T> são classes que representam coleções de modelos retornados pelo serviço em páginas. A única diferença entre eles é que um é usado com métodos síncronos, enquanto o outro é usado com métodos assíncronos.
Para criar uma instância de teste de Pageable ou AsyncPageable, use o FromPages método estático:
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 });
Escrever um teste de unidade simulado
Suponha que seu aplicativo contenha uma classe que encontre os nomes das chaves que expirarão dentro de um determinado período de tempo.
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();
}
}
Você deseja testar os seguintes comportamentos do AboutToExpireSecretFinder para garantir que eles continuem funcionando conforme o esperado:
- Os segredos sem uma data de validade definida não são devolvidos.
- Os segredos com uma data de expiração mais próxima da data atual do que o limite são devolvidos.
Ao testar a unidade, você deseja apenas que os testes de unidade verifiquem a lógica do aplicativo e não se o serviço ou a biblioteca do Azure funciona corretamente. O exemplo a seguir testa os principais comportamentos usando a popular biblioteca 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);
}
}
Refatore seus tipos para testar
As classes que precisam ser testadas devem ser projetadas para injeção de dependência, o que permite que a classe receba suas dependências em vez de criá-las internamente. Foi um processo sem dificuldade substituir a SecretClient implementação no exemplo da seção anterior porque era um dos parâmetros do construtor. No entanto, pode haver classes em seu código que criam suas próprias dependências e não são facilmente testáveis, como a seguinte classe:
public class AboutToExpireSecretFinder
{
public AboutToExpireSecretFinder(TimeSpan threshold)
{
_threshold = threshold;
_client = new SecretClient(
new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")),
new DefaultAzureCredential());
}
}
A refatoração mais simples que você pode fazer para habilitar o teste com injeção de dependência seria expor o cliente como um parâmetro e executar o código de criação padrão quando nenhum valor é fornecido. Essa abordagem permite que você torne a aula testável, mantendo a flexibilidade de usar o tipo sem muita cerimônia.
public class AboutToExpireSecretFinder
{
public AboutToExpireSecretFinder(TimeSpan threshold, SecretClient client = null)
{
_threshold = threshold;
_client = client ?? new SecretClient(
new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")),
new DefaultAzureCredential());
}
}
Outra opção é mover a criação de dependência inteiramente para o código de chamada:
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);
Essa abordagem é útil quando você deseja consolidar a criação de dependência e compartilhar o cliente entre várias classes de consumo.
Compreender os clientes do Azure Resource Manager (ARM)
Nas bibliotecas ARM, os clientes foram projetados para enfatizar seu relacionamento uns com os outros, espelhando a hierarquia de serviços. Para atingir esse objetivo, os métodos de extensão são amplamente utilizados para adicionar recursos adicionais aos clientes.
Por exemplo, existe uma máquina virtual do Azure em um grupo de recursos do Azure. O Azure.ResourceManager.Compute namespace modela a máquina virtual do Azure como VirtualMachineResource. O Azure.ResourceManager namespace modela o grupo de recursos do Azure como ResourceGroupResource. Para consultar as máquinas virtuais para um grupo de recursos, você deve escrever:
VirtualMachineCollection virtualMachineCollection = resourceGroup.GetVirtualMachines();
Como a funcionalidade relacionada à máquina virtual, como GetVirtualMachines on ResourceGroupResource, é implementada como métodos de extensão, é impossível apenas criar uma simulação do tipo e substituir o método. Em vez disso, você também terá que criar uma classe simulada para o "recurso simulável" e conectá-los.
O tipo de recurso simulável está sempre no Mocking subnamespace do método de extensão. No exemplo anterior, o tipo de recurso simulável está no Azure.ResourceManager.Compute.Mocking namespace. O tipo de recurso simulável é sempre nomeado após o tipo de recurso com "Mockable" e o nome da biblioteca como prefixos. No exemplo anterior, o tipo de recurso simulável é chamado MockableComputeResourceGroupResource, onde ResourceGroupResource é o tipo de recurso do método de extensão e Compute é o nome da biblioteca.
Um requisito adicional antes de executar o teste de unidade é simular o método GetCachedClient no tipo de recurso associado ao método de extensão. A conclusão desta etapa conecta o método de extensão e o método no tipo de recurso simulável.
Eis como funciona:
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)
{}
}