Pruebas de integración
Cada proyecto generado a partir de las plantillas incluye un <YourProject>.Tests.Integration proyecto, cableado y listo para ejecutarse. Sus pruebas se enfrentan a un entorno real de Dataverse en lugar de a mocks, porque la mayor parte de lo que se interrumpe en un portal reside en las costuras que reemplaza un mock — traducción de consultas, metadatos, formato, permisos. Se proporcionan dos clases base, una para cada lado de esa clase.
¿Qué hay en el Proyecto
La plantilla incluye estos archivos; son fuentes ordinarias en tu repositorio, así que cámbialos libremente.
| Archivo | Propósito |
|---|---|
PortalEndpointTestBase.cs |
Arranca tu portal en memoria y expone un HttpClient contra él. Deriva de esto para pruebas de endpoint y permisos. |
TestBase.cs |
Construye un contenedor de servicios con una conexión autenticada de Dataverse. Deriva de esto para llamar directamente a tus propios servicios y lógica. |
TestAuthentication.cs |
El esquema de autenticación de prueba y el TestUserContext que decide con quién se ejecuta una solicitud. |
appsettings.json, xunit.runner.json |
Configuraciones no secretas, y la configuración del runner xunit que mantiene las colecciones seriales. |
ExampleTests.cs, README.md |
Ejemplos funcionales de ambas bases (saltándolas hasta que las apuntas a registros reales) y una breve referencia local. |
Elegir una base
Ambos son complementarios: prueban capas diferentes, y una Suite Healthy utiliza ambas.
PortalEndpointTestBaseEjecuta tu , paraProgramque la solicitud pase por el orden real del middleware, la autorización real y los manejadores de permisos exactamente como los registraste. Úsalo para permisos de tabla, la superficie del/api/*framework y cualquier endpoint que añadas. Como es tu aplicación, una prueba aquí no puede derivarse del cableado de producción.TestBaseNo inicia ningún host. Construye un contenedor y te da un cliente Dataverse, lo que lo convierte en la opción más barata para lógica tipo plugin, servicios de dominio y formas de consulta. No detecta HTTP, así que no se aplican intermediarios, autorizaciones ni manejadores de permisos.
Configuración y secretos
Señala D365:Url el entorno contra el que deben ejecutarse las pruebas — appsettings.json un entorno de desarrollo o desechable, ya que las pruebas crean y eliminan registros. Las credenciales provienen de secretos de usuario, y el proyecto de prueba declara deliberadamente lo mismo UserSecretsId que el proyecto web, por lo que un portal que ya se ejecuta localmente no necesita nada más. Para establecerlas explícitamente:
dotnet user-secrets set "D365:ClientId" "<application (client) id>"
dotnet user-secrets set "D365:ClientSecret" "<client secret value>"
Las fuentes están superpuestas appsettings.json → secretos de usuario → variables de entorno, por lo que un agente de compilación puede suministrar todo a través de variables de entorno (D365__ClientSecretusando el doble guion subtirado) sin tocar ningún archivo. Nunca pongas el secreto cliente en appsettings.json, que está comprometido.
Nota
No necesitas una clave de licencia para ejecutar pruebas. El host en memoria escucha en una dirección de loopback, y loopback está licenciado sin una — no hay registro del portal web ni se requiere doble de prueba.
Pruebas de endpoints y permisos
Derivar de PortalEndpointTestBase y hacer peticiones con Client. Las peticiones comienzan anónimas, que es el valor por defecto correcto para afirmar que un endpoint rechaza a los llamantes anónimos. No se siguen las redirecciones, por lo que una prueba puede afirmar sobre un 302 en lugar de en la página en la que caería.
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);
}
}
Actuar como usuario
SignInAs es a quien debes recurrir. Solo hace stub al paso de credencial — el principal que emite lleva las mismas afirmaciones que emite un inicio de sesión real, por lo que la autorización, los manejadores de permisos y todas las extensiones de lectura de reclamaciones se comportan como en producción. Sin contraseña, sin provisionamiento, y puede cambiar de identidad entre solicitudes en una sola prueba.
LoginAsync publica credenciales reales en el endpoint de inicio de sesión y conserva la cookie, para cuando la ruta de inicio de sesión esté bajo prueba. Necesita un contacto con contraseña y un correo confirmado, así que es más lento; ambos coexisten y la base vuelve a la cookie real siempre que no se configura ningún usuario suplantado.
// 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");
Nota
LoginAsyncnecesita los endpoints de autenticación JSON, que están mapeados para los hosts de React y Blazor WebAssembly/Auto. Un proyecto Blazor renderizado por servidor inicia sesión mediante publicaciones de formulario en las páginas de la cuenta de Razor, por lo que/api/auth/loginno está mapeado allí yLoginAsyncdevuelve 404 — usaSignInAs, que funciona en todos los hosts.
Preparación, desmontaje y sustituciones
Tres ganchos cubren las necesidades habituales. ConfigureTestServices Se ejecuta después de los propios registros de tu aplicación, así que un Replace ahí gana Program.cs — la forma de posicionarse en un falso para una dependencia saliente. OnInitializedAsync y OnDisposingAsync coloca cada prueba entre corchetes para seeding y cleanup, y el desmontaje se ejecuta incluso cuando una prueba falla.
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);
}
}
Importante
Estas pruebas inician tu portal, así que cualquier cosa que lo impida también los detiene a ellos. En particular, con la autorización mejorada activada, el anfitrión valida el ID del sitio web configurado al inicio y falla con guía hasta que se proporciona un ID real
powerpagesite— la misma condición previa que ejecutar el portal.
Servicios de pruebas y lógica
Deriva de TestBase, registra lo que necesita el código bajo prueba y resuélvelo. El cliente Dataverse ya está autenticado.
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();
}
}
Pasar un ID de usuario a CreateOrganizationService(userId) para suplantar — Dataverse entonces hace cumplir los propios privilegios de ese usuario, que es la forma de comprobar el acceso a nivel de registro desde el lado de datos en lugar de a través de un punto final.
CreateWebApiHttpClientAsync() devuelve un cliente autenticado contra la API web de Dataverse (/api/data/v9.2/) para aserciones que son más fáciles de expresar en OData que a través del SDK. Necesita Azure:TenantId en configuración además del id del cliente y el secreto.
Cómo se desarrollan las pruebas
Ejecutalas con dotnet test, o desde el explorador de pruebas de tu IDE. Algunas características que merece la pena conocer antes de escribir muchas:
- Las colecciones se ejecutan en serie. Comparten un entorno Dataverse, y las escrituras paralelas en los mismos registros dificultan la reproducción de fallos. Esto se establece en
xunit.runner.json. - xunit construye una nueva instancia de la clase de prueba para cada método de prueba. La conexión autenticada de Dataverse se almacena en caché para todo el proceso y se distribuye como un clon ligero, por lo que eso cuesta un inicio de sesión por ejecución en lugar de uno por prueba — pero cualquier cosa cara que añadas a un constructor paga en cada prueba.
- Limpia lo que crees. Una prueba que deja registros hace que las aserciones de la siguiente partida dependan de la última. Prefiere valores únicos por prueba (un GUID en un campo de nombre) antes que los fijos, para que las ejecuciones repetidas no puedan colisionar.
Buenas prácticas
Apunta la suite a un entorno de desarrollo, nunca a producción. Estas son escrituras reales sobre datos reales, y una prueba que elimina lo que creó borrará el registro real si lo apuntas a uno.
