Integrationstests
Jedes Projekt, das aus den Vorlagen generiert wird, enthält ein <YourProject>.Tests.Integration Projekt, das verkabelt und bereit zum Ausführen ist. Seine Tests basieren auf einer echten Dataverse-Umgebung statt auf Mocks, weil das meiste, was in einem Portal kaputtgeht, in den Nähten eines Mocks lebt – Abfrageübersetzung, Metadaten, Formatierung, Berechtigungen. Es werden zwei Basisklassen bereitgestellt, jeweils eine für jede Seite davon.
Was ist im Projekt enthalten
Die Vorlage liefert diese Dateien; sie sind normale Quellcodes in deinem Repository, also ändere sie frei.
| Akte | Zweck |
|---|---|
PortalEndpointTestBase.cs |
Bootet dein Portal im Speicher und stellt ein HttpClient Signal gegen es frei. Daraus leide ich für Endpunkt- und Berechtigungstests ab. |
TestBase.cs |
Erstellt einen Service-Container mit einer authentifizierten Dataverse-Verbindung. Leite daraus ab, um direkt deine eigenen Services und Logik aufzurufen. |
TestAuthentication.cs |
Das Test-Authentifizierungsschema und das TestUserContext , das entscheidet, für wen eine Anfrage ausgeführt wird. |
appsettings.json, xunit.runner.json |
Nicht-geheime Einstellungen und die Xunit-Runner-Konfiguration, die Sammlungen serielle hält. |
ExampleTests.cs, README.md |
Es wurden Beispiele beider Basen ausgearbeitet (übersprungen, bis man sie auf echte Aufzeichnungen zeigt) und eine kurze lokale Referenz. |
Die Wahl einer Basis
Die beiden ergänzen sich – sie testen verschiedene Schichten, und eine gesunde Suite nutzt beides.
PortalEndpointTestBaseFührt dein tatsächlichesProgramaus, sodass die Anfrage durch die echte Middleware-Ordnung, die echte Autorisierung und die Berechtigungshandler genau so läuft, wie du sie registriert hast. Nutze es für Tabellenberechtigungen, die/api/*Oberfläche des Frameworks und jeden von dir hinzugefügten Endpunkt. Da es deine Anwendung ist , kann ein Test hier nicht von der Produktionsleitung abweichen.TestBaseEs startet keinen Host. Es baut einen Container und stellt Ihnen einen Dataverse-Client zur Verfügung, was es zur günstigeren Wahl für Plugin-ähnliche Logik, Domänendienste und Abfrageformen macht. Es sieht kein HTTP, daher gelten keine Middleware, Autorisierungs- oder Berechtigungshandler.
Konfiguration und Geheimnisse
Zeigen D365:Url Sie auf die Umgebung, in der die Tests laufen appsettings.json sollten – eine Entwicklungs- oder Wegwerfumgebung, da Tests Datensätze erstellen und löschen. Zugangsdaten stammen aus Benutzergeheimnissen, und das Testprojekt deklariert absichtlich dasselbe UserSecretsId wie das Webprojekt, sodass ein Portal, das bereits lokal läuft, nichts Weiteres benötigt. Um sie explizit zu setzen:
dotnet user-secrets set "D365:ClientId" "<application (client) id>"
dotnet user-secrets set "D365:ClientSecret" "<client secret value>"
Quellen sind → Benutzergeheimnissen → Umgebungsvariablen geschichtet appsettings.json , sodass ein Build-Agent alles über Umgebungsvariablen (D365__ClientSecret, unter Verwendung des doppelten Unterstrichs) liefern kann, ohne eine Datei zu berühren. Fügen Sie niemals das Client-Geheimnis in appsettings.json, das committed ist.
Anmerkung
Man braucht keinen Lizenzschlüssel, um Tests durchzuführen. Der In-Memory-Host hört über eine Loopback-Adresse, und Loopback ist ohne eine Lizenz lizenziert – kein Portal-Website-Datensatz und kein Test-Doppel erforderlich.
Test-Endpunkte und Berechtigungen
Man leitet ab und PortalEndpointTestBase stellt Anfragen mit Client. Anfragen beginnen anonym, was der richtige Standard ist, um zu behaupten, dass ein Endpunkt anonyme Anrufer abweist. Weiterleitungen werden nicht befolgt, sodass ein Test auf einer 302 statt auf der Seite ausgeführt werden kann, auf der er landen würde.
public class AccountPermissionTests : PortalEndpointTestBase
{
[Fact]
public async Task AnonymousCallersAreRefused()
{
// No sign-in: the request is anonymous.
var response = await this.Client.GetAsync("/api/table/account/records");
response.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
}
[Fact]
public async Task SignedInContactCanRead()
{
this.SignInAs(contactId);
var response = await this.Client.GetAsync("/api/table/account/records");
response.StatusCode.Should().Be(HttpStatusCode.OK);
}
}
Handeln als Nutzer
SignInAs ist derjenige, zu dem man greifen muss. Er stubbt nur den Credential-Schritt – der von ihm ausgegebene Principal trägt dieselben Behauptungen, die auch eine echte Anmeldung ausgibt, sodass Autorisierung, die Berechtigungshandler und jede claim-reading-Erweiterung sich wie in der Produktion verhalten. Kein Passwort, keine Bereitstellung, und es kann die Identität zwischen Anfragen in einem einzigen Test ändern.
LoginAsync Speichert echte Zugangsdaten auf den Login-Endpunkt und behält den Cookie, wenn der Anmeldepfad selbst getestet wird. Er benötigt einen Kontakt mit Passwort und eine bestätigte E-Mail, daher ist er langsamer; beide existieren nebeneinander und die Basis greift auf das echte Cookie zurück, wenn kein Imitationsnutzer gesetzt wird.
// Impersonate — fast, no password, still runs the real authorization path.
this.SignInAs(contactId);
this.SignInAs(contactId, "Administrator", "Manager"); // with role claims
this.SignInAs(userId, PortalUserType.SystemUser); // an internal user
this.SignOut(); // anonymous again
// Or sign in for real, when the sign-in path itself is what's under test.
var login = await this.LoginAsync("someone@example.com", "password");
Anmerkung
LoginAsyncbenötigt die JSON-Authentifizierungs-Endpunkte, die für die React- und Blazor-WebAssembly/Auto-Hosts zugeordnet sind. Ein servergerendertes Blazor-Projekt meldet sich über Formular an, postet stattdessen auf die Razor-Kontoseiten, ist daher/api/auth/logindort nicht zugeordnet und gibt 404 zurück –LoginAsyncuseSignInAs, was in jedem Host funktioniert.
Aufbau, Abbau und Auswechslungen
Drei Haken decken die üblichen Bedürfnisse ab.ConfigureTestServices Läuft nach den eigenen Registrierungen deiner Anwendung, also gewinnt Program.cs ein Replace Dort – der Weg, eine Fälschung für eine ausgehende Abhängigkeit einzusetzen. OnInitializedAsync Und OnDisposingAsync jeden Test für Seeding und Cleanup einzurahmen, und die Demontage läuft auch dann, wenn ein Test fehlschlägt.
public class OrderTests : PortalEndpointTestBase
{
private Guid _contactId;
// Replace a service in the running host — a fake for an outbound dependency, say.
protected override void ConfigureTestServices(IServiceCollection services)
{
services.Replace(ServiceDescriptor.Transient<IShippingQuotes, FakeShippingQuotes>());
}
// Seed what the test needs. Runs after the host starts.
protected override async Task OnInitializedAsync()
{
_contactId = await this.CreateTestContactAsync();
}
// Always runs, including after a failing test.
protected override async Task OnDisposingAsync()
{
await this.DeleteContactAsync(_contactId);
}
}
Wichtig
Diese Tests starten dein Portal, also stoppt alles, was das Starten verhindert, auch sie. Insbesondere validiert der Host mit aktivierter verbesserter Autorisierung die konfigurierte Website-ID beim Start und scheitert mit Anleitung – bis eine echte
powerpagesiteID bereitgestellt wird – dieselbe Voraussetzung wie das Ausführen des Portals.
Testdienste und Logik
Herleiten Sie aus TestBase, registrieren Sie, was der unter Test befindliche Code benötigt, und lösen Sie es auf. Der Dataverse-Client ist bereits authentifiziert.
public class InvoiceLogicTests : TestBase
{
// Register what the code under test needs; always call base first.
protected override void RegisterServices(IServiceCollection services)
{
base.RegisterServices(services);
services.AddMyPortalLogic();
}
[Fact]
public async Task PostingAnInvoiceSetsTheBalance()
{
var logic = this.ServiceProvider.GetRequiredService<InvoiceLogic>();
// A Dataverse client, already authenticated. IOrganizationService resolves to the
// same connection, so resolving either doesn't open a second one.
using var service = this.CreateOrganizationService();
var invoice = await service.Queryable("invoice")
.FirstOrDefaultAsync(i => i.GetAttributeValue<string>("invoicenumber") == "INV-1");
invoice.Should().NotBeNull();
}
}
Geben Sie eine Benutzer-ID weiter CreateOrganizationService(userId) , um sich auszugeben – Dataverse setzt dann die eigenen Rechte dieses Benutzers durch, was bedeutet, wie man den Zugriff auf Datenebene von der Datenseite überprüft und nicht über einen Endpunkt.
CreateWebApiHttpClientAsync() gibt einen Client zurück, der gegen die Dataverse Web API (/api/data/v9.2/) authentifiziert ist, für Assertions, die in OData leichter auszudrücken sind als über das SDK. Es benötigt Azure:TenantId in der Konfiguration sowie die Client-ID und das Geheimnis.
Wie die Tests ablaufen
Führe sie mit dotnet testoder aus dem Test-Explorer deiner IDE aus. Einige Eigenschaften sind zu kennen, bevor du viele schreibst:
- Sammlungen laufen serienmäßig. Sie teilen sich eine Dataverse-Umgebung, und parallele Schreibvorgänge zu denselben Datensätzen erschweren die Reproduktion von Fehlern. Dies ist in
xunit.runner.jsongesetzt. - xunit baut für jede Testmethode eine neue Instanz der Testklasse. Die authentifizierte Dataverse-Verbindung wird für den gesamten Prozess zwischengespeichert und als leichter Klon verteilt, sodass pro Durchlauf ein Anmelden statt eines pro Test kostet – aber alles, was teur, was man einem Konstruktor hinzufügt, zahlt für jeden Test.
- Bereinigen Sie, was Sie erstellen. Ein Test, der Datensätze hinterlässt, lässt die Behauptungen des nächsten Durchlaufs vom letzten abhängen. Bevorzugen Sie eindeutige Werte pro Test (eine GUID in einem Namensfeld) gegenüber festen, damit wiederholte Durchläufe nicht kollidieren können.
Best Practice
Richte die Suite auf eine Entwicklungsumgebung, niemals auf die Produktion. Das sind echte Schreibvorgänge gegen echte Daten, und ein Test, der das Erstellte löscht, löscht den echten Datensatz, wenn du ihn auf einen zielst.
