Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Testowanie jednostkowe jest ważną częścią procesu zrównoważonego programowania, który może poprawić jakość kodu i zapobiec regresji lub usterek w aplikacjach. Jednak testowanie jednostkowe stwarza wyzwania, gdy kod, który testujesz, wykonuje wywołania sieciowe, takie jak te wykonywane w zasobach platformy Azure. Testy uruchamiane względem usług na żywo mogą napotkać problemy, takie jak opóźnienie, które spowalnia wykonywanie testu, zależności od kodu poza izolowanym testem oraz problemy z zarządzaniem stanem usługi i kosztami za każdym razem, gdy test jest uruchamiany. Zamiast testować na żywo usługi platformy Azure, zastąp klientów usługi symulowanymi lub bazującymi na pamięci implementacjami. Pozwala to uniknąć powyższych problemów i umożliwia deweloperom skoncentrowanie się na testowaniu logiki aplikacji niezależnie od sieci i usługi.
Z tego artykułu dowiesz się, jak pisać testy jednostkowe dla Azure SDK dla .NET, które izolują twoje zależności, aby testy były bardziej niezawodne. Dowiesz się również, jak zastąpić kluczowe składniki implementacjami testów w pamięci w celu tworzenia szybkich i niezawodnych testów jednostkowych oraz dowiedzieć się, jak zaprojektować własne klasy, aby lepiej obsługiwać testowanie jednostkowe. W tym artykule zawarto przykłady, które używają bibliotek Moq i NSubstitute, które są popularnymi bibliotekami pozorowania dla platformy .NET.
Zrozumieć klientów usług
Klasa klienta usługi jest głównym punktem wejścia dla deweloperów w bibliotece zestawu Azure SDK i implementuje większość logiki do komunikowania się z usługą platformy Azure. W przypadku klas klientów usługi testowania jednostkowego ważne jest, aby móc utworzyć wystąpienie klienta, które działa zgodnie z oczekiwaniami bez wykonywania żadnych wywołań sieciowych.
Każdy z klientów Azure SDK jest zgodny z wytycznymi dotyczącymi mockowania, które umożliwiają przesłanianie ich zachowania.
- Każdy klient oferuje co najmniej jeden chroniony konstruktor, aby umożliwić dziedziczenie do testowania.
- Wszyscy publiczni członkowie klienta są wirtualni, aby umożliwić zastępowanie.
Uwaga
Przykłady kodu w tym artykule używają typów z biblioteki Azure.Security.KeyVault.Secrets dla usługi Azure Key Vault. Pojęcia przedstawione w tym artykule dotyczą również klientów usług z wielu innych usług platformy Azure, takich jak Azure Storage lub Azure Service Bus.
Aby utworzyć klienta usługi testowej, możesz użyć biblioteki do tworzenia pozorantów lub standardowych funkcji, takich jak dziedziczenie, w języku C#. Frameworki do tworzenia atrap pozwalają uprościć kod, który należy napisać, aby nadpisać zachowanie członkowskie. (Te struktury mają również inne przydatne funkcje wykraczające poza zakres tego artykułu).
Aby utworzyć wystąpienie klienta testowego przy użyciu języka C# bez użycia biblioteki do pozorowania, dziedzicz po typie klienta i zastąp metody, które wywołujesz w swoim kodzie, implementacją zwracającą zestaw obiektów testowych. Większość klientów zawiera metody synchroniczne i asynchroniczne dla operacji; przesłoń tylko tę metodę, którą wywołuje twój kod aplikacji.
Uwaga
Może to być kłopotliwe, aby ręcznie zdefiniować klasy testowe, zwłaszcza jeśli trzeba dostosować zachowanie inaczej dla każdego testu. Rozważ użycie biblioteki, takiej jak Moq lub NSubstitute, aby usprawnić testowanie.
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;
}
Modele danych wejściowych i wyjściowych klienta usługi
Typy modeli przechowują wysyłane i odbierane dane z usług platformy Azure. Istnieją trzy typy modeli:
- Modele wejściowe mają być tworzone i przekazywane jako parametry do metod usług przez deweloperów. Mają co najmniej jeden publiczny konstruktor i właściwości możliwe do zapisu.
- Modele wyjściowe są zwracane tylko przez usługę i nie mają publicznych konstruktorów ani właściwości zapisywalnych.
- Modele cykliczne są rzadziej spotykane, ale zostają zwrócone przez usługę, zmodyfikowane i używane jako dane wejściowe.
Aby utworzyć wystąpienie testowe modelu wejściowego, użyj jednego z dostępnych konstruktorów publicznych i ustaw potrzebne dodatkowe właściwości.
var secretProperties = new SecretProperties("secret")
{
NotBefore = DateTimeOffset.Now
};
Aby utworzyć modele wyjściowe, używana jest fabryka modeli. Biblioteki klienckie zestawu Azure SDK udostępniają klasę fabryki modeli statycznych z sufiksem ModelFactory
w nazwie. Klasa zawiera zestaw metod statycznych do inicjowania typów modeli wyjściowych biblioteki. Na przykład model fabryczny dla SecretClient
to SecretModelFactory
:
KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret(
new SecretProperties("secret"), "secretValue");
Uwaga
Niektóre modele wejściowe mają właściwości tylko do odczytu, które są wypełniane tylko wtedy, gdy model jest zwracany przez usługę. W takim przypadku dostępna będzie fabryczna metoda modelu, która umożliwia ustawienie tych właściwości. Na przykład 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);
Eksplorowanie typów odpowiedzi
Klasa Response jest abstrakcyjną klasą, która reprezentuje odpowiedź HTTP i jest zwracana przez większość metod klienta usługi. Wystąpienia testowe Response
można tworzyć przy użyciu biblioteki do mockowania lub standardowego dziedziczenia w języku C#.
Klasa Response
jest abstrakcyjna, co oznacza, że istnieje wiele metod do zastąpienia. Rozważ użycie biblioteki, aby usprawnić podejście.
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();
}
Niektóre usługi obsługują również użycie Response<T> typu , który jest klasą zawierającą model i odpowiedź HTTP, która ją zwróciła. Aby utworzyć instancję testową Response<T>
, użyj statycznej metody Response.FromValue
:
KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret(
new SecretProperties("secret"), "secretValue");
Response<KeyVaultSecret> response = Response.FromValue(keyVaultSecret, new MockResponse());
Eksplorowanie stronicowania
Klasa Page<T> jest używana jako blok konstrukcyjny w metodach usługi, które wywołują operacje zwracające wyniki na wielu stronach. element Page<T>
jest rzadko zwracany bezpośrednio z interfejsów API, ale jest przydatny do utworzenia wystąpień AsyncPageable<T>
i Pageable<T>
w następnej sekcji. Aby utworzyć wystąpienie Page<T>
, użyj metody Page<T>.FromValues, przekazując listę elementów, token kontynuacji i Response
.
Parametr continuationToken
służy do pobierania następnej strony z usługi. Dla celów testów jednostkowych należy ustawić wartość null
dla ostatniej strony i powinna być niepusta dla innych stron.
Page<SecretProperties> responsePage = Page<SecretProperties>.FromValues(
new[] {
new SecretProperties("secret1"),
new SecretProperties("secret2")
},
continuationToken: null,
new MockResponse());
AsyncPageable<T> i Pageable<T> to klasy reprezentujące kolekcje modeli zwracanych przez usługę na stronach. Jedyną różnicą między nimi jest to, że jedna jest używana z metodami synchronicznymi, podczas gdy druga jest używana z metodami asynchronicznymi.
Aby utworzyć wystąpienie testowe klasy Pageable
lub AsyncPageable
, użyj metody statycznej 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 });
Pisanie wyśmiewanego testu jednostkowego
Załóżmy, że aplikacja zawiera klasę, która znajduje nazwy kluczy, które wygasną w danym czasie.
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();
}
}
Chcesz przetestować następujące zachowania elementu AboutToExpireSecretFinder
, aby upewnić się, że nadal działają zgodnie z oczekiwaniami:
- Sekrety bez określonej daty wygaśnięcia nie są zwracane.
- Tajemnice z datą ważności bliższą bieżącej dacie niż próg są zwracane.
Podczas testowania jednostkowego chcesz tylko, aby testy jednostkowe weryfikowały logikę aplikacji, a nie, czy usługa lub biblioteka platformy Azure działają prawidłowo. Poniższy przykład testuje kluczowe zachowania przy użyciu popularnej biblioteki 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);
}
}
Refaktoryzuj swoje typy pod kątem testowalności
Klasy, które należy przetestować, powinny być zaprojektowane pod kątem wstrzykiwania zależności, co umożliwia im odbieranie zależności zamiast tworzenia ich wewnętrznie. Był to bezproblemowy proces zastępowania SecretClient
implementacji w przykładzie z poprzedniej sekcji, ponieważ był to jeden z parametrów konstruktora. Jednak w kodzie mogą istnieć klasy, które tworzą własne zależności i nie są łatwo testowalne, takie jak następująca klasa:
public class AboutToExpireSecretFinder
{
public AboutToExpireSecretFinder(TimeSpan threshold)
{
_threshold = threshold;
_client = new SecretClient(
new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")),
new DefaultAzureCredential());
}
}
Najprostszą refaktoryzację, którą można wykonać, aby umożliwić testowanie za pomocą wstrzykiwania zależności, byłoby uwidocznienie klienta jako parametru i uruchomienie domyślnego kodu tworzenia, gdy nie podano żadnej wartości. Takie podejście umożliwia przetestowanie klasy przy jednoczesnym zachowaniu elastyczności korzystania z tego typu bez dużej liczby ceremonii.
public class AboutToExpireSecretFinder
{
public AboutToExpireSecretFinder(TimeSpan threshold, SecretClient client = null)
{
_threshold = threshold;
_client = client ?? new SecretClient(
new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")),
new DefaultAzureCredential());
}
}
Inną opcją jest przeniesienie tworzenia zależności w całości do kodu wywołującego:
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);
Takie podejście jest przydatne, gdy chcesz skonsolidować tworzenie zależności i współdzielić obiekt klienta między wieloma klasami korzystających.
Zrozumienie klientów Azure Resource Manager (ARM)
W bibliotekach ARM klienci zostali zaprojektowani tak, aby podkreślić relacje między sobą, odzwierciedlając hierarchię usług. Aby osiągnąć ten cel, metody rozszerzeń są powszechnie używane do dodawania dodatkowych funkcji dla klientów.
Na przykład maszyna wirtualna platformy Azure istnieje w grupie zasobów platformy Azure.
Azure.ResourceManager.Compute
Przestrzeń nazw modeluje maszynę wirtualną platformy Azure jako VirtualMachineResource
.
Azure.ResourceManager
Przestrzeń nazw modeluje grupę zasobów platformy Azure jako ResourceGroupResource
. Aby wykonać zapytanie dotyczące maszyn wirtualnych dla grupy zasobów, należy napisać:
VirtualMachineCollection virtualMachineCollection = resourceGroup.GetVirtualMachines();
Ponieważ funkcje związane z maszyną wirtualną, takie jak GetVirtualMachines
w systemie ResourceGroupResource
, są implementowane jako metody rozszerzenia, nie można po prostu utworzyć makiety typu i zastąpić metodę. Zamiast tego musisz również utworzyć klasę mock dla "zasobu do mockowania" i połączyć je.
Typ zasobu, który jest możliwy do zamockowania, znajduje się zawsze w poddomenie metody rozszerzenia. W poprzednim przykładzie typ zasobu możliwy do wyśmiewania znajduje się w Azure.ResourceManager.Compute.Mocking
przestrzeni nazw. Typ zasobu mockowalnego jest zawsze nazywany z prefiksami "Mockable" i nazwą biblioteki przed typem zasobu. W poprzednim przykładzie typ zasobu do pozorowania nosi nazwę MockableComputeResourceGroupResource
, gdzie ResourceGroupResource
jest typem zasobu metody rozszerzenia i Compute
jest nazwą biblioteki.
Jeszcze jednym wymogiem przed uruchomieniem testu jednostkowego jest zasymulowanie GetCachedClient
metody dla typu zasobu metody rozszerzenia. Ukończenie tego kroku powoduje podłączenie metody rozszerzenia oraz metody na rodzaju zasobu, który można symulować.
Oto, jak to działa:
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)
{}
}