Tests d’intégration
Chaque projet généré à partir des modèles comprend un <YourProject>.Tests.Integration projet, câblé et prêt à être exécuté. Ses tests vont sur un environnement Dataverse réel plutôt que sur des simulations, car la plupart des failles d’un portail résident dans les joints qu’un mock remplace — traduction de requêtes, métadonnées, formatage, permissions. Deux classes de base sont fournies, une pour chaque côté de celle-ci.
Qu’y a-t-il dans le projet
Le modèle envoie ces fichiers ; ils sont des sources ordinaires dans votre dépôt, donc changez-les librement.
| Fichier | Objectif |
|---|---|
PortalEndpointTestBase.cs |
Démarre votre portail en mémoire et expose un HttpClient contre. On dérive de cela pour les tests de terminaison et de permissions. |
TestBase.cs |
Construit un conteneur de services avec une connexion Dataverse authentifiée. Dérivez de cela pour appeler directement vos propres services et logique. |
TestAuthentication.cs |
Le schéma d’authentification de test et le TestUserContext qui décide sous qui une requête s’exécute. |
appsettings.json, xunit.runner.json |
Paramètres non secrets, et la configuration xunit runner qui maintient les collections en série. |
ExampleTests.cs, README.md |
Des exemples fonctionnels des deux bases (j’ai sauté jusqu’à ce qu’on les pointe vers de vrais documents), et une courte référence locale. |
Choisir une base
Les deux sont complémentaires — ils testent différentes couches, et une suite saine utilise les deux.
PortalEndpointTestBaseExécute votre véritableProgram, donc la requête passe par l’ordre réel du middleware, la véritable autorisation et les gestionnaires d’autorisations exactement tels que vous les avez enregistrés. Utilisez-le pour les permissions de table, la surface du/api/*framework, et tout point de terminaison que vous ajoutez. Parce que c’est votre application, un test ici ne peut pas dériver du câblage en production.TestBaseAucun hôte ne démarre. Il construit un conteneur et vous donne un client Dataverse, ce qui en fait le choix le moins cher pour la logique de type plugin, les services de domaine et les formes de requête. Il ne détecte pas d’HTTP, donc aucun middleware, autorisation ou gestionnaire de permissions ne s’applique.
Configuration et secrets
Pointez D365:Url l’environnement contre lequel les tests doivent fonctionner — appsettings.json un environnement de développement ou jetable, puisque les tests créent et suppriment des enregistrements. Les identifiants proviennent de secrets utilisateur, et le projet de test déclare délibérément la même UserSecretsId chose que le projet web, donc un portail qui fonctionne déjà localement n’a pas besoin de plus. Pour les définir explicitement :
dotnet user-secrets set "D365:ClientId" "<application (client) id>"
dotnet user-secrets set "D365:ClientSecret" "<client secret value>"
Les sources sont superposées appsettings.json → secrets utilisateur → variables d’environnement, donc un agent de compilation peut tout fournir via des variables d’environnement (D365__ClientSecret, en utilisant le double soulignement) sans toucher à un fichier. Ne jamais mettre le secret client dans appsettings.json, qui est validé.
Note
Vous n’avez pas besoin d’une clé de licence pour effectuer des tests. L’hôte en mémoire écoute sur une adresse loopback, et loopback est licencié sans une — pas d’enregistrement Portal Website et pas de double test requis.
Test des terminaux et des permissions
Dériver de PortalEndpointTestBase et effectuer des requêtes avec Client. Les requêtes commencent anonymes, ce qui est la règle par défaut pour affirmer qu’un point de terminaison repousse les appelants anonymes. Les redirections ne sont pas suivies, donc un test peut s’affirmer sur un 302 plutôt que sur la page sur laquelle il atterrirait.
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);
}
}
Agir en tant qu’utilisateur
SignInAs est celui qu’il faut atteindre. Il ne stub que l’étape de la certification — le principal qu’il émet porte les mêmes revendications qu’une vraie connexion, donc l’autorisation, les gestionnaires de permissions et toutes les extensions de lecture de revendications se comportent comme en production. Pas de mot de passe, pas de provisionnement, et il peut changer d’identité entre les requêtes dans un seul test.
LoginAsync publie les véritables identifiants sur le point de connexion et conserve le cookie, pour quand le chemin de connexion lui-même est en cours de test. Il faut un contact avec un mot de passe et un email confirmé, donc c’est plus lent ; les deux coexistent et la base revient au vrai cookie dès qu’aucun utilisateur usurpé n’est défini.
// 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");
Note
LoginAsynca besoin des terminaux d’authentification JSON, qui sont mappés pour les hôtes React et Blazor WebAssembly/Auto. Un projet Blazor affiché par serveur se connecte via des messages de formulaire sur les pages du compte Razor, donc/api/auth/loginn’est pas mappé là etLoginAsyncrenvoie 404 — utilisezSignInAs, qui fonctionne dans tous les hôtes.
Mise en place, démontage et remplacements
Trois crochets couvrent les besoins habituels. ConfigureTestServices S’exécute après les propres enregistrements de votre application, donc un Replace là l’emporte Program.cs — la manière de faire un faux pour une dépendance sortante. OnInitializedAsync Et OnDisposingAsync mettez chaque test entre crochets pour le seeding et le cleanup, et le démontage s’exécute même en cas d’échec du test.
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);
}
}
Important
Ces tests lancent votre portail, donc tout ce qui l’empêche de démarrer les arrête aussi. En particulier, avec l’autorisation améliorée activée, l’hébergeur valide l’identifiant du site web configuré au démarrage et échoue avec des instructions jusqu’à ce qu’un identifiant réel
powerpagesitesoit fourni — la même condition préalable que pour l’exécution du portail.
Services de test et logique
Dérivez de TestBase, enregistrez ce dont le code testé a besoin, et résolvez-le. Le client Dataverse est déjà authentifié.
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();
}
}
Passez un identifiant utilisateur à CreateOrganizationService(userId) pour l’usurper — Dataverse applique alors les privilèges propres à cet utilisateur, ce qui permet de vérifier l’accès au niveau des enregistrements depuis le côté données plutôt que via un point de terminaison.
CreateWebApiHttpClientAsync() renvoie un client authentifié par rapport à l’API Web Dataverse (/api/data/v9.2/) pour des assertions plus faciles à exprimer dans OData que via le SDK. Il nécessite Azure:TenantId à la fois la configuration ainsi que l’identifiant client et le secret.
Comment se déroulent les tests
Exécutez-les avec dotnet test, ou depuis l’explorateur de test de votre IDE. Quelques caractéristiques valent la peine d’être connues avant d’en écrire beaucoup :
- Les collections s’exécutent en série. Elles partagent un environnement Dataverse, et les écritures parallèles sur les mêmes enregistrements rendent les défaillances difficiles à reproduire. Cela est défini dans
xunit.runner.json. - xunit construit une nouvelle instance de la classe de test pour chaque méthode de test. La connexion Dataverse authentifiée est mise en cache pour tout le processus et distribuée sous forme de clone léger, ce qui coûte donc une connexion par exécution au lieu d’une par test — mais tout ce que vous ajoutez coûteux à un constructeur paie à chaque test.
- Nettoyez ce que vous créez. Un test qui laisse des enregistrements dépend des assertions de la prochaine exécution de la précédente. Privilégiez des valeurs uniques par test (un GUID dans un champ de nom) plutôt que des valeurs fixes, afin que les exécutions répétées ne puissent pas entrer en collision.
Meilleures pratiques
Pointez la suite vers un environnement de développement, jamais vers la production. Ce sont de vraies écritures sur de vraies données, et un test qui supprime ce qu’il a créé supprimera l’enregistrement réel si vous le visez vers un.
