Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
Az egységtesztelés fontos része egy fenntartható fejlesztési folyamatnak, amely javíthatja a kódminőséget, és megelőzheti a regressziókat vagy hibákat az alkalmazásokban. Az egységtesztelés azonban kihívást jelent, amikor a tesztelt kód hálózati hívásokat hajt végre, például az Azure-erőforrásokra irányuló hívásokat. Az élő szolgáltatásokon futtatott tesztek olyan problémákat tapasztalhatnak, mint például a teszt végrehajtását lassító késés, az izolált teszten kívüli kódfüggőségek, valamint a szolgáltatásállapot és a költségek kezelésével kapcsolatos problémák minden alkalommal, amikor a teszt lefut. Az élő Azure-szolgáltatások tesztelése helyett cserélje le a szolgáltatás-ügyfeleket a szimulált vagy memórián belüli implementációkra. Ez elkerüli a fenti problémákat, és lehetővé teszi, hogy a fejlesztők a hálózattól és a szolgáltatástól független alkalmazáslogika tesztelésére összpontosítsanak.
Ebben a cikkben megtudhatja, hogyan írhat egységteszteket az Azure SDK for .NET-hez, amelyek elkülönítik a függőségeket, hogy a tesztek megbízhatóbbak legyenek. Azt is megtudhatja, hogyan cserélheti le a kulcsfontosságú összetevőket a memórián belüli tesztelési implementációkra a gyors és megbízható egységtesztek létrehozásához, és megtudhatja, hogyan tervezheti meg saját osztályait az egységtesztelés jobb támogatása érdekében. Ez a cikk olyan példákat tartalmaz, amelyek a Moq-t és az NSubstitute-ot használják, amelyek a .NET népszerű mintakódtárai.
Szolgáltatás-ügyfelek ismertetése
A szolgáltatásügyfél-osztály az Azure SDK-tár fejlesztőinek fő belépési pontja, és a legtöbb logikát implementálja az Azure-szolgáltatással való kommunikációhoz. Az egységtesztelési szolgáltatás ügyfélosztályainál fontos, hogy az ügyfél egy olyan példányát hozza létre, amely a várt módon viselkedik hálózati hívások nélkül.
Az Azure SDK-ügyfelek mindegyike olyan modellelési irányelveket követ, amelyek lehetővé teszik a viselkedésük felülbírálását:
- Minden ügyfél legalább egy védett konstruktort kínál a teszteléshez szükséges öröklés engedélyezéséhez.
- Minden nyilvános ügyfél tag virtuális, hogy lehessen felülírni.
Megjegyzés:
A cikkben szereplő példakódok az Azure.Security.KeyVault.Secrets kódtárból származó típusokat használnak az Azure Key Vault szolgáltatáshoz. A cikkben bemutatott fogalmak számos más Azure-szolgáltatás, például az Azure Storage vagy az Azure Service Bus szolgáltatás ügyfeleire is érvényesek.
Tesztszolgáltatás-ügyfél létrehozásához használhatja a mocking könyvtárat vagy a szokásos C#-funkciókat, például az öröklést. A modellezési keretrendszerek lehetővé teszik a tagok viselkedésének felülbírálásához megírt kód egyszerűsítését. (Ezek a keretrendszerek más hasznos funkciókkal is rendelkeznek, amelyek túlmutatnak a jelen cikk hatókörén.)
Ha egy tesztügyfél-példányt a C# használatával szeretne létrehozni egy kódtár használata nélkül, örökölje az ügyféltípust és a kódban meghívandó felülbírálási metódusokat egy olyan implementációval, amely tesztobjektumok készletét adja vissza. A legtöbb ügyfél szinkron és aszinkron metódusokat is tartalmaz a műveletekhez; csak azt bírálja felül, amelyet az alkalmazáskód hív.
Megjegyzés:
Nehézkes lehet manuálisan meghatározni a tesztosztályokat, különösen akkor, ha az egyes tesztek viselkedését eltérően kell testre szabnia. Fontolja meg egy olyan kódtár használatát, mint a Moq vagy az NSubstitute a tesztelés egyszerűsítése érdekében.
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;
}
Szolgáltatásügyfél bemeneti és kimeneti modelljei
A modelltípusok az Azure-szolgáltatásokból küldött és fogadott adatokat tárolják. Három modelltípus létezik:
- A bemeneti modelleket a fejlesztők a szolgáltatásmódszerek paramétereként kívánják létrehozni és átadni. Egy vagy több nyilvános konstruktoruk és írható tulajdonságaik vannak.
- A kimeneti modelleket csak a szolgáltatás adja vissza, és nem rendelkeznek nyilvános konstruktorokkal vagy írható tulajdonságokkal.
- A körutazási modellek kevésbé gyakoriak, de a szolgáltatás visszaadja, módosítja és bemenetként használja.
Egy bemeneti modell tesztpéldányának létrehozásához használja az egyik elérhető nyilvános konstruktort, és állítsa be a szükséges további tulajdonságokat.
var secretProperties = new SecretProperties("secret")
{
NotBefore = DateTimeOffset.Now
};
A kimeneti modellek példányainak létrehozásához a rendszer egy modell-előállítót használ. Az Azure SDK ügyfélkönyvtárak egy statikus modellosztály-gyárat biztosítanak, amely nevében egy ModelFactory utótag található. Az osztály statikus metódusok készletét tartalmazza a kódtár kimeneti modelltípusainak inicializálásához. Például a SecretClient modellgyára a SecretModelFactory:
KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret(
new SecretProperties("secret"), "secretValue");
Megjegyzés:
Egyes bemeneti modellek írásvédett tulajdonságokkal rendelkeznek, amelyek csak akkor vannak feltöltve, amikor a modellt visszaadja a szolgáltatás. Ebben az esetben elérhető lesz egy modell-előállító metódus, amely lehetővé teszi ezeknek a tulajdonságoknak a beállítását. Például: 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);
Választípusok felfedezése
Az Response osztály egy absztrakt osztály, amely EGY HTTP-választ jelöl, és a legtöbb szolgáltatásügyfél-metódus visszaadja. Tesztpéldányokat Response hozhat létre mock könyvtár vagy szabványos C# öröklés használatával.
Az Response osztály absztrakt, ami azt jelenti, hogy sok tagot felül kell bírálni. Érdemes lehet egy kódtárat használni a megközelítés gördülékenyebbé tételéhez.
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();
}
Egyes szolgáltatások a típus használatát Response<T> is támogatják, amely egy modellt és a visszaadott HTTP-választ tartalmazó osztály. A tesztpéldány Response<T>létrehozásához használja a statikus Response.FromValue metódust:
KeyVaultSecret keyVaultSecret = SecretModelFactory.KeyVaultSecret(
new SecretProperties("secret"), "secretValue");
Response<KeyVaultSecret> response = Response.FromValue(keyVaultSecret, new MockResponse());
Lapozási lehetőségek felfedezése
Az Page<T> osztály építőelemként használatos olyan szolgáltatásmetóciókban, amelyek több oldalon visszaadott műveleteket hívnak meg. A Page<T> ritkán jelenik meg közvetlenül az API-kból, de hasznos a következő szakaszban található AsyncPageable<T> és Pageable<T> példányok létrehozásához. Példány létrehozásához használja a Page<T> metódust, adja át az elemek listáját, a folytatás tokenjét és a Page<T>.FromValues.
A continuationToken paraméter a következő lap lekérésére szolgál a szolgáltatásból. Egységtesztelés céljából az utolsó oldal esetén null-ra kell beállítani, más oldalaknál pedig nem maradhat üres.
Page<SecretProperties> responsePage = Page<SecretProperties>.FromValues(
new[] {
new SecretProperties("secret1"),
new SecretProperties("secret2")
},
continuationToken: null,
new MockResponse());
AsyncPageable<T> és Pageable<T> olyan osztályok, amelyek a szolgáltatás által a lapokban visszaadott modellek gyűjteményeit jelölik. Az egyetlen különbség közöttük az, hogy az egyiket szinkron metódusokkal, a másikat pedig aszinkron metódusokkal használják.
A Pageable statikus metódust használja a AsyncPageable vagy FromPages tesztpéldány létrehozásához.
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 });
Szimulált egységteszt írása
Tegyük fel, hogy az alkalmazás tartalmaz egy osztályt, amely megkeresi az adott időn belül lejáró kulcsok nevét.
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();
}
}
A következő viselkedéseket szeretné tesztelni AboutToExpireSecretFinder, hogy megbizonyosodjon arról, hogy továbbra is a várt módon működnek.
- A lejárati dátummal nem rendelkező titkos kulcsokat a rendszer nem adja vissza.
- A rendszer az aktuális dátumhoz közelebbi lejárati dátummal rendelkező titkos kulcsokat ad vissza.
Az egységtesztelés során csak az egységtesztek ellenőrzik az alkalmazás logikáját, és nem azt, hogy az Azure-szolgáltatás vagy a kódtár megfelelően működik-e. Az alábbi példa a népszerű xUnit-kódtár használatával teszteli a kulcs viselkedését:
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);
}
}
A típusok újrabontása a tesztelhetőség érdekében
A tesztelni kívánt osztályokat függőséginjektálásra kell tervezni, ami lehetővé teszi, hogy az osztály megkapja a függőségeit ahelyett, hogy belsőleg hozza létre őket. Zökkenőmentes folyamat volt az SecretClient előző szakaszban szereplő példában szereplő implementáció lecserélése, mivel ez volt az egyik konstruktorparaméter. Előfordulhat azonban, hogy a kódban vannak olyan osztályok, amelyek saját függőségeket hoznak létre, és nem tesztelhetők könnyen, például a következő osztály:
public class AboutToExpireSecretFinder
{
public AboutToExpireSecretFinder(TimeSpan threshold)
{
_threshold = threshold;
_client = new SecretClient(
new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")),
new DefaultAzureCredential());
}
}
A legegyszerűbb újrabontással engedélyezheti a függőséginjektálással végzett tesztelést, ha az ügyfelet paraméterként teszi elérhetővé, és alapértelmezett létrehozási kódot futtat, ha nincs megadva érték. Ez a megközelítés lehetővé teszi, hogy az osztály tesztelhető legyen, miközben továbbra is megtartja a típus használatának rugalmasságát anélkül, hogy sok ceremónia nélkül.
public class AboutToExpireSecretFinder
{
public AboutToExpireSecretFinder(TimeSpan threshold, SecretClient client = null)
{
_threshold = threshold;
_client = client ?? new SecretClient(
new Uri(Environment.GetEnvironmentVariable("KeyVaultUri")),
new DefaultAzureCredential());
}
}
Egy másik lehetőség a függőség létrehozása teljes egészében a hívókódba való áthelyezése:
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);
Ez a módszer akkor hasznos, ha össze szeretné konszolidálni a függőség létrehozását, és meg szeretné osztani az ügyfelet több fogyasztó osztály között.
Az Azure Resource Manager (ARM) ügyfelek megértése
Az ARM-kódtárakban az ügyfelek úgy lettek kialakítva, hogy hangsúlyozzák a kapcsolatukat egymással, tükrözve a szolgáltatáshierarchiát. A cél elérése érdekében a bővítménymetelyeket széles körben használják további funkciók ügyfelekhez való hozzáadásához.
Egy Azure-beli virtuális gép például létezik egy Azure-erőforráscsoportban. A Azure.ResourceManager.Compute névtér az Azure-beli virtuális gépet VirtualMachineResourcea következőképpen modellozza: . A Azure.ResourceManager névtér az Azure-erőforráscsoportot ResourceGroupResourcea következőképpen modellozza: . Egy erőforráscsoport virtuális gépeinek lekérdezéséhez a következőt kell írnia:
VirtualMachineCollection virtualMachineCollection = resourceGroup.GetVirtualMachines();
Mivel a virtuális géppel kapcsolatos funkciók, például GetVirtualMachines az on ResourceGroupResource, bővítménymetódusként lettek implementálva, lehetetlen egyszerűen létrehozni egy ilyen típusú mintapéldányt, és felülbírálni a metódust. Ehelyett létre kell hoznia egy modellosztályt a "modellezhető erőforráshoz", és össze kell fűznie őket.
A modellezhető erőforrástípus mindig a Mocking bővítménymetódus alnévterében található. Az előző példában a modellezhető erőforrástípus a Azure.ResourceManager.Compute.Mocking névtérben található. A modellezhető erőforrástípus neve mindig a "Mockable" és az erőforrástár neve előtagként szerepel az erőforrástípus neve előtt. Az előző példában a modellezhető erőforrástípus neve MockableComputeResourceGroupResource, ahol ResourceGroupResource a bővítménymetódus erőforrástípusa, és Compute az erőforrástár neve.
Az egységteszt futtatása előtt még egy követelmény a metódus szimulálása a GetCachedClient bővítménymetódus erőforrástípusán. A lépés végrehajtása összekapcsolja a bővítménymetódust és a metódust a modellezhető erőforrástípuson.
Így működik:
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)
{}
}