Pruebas de integración en ASP.NET Core

De Jos van der Til, Martin Costello y Javier Calvarro Nelson.

Las pruebas de integración garantizan que los componentes de una aplicación funcionan correctamente en un nivel que incluye la infraestructura auxiliar de la aplicación, como la base de datos, el sistema de archivos y la red. ASP.NET Core admite las pruebas de integración mediante un marco de pruebas unitarias con un host web de prueba y un servidor de pruebas en memoria.

En este artículo se da por hecho un conocimiento básico de las pruebas unitarias. Si no está familiarizado con los conceptos de las pruebas, consulte el artículo Pruebas unitarias en .NET Core y .NET Standard y su contenido vinculado.

Vea o descargue el código de ejemplo (cómo descargarlo)

La aplicación de ejemplo es una aplicación de Razor Pages que da por hecho un conocimiento básico de Razor Pages. Si no está familiarizado con Razor Pages, consulte los siguientes artículos:

Para probar SPA, se recomienda una herramienta como Playwright for .NET, que puede automatizar un explorador.

Introducción a las pruebas de integración

Las pruebas de integración evalúan los componentes de una aplicación en un nivel más amplio que las pruebas unitarias. Las pruebas unitarias se usan para probar componentes de software aislados, como métodos de clase individuales. Las pruebas de integración confirman que dos o más componentes de una aplicación funcionan juntos para generar un resultado esperado, lo que posiblemente incluya a todos los componentes necesarios para procesar por completo una solicitud.

Estas pruebas más amplias se usan para probar la infraestructura de la aplicación y todo el marco, lo que a menudo incluye los siguientes componentes:

  • Base de datos
  • Sistema de archivos
  • Dispositivos de red
  • Canalización de solicitud-respuesta

Las pruebas unitarias usan componentes fabricados, conocidos como emulaciones u objetos ficticios, en lugar de componentes de las infraestructura.

A diferencia de las pruebas unitarias, las pruebas de integración:

  • Usan los componentes reales que emplea la aplicación en producción.
  • Necesitan más código y procesamiento de datos.
  • Tardan más en ejecutarse.

Por lo tanto, limite el uso de pruebas de integración a los escenarios de infraestructura más importantes. Si un comportamiento se puede probar mediante una prueba unitaria o una prueba de integración, opte por la prueba unitaria.

En las conversaciones de las pruebas de integración, el proyecto probado suele denominarse Sistema a prueba o "SUT" para abreviar. En este artículo se usa "SUT" para referirse a la aplicación de ASP.NET Core que se va a probar.

No escriba pruebas de integración para cada permutación de datos y acceso a archivos con bases de datos y sistemas de archivos. Independientemente de cuántas ubicaciones de una aplicación interactúen con bases de datos y sistemas de archivos, un conjunto centrado de pruebas de integración de lectura, escritura, actualización y eliminación suele ser capaz de probar adecuadamente los componentes de sistema de archivos y base de datos. Use pruebas unitarias para las pruebas rutinarias de lógica de métodos que interactúan con estos componentes. En las pruebas unitarias, el uso de simulaciones o emulaciones de infraestructura provoca una ejecución más rápida de esas pruebas.

Pruebas de integración de ASP.NET Core

Para las pruebas de integración en ASP.NET Core se necesita lo siguiente:

  • Un proyecto de prueba, que se usa para contener y ejecutar las pruebas. El proyecto de prueba tiene una referencia al SUT.
  • El proyecto de prueba crea un host web de prueba para el SUT y usa un cliente de servidor de pruebas para controlar las solicitudes y las respuestas con el SUT.
  • Se usa un ejecutor de pruebas para ejecutar las pruebas y notificar los resultados de estas.

Las pruebas de integración siguen una secuencia de eventos que incluye los pasos de prueba normales Arrange, Act y Assert:

  1. Se configura el host web del SUT.
  2. Se crea un cliente de servidor de pruebas para enviar solicitudes a la aplicación.
  3. Se ejecuta el paso de prueba Arrange: la aplicación de prueba prepara una solicitud.
  4. Se ejecuta el paso de prueba Act: el cliente envía la solicitud y recibe la respuesta.
  5. Se ejecuta el paso de prueba Assert: la respuesta real se valida como correcta o errónea en función de una respuesta esperada.
  6. El proceso continúa hasta que se ejecutan todas las pruebas.
  7. Se notifican los resultados de la prueba.

Normalmente, el host web de prueba se configura de manera diferente al host web normal de la aplicación para las series de pruebas. Por ejemplo, puede usarse una base de datos diferente u otra configuración de aplicación para las pruebas.

Los componentes de infraestructura, como el host web de prueba y el servidor de pruebas en memoria (TestServer), se proporcionan o se administran mediante el paquete Microsoft.AspNetCore.Mvc.Testing. El uso de este paquete simplifica la creación y ejecución de pruebas.

El paquete Microsoft.AspNetCore.Mvc.Testing controla las siguientes tareas:

  • Copia el archivo de dependencias ( .deps) del SUT en el directorio bin del proyecto de prueba.
  • Establece la raíz de contenido en la raíz de proyecto del SUT de modo que se puedan encontrar archivos estáticos y páginas o vistas cuando se ejecuten las pruebas.
  • Proporciona la clase WebApplicationFactory para simplificar el arranque del SUT con TestServer.

En la documentación de las pruebas unitarias se explica cómo configurar un proyecto de prueba y un ejecutor de pruebas, además de incluirse instrucciones detalladas sobre cómo ejecutar pruebas y recomendaciones sobre cómo asignar nombres a las pruebas y las clases de prueba.

Separe las pruebas unitarias de las pruebas de integración en proyectos diferentes. Separación de las pruebas:

  • Ayuda a garantizar que los componentes de pruebas de infraestructura no se incluyan accidentalmente en las pruebas unitarias.
  • Permite controlar qué conjunto de pruebas se ejecutan.

Prácticamente no hay ninguna diferencia entre la configuración de las pruebas de aplicaciones Razor Pages y MVC. La única diferencia es cómo se asigna nombre a las pruebas. En una aplicación Razor Pages, las pruebas de puntos de conexión de página normalmente se nombran según la clase del modelo de página (por ejemplo, IndexPageTests para probar la integración de los componentes de la página Index). En una aplicación de MVC, las pruebas suelen organizarse por clases de controlador y denominarse según los controladores que prueban (por ejemplo, HomeControllerTests para probar la integración de componentes del controlador Home).

Requisitos previos de la aplicación de prueba

El proyecto de prueba debe:

Estos requisitos previos se pueden observar en la aplicación de ejemplo. Inspeccione el archivo tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. La aplicación de ejemplo usa el marco de pruebas xUnit y la biblioteca de análisis AngleSharp, así que la aplicación de ejemplo también hace referencia a:

En las aplicaciones que usan xunit.runner.visualstudio 2.4.2 o una versión posterior, el proyecto de prueba debe hacer referencia al paquete Microsoft.NET.Test.Sdk.

También se usa Entity Framework Core en las pruebas. Consulte el archivo del proyecto en GitHub.

Entorno del SUT

Si el entorno del SUT no está establecido, el valor predeterminado del entorno es Desarrollo.

Pruebas básicas con el valor predeterminado WebApplicationFactory

Exponga la clase Program definida implícitamente al proyecto de prueba de una de las maneras siguientes:

  • Exponga los tipos internos de la aplicación web al proyecto de prueba. Esto se puede hacer en el archivo del proyecto del SUT (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Haga que la clase Program sea pública mediante una declaración de clase parcial:

    var builder = WebApplication.CreateBuilder(args);
    // ... Configure services, routes, etc.
    app.Run();
    + public partial class Program { }
    

    La aplicación de ejemplo usa el enfoque de clase parcial Program.

WebApplicationFactory<TEntryPoint> se usa para crear un elemento TestServer para pruebas de integración. TEntryPoint es la clase de punto de entrada del SUT, normalmente Program.cs.

Las clases de prueba implementan una interfaz de accesorio de clase (IClassFixture) para indicar que la clase contiene pruebas y proporcionan instancias de objeto compartidas en las pruebas de la clase.

La siguiente clase de prueba, BasicTests, usa WebApplicationFactory para arrancar el SUT y proporcionar un valor HttpClient a un método de prueba, Get_EndpointsReturnSuccessAndCorrectContentType. El método comprueba que el código de estado de la respuesta es correcto (200-299) y que el encabezado Content-Type es text/html; charset=utf-8 para varias páginas de la aplicación.

CreateClient() crea una instancia de HttpClient que sigue automáticamente los redireccionamientos y controla las cookies.

public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

De forma predeterminada, las cookies no esenciales no se conservan entre solicitudes si está habilitada la directiva de consentimiento del Reglamento general de protección de datos. Para conservar las cookies no esenciales, como las usadas por el proveedor TempData, márquelas como esenciales en las pruebas. Para obtener instrucciones sobre cómo marcar una cookie como esencial, consulta Cookies esenciales.

AngleSharp frente a Application Parts para las comprobaciones antifalsificación

En este artículo se usa el analizador AngleSharp para controlar las comprobaciones antifalsificación cargando páginas y analizando el HTML. Para probar los puntos de conexión de las vistas de controlador y Razor Pages en un nivel inferior, sin preocuparse de cómo se representan en el explorador, considere la posibilidad de usar Application Parts. El enfoque de elementos de aplicación inserta un controlador o una instancia de Razor Pages en la aplicación que se puede usar para realizar solicitudes JSON para obtener los valores necesarios. Para obtener más información, consulte el blog sobre pruebas de integración en recursos de ASP.NET Core protegidos con antifalsificación mediante elementos de aplicación y el repositorio de GitHub asociado de Martin Costello.

Personalización de WebApplicationFactory

La configuración de host web se puede crear independientemente de las clases de prueba al heredar de WebApplicationFactory<TEntryPoint> para crear una o más fábricas personalizadas:

  1. Herede de WebApplicationFactory y reemplace ConfigureWebHost. IWebHostBuilder permite la configuración de la colección de servicios con IWebHostBuilder.ConfigureServices

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    

    El método InitializeDbForTests realiza la propagación de la base de datos de la aplicación de ejemplo. El método se describe en la sección Ejemplo de pruebas de integración: organización de la aplicación de prueba.

    El contexto de la base de datos del SUT se registra en Program.cs. La devolución de llamada builder.ConfigureServices de la aplicación de prueba se ejecuta después de que se ejecute el código Program.cs de la aplicación. Para usar una base de datos diferente para las pruebas a la base de datos de la aplicación, el contexto de la base de datos de la aplicación debe reemplazarse en builder.ConfigureServices.

    La aplicación de ejemplo busca el descriptor de servicio del contexto de la base de datos y usa el descriptor para quitar el registro del servicio. Luego, la fábrica agrega una nueva instancia de ApplicationDbContext que usa una base de datos en memoria para las pruebas.

    Para conectarse a otra base de datos, cambie la DbConnection. Para usar una base de datos de prueba de SQL Server:

  1. Use la instancia de CustomWebApplicationFactory personalizada en las clases de prueba. En el ejemplo siguiente se usa la fábrica en la clase IndexPageTests:

    public class IndexPageTests :
        IClassFixture<CustomWebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<Program>
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<Program> factory)
        {
            _factory = factory;
            _client = factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
        }
    

    El cliente de la aplicación de ejemplo está configurado para evitar que HttpClient siga los redireccionamientos. Como se explica más adelante en la sección Autenticación ficticia, esto permite que las pruebas comprueben el resultado de la primera respuesta de la aplicación. La primera respuesta es un redireccionamiento en muchas de estas pruebas con un encabezado Location.

  2. Una prueba típica usa los métodos auxiliares y HttpClient para procesar la solicitud y la respuesta:

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
    

Cualquier solicitud POST al SUT debe satisfacer la comprobación antifalsificación que realiza automáticamente el sistema antifalsificación de protección de datos de la aplicación. Para organizar la solicitud POST de una prueba, la aplicación de prueba debe:

  1. Realizar una solicitud para la página.
  2. Analizar la cookie antifalsificación y solicitar el token de validación de la respuesta.
  3. Realizar la solicitud POST con la cookie antifalsificación y el token de validación de la solicitud aplicados.

Los métodos de extensión auxiliares SendAsync (Helpers/HttpClientExtensions.cs) y el método auxiliar GetDocumentAsync (Helpers/HtmlHelpers.cs) de la aplicación de ejemplo usan el analizador AngleSharp para controlar la comprobación antifalsificación con los métodos siguientes:

  • GetDocumentAsync: recibe HttpResponseMessage y devuelve IHtmlDocument. GetDocumentAsync usa una fábrica que prepara una respuesta virtual basada en la instancia de HttpResponseMessage original. Para obtener más información, vea la documentación de AngleSharp.
  • Métodos de extensión SendAsync para que HttpClient redacte un elemento HttpRequestMessage y llame SendAsync(HttpRequestMessage) a fin de enviar solicitudes al SUT. Las sobrecargas de SendAsync aceptan el formulario HTML (IHtmlFormElement) y lo siguiente:
    • Botón Enviar del formulario (IHtmlElement)
    • Colección de valores del formulario (IEnumerable<KeyValuePair<string, string>>)
    • Botón Enviar (IHtmlElement) y valores del formulario (IEnumerable<KeyValuePair<string, string>>)

AngleSharp es una biblioteca de análisis de terceros que se usa con fines de demostración en este artículo y en la aplicación de ejemplo. AngleSharp no es compatible con las pruebas de integración de aplicaciones ASP.NET Core ni necesario. Se pueden usar otros analizadores, como Html Agility Pack (HAP). Otro enfoque consiste en escribir código para controlar el token de comprobación de solicitudes y la cookie antifalsificación del sistema antifalsificación directamente. Consulte AngleSharp frente a Application Parts para las comprobaciones antifalsificación en este artículo para obtener más información.

El proveedor de base de datos en memoria de EF-Core se puede usar para pruebas básicas y limitadas, pero el proveedor SQLite es la opción que se recomienda para las pruebas en memoria.

Consulte Extensión del inicio con filtros de inicio que muestra cómo configurar el middleware mediante IStartupFilter, que es útil cuando una prueba requiere un servicio personalizado o middleware.

Personalización del cliente con WithWebHostBuilder

Cuando se requiere configuración adicional en un método de prueba, WithWebHostBuilder crea una nueva instancia de WebApplicationFactory con un elemento IWebHostBuilder que se personaliza aún más mediante la configuración.

El código de ejemplo llama a WithWebHostBuilder para reemplazar los servicios configurados por códigos auxiliares de prueba. Para más información y uso de ejemplo, consulte Inserción de servicios ficticios en este artículo.

El método de prueba Post_DeleteMessageHandler_ReturnsRedirectToRoot de la aplicación de ejemplo muestra el uso de WithWebHostBuilder. Esta prueba realiza una eliminación de registros en la base de datos al desencadenar un envío de formulario en el SUT.

Dado que otra prueba en la clase IndexPageTests realiza una operación que elimina todos los registros de la base de datos y puede ejecutarse antes que el método Post_DeleteMessageHandler_ReturnsRedirectToRoot, la base de datos se vuelve a propagar en este método de prueba para asegurarse de que un registro está presente para que lo elimine el SUT. La selección del primer botón de eliminación del formulario messages en el SUT se simula en la solicitud al SUT:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Opciones de cliente

Consulte la página WebApplicationFactoryClientOptions para ver los valores predeterminados y las opciones disponibles al crear instancias de HttpClient.

Cree la clase WebApplicationFactoryClientOptions y pásela al método CreateClient():

public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program>
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

NOTA: Para evitar advertencias de redireccionamiento HTTPS en los registros al usar middleware de redirección HTTPS, establezca BaseAddress = new Uri("https://localhost")

Inserción de servicios ficticios

Los servicios se pueden invalidar en una prueba con una llamada a ConfigureTestServices en el generador de hosts. Para limitar el ámbito de los servicios invalidados a la propia prueba, el método WithWebHostBuilder se usa para recuperar un generador de hosts. Esto se puede ver en las siguientes pruebas:

El SUT de ejemplo incluye un servicio con ámbito que devuelve una cita. La cita se inserta en un campo oculto de la página Index cuando se solicita esta.

Services/IQuoteService.cs:

public interface IQuoteService
{
    Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

Program.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.cs:

public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

El siguiente marcado se genera cuando se ejecuta la aplicación SUT:

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">

Para probar el servicio y la inserción de citas en una prueba de integración, la prueba inserta un servicio ficticio en el SUT. El servicio ficticio reemplaza el elemento QuoteService de la aplicación por un servicio proporcionado por la aplicación de prueba, denominado TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

Se llama a ConfigureTestServices y se registra el servicio con ámbito:

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

El marcado producido durante la ejecución de la prueba refleja el texto de la cita proporcionado por TestQuoteService, por lo que la aserción pasa:

<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">

Autenticación ficticia

Las pruebas de la clase AuthTests comprueban que un punto de conexión seguro:

  • Redirige a un usuario no autenticado a la página de inicio de sesión de la aplicación.
  • Devuelve el contenido de un usuario autenticado.

En el SUT, la página /SecurePage utiliza una convención AuthorizePage para aplicar un elemento AuthorizeFilter a la página. Para más información, consulte las convenciones de autorización de Razor Pages.

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});

En la prueba Get_SecurePageRedirectsAnUnauthenticatedUser, un elemento WebApplicationFactoryClientOptions se establece para no permitir redireccionamientos configurando AllowAutoRedirect en false:

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}

Al no permitir que el cliente siga el redireccionamiento, se pueden realizar las siguientes comprobaciones:

  • El código de estado devuelto por el SUT se puede comprobar con el resultado HttpStatusCode.Redirect esperado, no el código de estado final después del redireccionamiento a la página de inicio de sesión, que sería HttpStatusCode.OK.
  • Se comprueba el valor del encabezado Location en los encabezados de respuesta para confirmar que empieza con http://localhost/Identity/Account/Login, no la respuesta de la página de inicio de sesión final, donde el encabezado Location no estaría presente.

La aplicación de prueba puede simular un elemento AuthenticationHandler<TOptions> en ConfigureTestServices para probar aspectos de autenticación y autorización. Un escenario mínimo devuelve un elemento AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

Se llama a TestAuthHandler para autenticar a un usuario cuando el esquema de autenticación está establecido en TestScheme, donde AddAuthentication está registrado para ConfigureTestServices. Es importante que el esquema de TestScheme coincida con el que espera la aplicación. En caso contrario, la autenticación no funcionará.

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(defaultScheme: "TestScheme")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "TestScheme", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Para obtener más información sobre WebApplicationFactoryClientOptions, vea la sección Opciones de cliente.

Pruebas básicas de middleware de autenticación

Consulte este repositorio de GitHub para ver las pruebas básicas de middleware de autenticación. Contiene un servidor de prueba que es específico del escenario de prueba.

Establecimiento del entorno

Establezca el entorno en el generador de aplicaciones personalizadas:

public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbContextOptions<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}

Cómo deduce la infraestructura de prueba la ruta de acceso raíz del contenido de la aplicación

El constructor WebApplicationFactory deduce la ruta de acceso raíz del contenido de la aplicación buscando un elemento WebApplicationFactoryContentRootAttribute en el ensamblado que contiene las pruebas de integración con una clave igual a TEntryPoint del ensamblado System.Reflection.Assembly.FullName. En caso de que no se encuentre la clave correcta, WebApplicationFactory vuelve a buscar un archivo de solución ( .sln) y anexa el nombre de ensamblado TEntryPoint al directorio de la solución. El directorio raíz de la aplicación (la ruta de acceso raíz del contenido) se usa para detectar los archivos de contenido y las vistas.

Deshabilitación de la creación de instantáneas

La creación de instantáneas hace que las pruebas se ejecuten en un directorio diferente al de salida. Si las pruebas se basan en la carga de archivos relativos a Assembly.Location y detecta algún problema, puede que tenga que deshabilitar la copia de propiedades reemplazadas.

Para deshabilitar dicha copia al usar xUnit, cree un archivo xunit.runner.json en el directorio del proyecto de prueba, con las opción de configuración correcta:

{
  "shadowCopy": false
}

Eliminación de objetos

Una vez ejecutadas las pruebas de la implementación IClassFixture, TestServer y HttpClient se eliminan cuando xUnit quita WebApplicationFactory. Si los objetos cuya instancia ha creado el desarrollador requieren eliminación, quítelos de la implementación IClassFixture. Para obtener más información, vea Implementar un método Dispose.

Ejemplo de pruebas de integración

La aplicación de ejemplo se compone de dos aplicaciones:

Aplicación Directorio del proyecto Descripción
Aplicación de mensajes (el SUT) src/RazorPagesProject Permite a un usuario agregar, analizar mensajes y eliminarlos todos o uno solo.
Probar la aplicación tests/RazorPagesProject.Tests Se usa para probar la integración del SUT.

Las pruebas se pueden ejecutar con las características de prueba integradas de un IDE, como Visual Studio. Si usa Visual Studio Code o la línea de comandos, ejecute el comando siguiente en un símbolo del sistema en el directorio tests/RazorPagesProject.Tests:

dotnet test

Organización de aplicación de mensajes (SUT)

El sistema a prueba es un sistema de mensajes de Razor Pages con las siguientes características:

  • La página Index de la aplicación (Pages/Index.cshtml y Pages/Index.cshtml.cs) proporciona una interfaz de usuario y métodos de modelo de página para controlar la adición, la eliminación y el análisis de mensajes (promedio de palabras por mensaje).
  • La clase Message (Data/Message.cs) describe un mensaje con dos propiedades: Id (clave) y Text (mensaje). Se necesita la propiedad Text, que está limitada a 200 caracteres.
  • Los mensajes se almacenan en la base de datos en memoria de Entity Framework†.
  • La aplicación contiene una capa de acceso a datos (DAL) en su clase de contexto de base de datos, AppDbContext (Data/AppDbContext.cs).
  • Si la base de datos está vacía al inicio de una aplicación, el almacén de mensajes se inicializa con tres mensajes.
  • La aplicación incluye un elemento /SecurePage al que solo puede acceder un usuario autenticado.

†En el artículo de EF, Pruebas con InMemory, se explica cómo usar una base de datos en memoria con las pruebas con MSTest. En este tema se usa el marco de pruebas xUnit. Los conceptos y las implementaciones de prueba de diferentes marcos de pruebas son similares, pero no idénticos.

Aunque la aplicación no usa el patrón del repositorio y no es un buen ejemplo del patrón de unidad de trabajo (UoW), Razor Pages admite estos patrones de desarrollo. Para obtener más información, vea Diseño del nivel de persistencia de infraestructura y Lógica del controlador de pruebas (el ejemplo implementa el patrón del repositorio).

Organización de la aplicación de prueba

La aplicación de prueba es una aplicación de consola dentro del directorio tests/RazorPagesProject.Tests.

Directorio de la aplicación de prueba Descripción
AuthTests Contiene métodos de prueba para:
  • Acceder a una página segura mediante un usuario no autenticado.
  • Acceder a una página segura mediante un usuario autenticado con una simulación AuthenticationHandler<TOptions>.
  • Obtener un perfil de usuario de GitHub y comprobar el inicio de sesión de usuario del perfil.
BasicTests Contiene un método de prueba para el enrutamiento y el tipo de contenido.
IntegrationTests Contiene las pruebas de integración de la página Index que usa la clase WebApplicationFactory personalizada.
Helpers/Utilities
  • Utilities.cs contiene el método InitializeDbForTests que se usa para propagar la base de datos con datos de prueba.
  • HtmlHelpers.cs proporciona un método para devolver un elemento IHtmlDocument de AngleSharp para que lo usen los métodos de prueba.
  • HttpClientExtensions.cs proporciona sobrecargas para que SendAsync envíe solicitudes al SUT.

El marco de pruebas es xUnit. Las pruebas de integración se llevan a cabo mediante el elemento Microsoft.AspNetCore.TestHost, que incluye TestServer. Dado que el paquete Microsoft.AspNetCore.Mvc.Testing se usa para configurar el host de prueba y el servidor de pruebas, los paquetes TestHost y TestServer no requieren referencias de paquete directas en el archivo de proyecto ni la configuración de desarrollador de la aplicación de prueba en la aplicación de prueba.

Las pruebas de integración suelen requerir un pequeño conjunto de datos en la base de datos antes de la ejecución de la prueba. Por ejemplo, una prueba de eliminación consiste en la eliminación de un registro de base de datos, por lo que la base de datos debe tener al menos un registro para que la solicitud de eliminación se realice correctamente.

La aplicación de ejemplo propaga la base de datos con tres mensajes en Utilities.cs que las pruebas pueden usar al ejecutarse:

public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}

El contexto de la base de datos del SUT se registra en Program.cs. La devolución de llamada builder.ConfigureServices de la aplicación de prueba se ejecuta después de que se ejecute el código Program.cs de la aplicación. Para usar otra base de datos para las pruebas, el contexto de la base de datos de la aplicación debe reemplazarse en builder.ConfigureServices. Para obtener más información, vea la sección Personalización de WebApplicationFactory.

Recursos adicionales

En este artículo se da por hecho un conocimiento básico de las pruebas unitarias. Si no está familiarizado con los conceptos de las pruebas, vea el tema Pruebas unitaria en .NET Core y .NET Standard y su contenido vinculado.

Vea o descargue el código de ejemplo (cómo descargarlo)

La aplicación de ejemplo es una aplicación de Razor Pages que da por hecho un conocimiento básico de Razor Pages. Si no está familiarizado con Razor Pages, consulte los temas siguientes:

Nota

Para probar SPA, se recomienda una herramienta como Playwright for .NET, que puede automatizar un explorador.

Introducción a las pruebas de integración

Las pruebas de integración evalúan los componentes de una aplicación en un nivel más amplio que las pruebas unitarias. Las pruebas unitarias se usan para probar componentes de software aislados, como métodos de clase individuales. Las pruebas de integración confirman que dos o más componentes de una aplicación funcionan juntos para generar un resultado esperado, lo que posiblemente incluya a todos los componentes necesarios para procesar por completo una solicitud.

Estas pruebas más amplias se usan para probar la infraestructura de la aplicación y todo el marco, lo que a menudo incluye los siguientes componentes:

  • Base de datos
  • Sistema de archivos
  • Dispositivos de red
  • Canalización de solicitud-respuesta

Las pruebas unitarias usan componentes fabricados, conocidos como emulaciones u objetos ficticios, en lugar de componentes de las infraestructura.

A diferencia de las pruebas unitarias, las pruebas de integración:

  • Usan los componentes reales que emplea la aplicación en producción.
  • Necesitan más código y procesamiento de datos.
  • Tardan más en ejecutarse.

Por lo tanto, limite el uso de pruebas de integración a los escenarios de infraestructura más importantes. Si un comportamiento se puede probar mediante una prueba unitaria o una prueba de integración, opte por la prueba unitaria.

En las conversaciones de las pruebas de integración, el proyecto probado suele denominarse Sistema a prueba o "SUT" para abreviar. En este artículo se usa "SUT" para referirse a la aplicación de ASP.NET Core que se va a probar.

No escriba pruebas de integración para cada permutación de datos y acceso a archivos con bases de datos y sistemas de archivos. Independientemente de cuántas ubicaciones de una aplicación interactúen con bases de datos y sistemas de archivos, un conjunto centrado de pruebas de integración de lectura, escritura, actualización y eliminación suele ser capaz de probar adecuadamente los componentes de sistema de archivos y base de datos. Use pruebas unitarias para las pruebas rutinarias de lógica de métodos que interactúan con estos componentes. En las pruebas unitarias, el uso de simulaciones o emulaciones de infraestructura provoca una ejecución más rápida de esas pruebas.

Pruebas de integración de ASP.NET Core

Para las pruebas de integración en ASP.NET Core se necesita lo siguiente:

  • Un proyecto de prueba, que se usa para contener y ejecutar las pruebas. El proyecto de prueba tiene una referencia al SUT.
  • El proyecto de prueba crea un host web de prueba para el SUT y usa un cliente de servidor de pruebas para controlar las solicitudes y las respuestas con el SUT.
  • Se usa un ejecutor de pruebas para ejecutar las pruebas y notificar los resultados de estas.

Las pruebas de integración siguen una secuencia de eventos que incluye los pasos de prueba normales Arrange, Act y Assert:

  1. Se configura el host web del SUT.
  2. Se crea un cliente de servidor de pruebas para enviar solicitudes a la aplicación.
  3. Se ejecuta el paso de prueba Arrange: la aplicación de prueba prepara una solicitud.
  4. Se ejecuta el paso de prueba Act: el cliente envía la solicitud y recibe la respuesta.
  5. Se ejecuta el paso de prueba Assert: la respuesta real se valida como correcta o errónea en función de una respuesta esperada.
  6. El proceso continúa hasta que se ejecutan todas las pruebas.
  7. Se notifican los resultados de la prueba.

Normalmente, el host web de prueba se configura de manera diferente al host web normal de la aplicación para las series de pruebas. Por ejemplo, puede usarse una base de datos diferente u otra configuración de aplicación para las pruebas.

Los componentes de infraestructura, como el host web de prueba y el servidor de pruebas en memoria (TestServer), se proporcionan o se administran mediante el paquete Microsoft.AspNetCore.Mvc.Testing. El uso de este paquete simplifica la creación y ejecución de pruebas.

El paquete Microsoft.AspNetCore.Mvc.Testing controla las siguientes tareas:

  • Copia el archivo de dependencias ( .deps) del SUT en el directorio bin del proyecto de prueba.
  • Establece la raíz de contenido en la raíz de proyecto del SUT de modo que se puedan encontrar archivos estáticos y páginas o vistas cuando se ejecuten las pruebas.
  • Proporciona la clase WebApplicationFactory para simplificar el arranque del SUT con TestServer.

En la documentación de las pruebas unitarias se explica cómo configurar un proyecto de prueba y un ejecutor de pruebas, además de incluirse instrucciones detalladas sobre cómo ejecutar pruebas y recomendaciones sobre cómo asignar nombres a las pruebas y las clases de prueba.

Separe las pruebas unitarias de las pruebas de integración en proyectos diferentes. Separación de las pruebas:

  • Ayuda a garantizar que los componentes de pruebas de infraestructura no se incluyan accidentalmente en las pruebas unitarias.
  • Permite controlar qué conjunto de pruebas se ejecutan.

Prácticamente no hay ninguna diferencia entre la configuración de las pruebas de aplicaciones Razor Pages y MVC. La única diferencia es cómo se asigna nombre a las pruebas. En una aplicación Razor Pages, las pruebas de puntos de conexión de página normalmente se nombran según la clase del modelo de página (por ejemplo, IndexPageTests para probar la integración de los componentes de la página Index). En una aplicación de MVC, las pruebas suelen organizarse por clases de controlador y denominarse según los controladores que prueban (por ejemplo, HomeControllerTests para probar la integración de componentes del controlador Home).

Requisitos previos de la aplicación de prueba

El proyecto de prueba debe:

Estos requisitos previos se pueden observar en la aplicación de ejemplo. Inspeccione el archivo tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. La aplicación de ejemplo usa el marco de pruebas xUnit y la biblioteca de análisis AngleSharp, así que la aplicación de ejemplo también hace referencia a:

En las aplicaciones que usan xunit.runner.visualstudio 2.4.2 o una versión posterior, el proyecto de prueba debe hacer referencia al paquete Microsoft.NET.Test.Sdk.

También se usa Entity Framework Core en las pruebas. La aplicación hace referencia a:

Entorno del SUT

Si el entorno del SUT no está establecido, el valor predeterminado del entorno es Desarrollo.

Pruebas básicas con el valor predeterminado WebApplicationFactory

WebApplicationFactory<TEntryPoint> se usa para crear un elemento TestServer para pruebas de integración. TEntryPoint es la clase de punto de entrada del SUT, normalmente la clase Startup.

Las clases de prueba implementan una interfaz de accesorio de clase (IClassFixture) para indicar que la clase contiene pruebas y proporcionan instancias de objeto compartidas en las pruebas de la clase.

La siguiente clase de prueba, BasicTests, usa WebApplicationFactory para arrancar el SUT y proporcionar un valor HttpClient a un método de prueba, Get_EndpointsReturnSuccessAndCorrectContentType. El método comprueba si el código de estado de la respuesta es correcto (códigos de estado del rango 200-299) y si el encabezado Content-Type es text/html; charset=utf-8 para varias páginas de la aplicación.

CreateClient() crea una instancia de HttpClient que sigue automáticamente los redireccionamientos y controla las cookies.

public class BasicTests 
    : IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
    private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;

    public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

De forma predeterminada, las cookies no esenciales no se conservan entre solicitudes si está habilitada la directiva de consentimiento de RGPD. Para conservar las cookies no esenciales, como las usadas por el proveedor TempData, márquelas como esenciales en las pruebas. Para obtener instrucciones sobre cómo marcar una cookie como esencial, consulta Cookies esenciales.

Personalización de WebApplicationFactory

La configuración de host web se puede crear independientemente de las clases de prueba al heredar de WebApplicationFactory para crear una o más fábricas personalizadas:

  1. Herede de WebApplicationFactory y reemplace ConfigureWebHost. IWebHostBuilder permite la configuración de la colección de servicios con ConfigureServices:

    public class CustomWebApplicationFactory<TStartup>
        : WebApplicationFactory<TStartup> where TStartup: class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(descriptor);
    
                services.AddDbContext<ApplicationDbContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                });
    
                var sp = services.BuildServiceProvider();
    
                using (var scope = sp.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices.GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
    
                    db.Database.EnsureCreated();
    
                    try
                    {
                        Utilities.InitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding the " +
                            "database with test messages. Error: {Message}", ex.Message);
                    }
                }
            });
        }
    }
    

    El método InitializeDbForTests realiza la propagación de la base de datos de la aplicación de ejemplo. El método se describe en la sección Ejemplo de pruebas de integración: organización de la aplicación de prueba.

    El contexto de la base de datos del SUT se registra en su método Startup.ConfigureServices. La devolución de llamada builder.ConfigureServices de la aplicación de prueba se ejecuta después de que se ejecute el código Startup.ConfigureServices de la aplicación. El orden de ejecución es un cambio importante para el host genérico con la versión de ASP.NET Core 3.0. Para usar una base de datos diferente para las pruebas a la base de datos de la aplicación, el contexto de la base de datos de la aplicación debe reemplazarse en builder.ConfigureServices.

    En el caso de sistemas a prueba que todavía usan el host de web, la devolución de llamada builder.ConfigureServices de la aplicación de prueba se ejecuta antes que el código Startup.ConfigureServices del sistema a prueba. La devolución de llamada builder.ConfigureTestServices de la aplicación de prueba se ejecuta después.

    La aplicación de ejemplo busca el descriptor de servicio del contexto de la base de datos y usa el descriptor para quitar el registro del servicio. Luego, la fábrica agrega una nueva instancia de ApplicationDbContext que usa una base de datos en memoria para las pruebas.

    Para conectarse a una base de datos diferente a la base de datos en memoria, cambie la llamada a UseInMemoryDatabase para conectar el contexto a otra base de datos. Para usar una base de datos de prueba de SQL Server:

    services.AddDbContext<ApplicationDbContext>((options, context) => 
    {
        context.UseSqlServer(
            Configuration.GetConnectionString("TestingDbConnectionString"));
    });
    
  2. Use la instancia de CustomWebApplicationFactory personalizada en las clases de prueba. En el ejemplo siguiente se usa la fábrica en la clase IndexPageTests:

    public class IndexPageTests : 
        IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> 
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<RazorPagesProject.Startup> factory)
        {
            _factory = factory;
            _client = factory.CreateClient(new WebApplicationFactoryClientOptions
                {
                    AllowAutoRedirect = false
                });
        }
    

    El cliente de la aplicación de ejemplo está configurado para evitar que HttpClient siga los redireccionamientos. Como se explica más adelante en la sección Autenticación ficticia, esto permite que las pruebas comprueben el resultado de la primera respuesta de la aplicación. La primera respuesta es un redireccionamiento en muchas de estas pruebas con un encabezado Location.

  3. Una prueba típica usa los métodos auxiliares y HttpClient para procesar la solicitud y la respuesta:

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
    

Cualquier solicitud POST al SUT debe satisfacer la comprobación antifalsificación que realiza automáticamente el sistema antifalsificación de protección de datos de la aplicación. Para organizar la solicitud POST de una prueba, la aplicación de prueba debe:

  1. Realizar una solicitud para la página.
  2. Analizar la cookie antifalsificación y solicitar el token de validación de la respuesta.
  3. Realizar la solicitud POST con la cookie antifalsificación y el token de validación de la solicitud aplicados.

Los métodos de extensión auxiliares SendAsync (Helpers/HttpClientExtensions.cs) y el método auxiliar GetDocumentAsync (Helpers/HtmlHelpers.cs) de la aplicación de ejemplo usan el analizador AngleSharp para controlar la comprobación antifalsificación con los métodos siguientes:

  • GetDocumentAsync: recibe HttpResponseMessage y devuelve IHtmlDocument. GetDocumentAsync usa una fábrica que prepara una respuesta virtual basada en la instancia de HttpResponseMessage original. Para obtener más información, vea la documentación de AngleSharp.
  • Métodos de extensión SendAsync para que HttpClient redacte un elemento HttpRequestMessage y llame SendAsync(HttpRequestMessage) a fin de enviar solicitudes al SUT. Las sobrecargas de SendAsync aceptan el formulario HTML (IHtmlFormElement) y lo siguiente:
    • Botón Enviar del formulario (IHtmlElement)
    • Colección de valores del formulario (IEnumerable<KeyValuePair<string, string>>)
    • Botón Enviar (IHtmlElement) y valores del formulario (IEnumerable<KeyValuePair<string, string>>)

Nota

AngleSharp es una biblioteca de análisis de terceros que se usa con fines de demostración en este tema y en la aplicación de ejemplo. AngleSharp no es compatible con las pruebas de integración de aplicaciones ASP.NET Core ni necesario. Se pueden usar otros analizadores, como Html Agility Pack (HAP). Otro enfoque consiste en escribir código para controlar el token de comprobación de solicitudes y la cookie antifalsificación del sistema antifalsificación directamente.

Nota:

El proveedor de base de datos en memoria de EF-Core se puede usar para pruebas básicas y limitadas, pero el proveedor SQLite es la opción que se recomienda para las pruebas en memoria.

Personalización del cliente con WithWebHostBuilder

Cuando se requiere configuración adicional en un método de prueba, WithWebHostBuilder crea una nueva instancia de WebApplicationFactory con un elemento IWebHostBuilder que se personaliza aún más mediante la configuración.

El método de prueba Post_DeleteMessageHandler_ReturnsRedirectToRoot de la aplicación de ejemplo muestra el uso de WithWebHostBuilder. Esta prueba realiza una eliminación de registros en la base de datos al desencadenar un envío de formulario en el SUT.

Dado que otra prueba en la clase IndexPageTests realiza una operación que elimina todos los registros de la base de datos y puede ejecutarse antes que el método Post_DeleteMessageHandler_ReturnsRedirectToRoot, la base de datos se vuelve a propagar en este método de prueba para asegurarse de que un registro está presente para que lo elimine el SUT. La selección del primer botón de eliminación del formulario messages en el SUT se simula en la solicitud al SUT:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                var serviceProvider = services.BuildServiceProvider();

                using (var scope = serviceProvider.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices
                        .GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<IndexPageTests>>();

                    try
                    {
                        Utilities.ReinitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding " +
                            "the database with test messages. Error: {Message}", 
                            ex.Message);
                    }
                }
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Opciones de cliente

En la tabla siguiente se muestran los elementos predeterminados WebApplicationFactoryClientOptions que hay disponibles al crear instancias de HttpClient.

Opción Descripción Valor predeterminado
AllowAutoRedirect Obtiene o establece si las instancias de HttpClient deben seguir automáticamente las respuestas de redireccionamiento. true
BaseAddress Obtiene o establece la dirección base de las instancias de HttpClient. http://localhost
HandleCookies Obtiene o establece si las instancias de HttpClient deben controlar las cookies. true
MaxAutomaticRedirections Obtiene o establece el número máximo de respuestas de redireccionamiento que deben seguir las instancias de HttpClient. 7

Cree la clase WebApplicationFactoryClientOptions y pásela al método CreateClient() (los valores predeterminados se muestran en el ejemplo de código):

// Default client option values are shown
var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = true;
clientOptions.BaseAddress = new Uri("http://localhost");
clientOptions.HandleCookies = true;
clientOptions.MaxAutomaticRedirections = 7;

_client = _factory.CreateClient(clientOptions);

Inserción de servicios ficticios

Los servicios se pueden invalidar en una prueba con una llamada a ConfigureTestServices en el generador de hosts. Para insertar servicios ficticios, el SUT debe tener una clase Startup con un método Startup.ConfigureServices.

El SUT de ejemplo incluye un servicio con ámbito que devuelve una cita. La cita se inserta en un campo oculto de la página Index cuando se solicita esta.

Services/IQuoteService.cs:

public interface IQuoteService
{
    Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

Startup.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.cs:

public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

El siguiente marcado se genera cuando se ejecuta la aplicación SUT:

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">

Para probar el servicio y la inserción de citas en una prueba de integración, la prueba inserta un servicio ficticio en el SUT. El servicio ficticio reemplaza el elemento QuoteService de la aplicación por un servicio proporcionado por la aplicación de prueba, denominado TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

Se llama a ConfigureTestServices y se registra el servicio con ámbito:

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

El marcado producido durante la ejecución de la prueba refleja el texto de la cita proporcionado por TestQuoteService, por lo que la aserción pasa:

<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">

Autenticación ficticia

Las pruebas de la clase AuthTests comprueban que un punto de conexión seguro:

  • Redirige a un usuario no autenticado a la página de inicio de sesión de la aplicación.
  • Devuelve el contenido de un usuario autenticado.

En el SUT, la página /SecurePage utiliza una convención AuthorizePage para aplicar un elemento AuthorizeFilter a la página. Para más información, consulte las convenciones de autorización de Razor Pages.

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});

En la prueba Get_SecurePageRedirectsAnUnauthenticatedUser, un elemento WebApplicationFactoryClientOptions se establece para no permitir redireccionamientos configurando AllowAutoRedirect en false:

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login", 
        response.Headers.Location.OriginalString);
}

Al no permitir que el cliente siga el redireccionamiento, se pueden realizar las siguientes comprobaciones:

  • El código de estado devuelto por el SUT se puede comprobar con el resultado HttpStatusCode.Redirect esperado, no el código de estado final después del redireccionamiento a la página de inicio de sesión, que sería HttpStatusCode.OK.
  • Se comprueba el valor del encabezado Location en los encabezados de respuesta para confirmar que empieza con http://localhost/Identity/Account/Login, no la respuesta de la página de inicio de sesión final, donde el encabezado Location no estaría presente.

La aplicación de prueba puede simular un elemento AuthenticationHandler<TOptions> en ConfigureTestServices para probar aspectos de autenticación y autorización. Un escenario mínimo devuelve un elemento AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, 
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "Test");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

Se llama a TestAuthHandler para autenticar a un usuario cuando el esquema de autenticación está establecido en Test, donde AddAuthentication está registrado para ConfigureTestServices. Es importante que el esquema de Test coincida con el que espera la aplicación. En caso contrario, la autenticación no funcionará.

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication("Test")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "Test", options => {});
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Test");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Para obtener más información sobre WebApplicationFactoryClientOptions, vea la sección Opciones de cliente.

Establecimiento del entorno

De forma predeterminada, el entorno de host y aplicación del SUT se configura para usar el entorno Desarrollo. Para invalidar el entorno del sistema a prueba al usar IHostBuilder:

  • Establezca la variable de entorno ASPNETCORE_ENVIRONMENT (por ejemplo, Staging, Production u otro valor personalizado, como Testing).
  • Invalide CreateHostBuilder en la aplicación de prueba para leer las variables de entorno con el prefijo ASPNETCORE.
protected override IHostBuilder CreateHostBuilder() =>
    base.CreateHostBuilder()
        .ConfigureHostConfiguration(
            config => config.AddEnvironmentVariables("ASPNETCORE"));

Si el sistema a prueba usa el host web (IWebHostBuilder), invalide CreateWebHostBuilder:

protected override IWebHostBuilder CreateWebHostBuilder() =>
    base.CreateWebHostBuilder().UseEnvironment("Testing");

Cómo deduce la infraestructura de prueba la ruta de acceso raíz del contenido de la aplicación

El constructor WebApplicationFactory deduce la ruta de acceso raíz del contenido de la aplicación buscando un elemento WebApplicationFactoryContentRootAttribute en el ensamblado que contiene las pruebas de integración con una clave igual a TEntryPoint del ensamblado System.Reflection.Assembly.FullName. En caso de que no se encuentre la clave correcta, WebApplicationFactory vuelve a buscar un archivo de solución ( .sln) y anexa el nombre de ensamblado TEntryPoint al directorio de la solución. El directorio raíz de la aplicación (la ruta de acceso raíz del contenido) se usa para detectar los archivos de contenido y las vistas.

Deshabilitación de la creación de instantáneas

La creación de instantáneas hace que las pruebas se ejecuten en un directorio diferente al de salida. Si las pruebas se basan en la carga de archivos relativos a Assembly.Location y detecta algún problema, puede que tenga que deshabilitar la copia de propiedades reemplazadas.

Para deshabilitar dicha copia al usar xUnit, cree un archivo xunit.runner.json en el directorio del proyecto de prueba, con las opción de configuración correcta:

{
  "shadowCopy": false
}

Eliminación de objetos

Una vez ejecutadas las pruebas de la implementación IClassFixture, TestServer y HttpClient se eliminan cuando xUnit quita WebApplicationFactory. Si los objetos cuya instancia ha creado el desarrollador requieren eliminación, quítelos de la implementación IClassFixture. Para obtener más información, vea Implementar un método Dispose.

Ejemplo de pruebas de integración

La aplicación de ejemplo se compone de dos aplicaciones:

Aplicación Directorio del proyecto Descripción
Aplicación de mensajes (el SUT) src/RazorPagesProject Permite a un usuario agregar, analizar mensajes y eliminarlos todos o uno solo.
Probar la aplicación tests/RazorPagesProject.Tests Se usa para probar la integración del SUT.

Las pruebas se pueden ejecutar con las características de prueba integradas de un IDE, como Visual Studio. Si usa Visual Studio Code o la línea de comandos, ejecute el comando siguiente en un símbolo del sistema en el directorio tests/RazorPagesProject.Tests:

dotnet test

Organización de aplicación de mensajes (SUT)

El sistema a prueba es un sistema de mensajes de Razor Pages con las siguientes características:

  • La página Index de la aplicación (Pages/Index.cshtml y Pages/Index.cshtml.cs) proporciona una interfaz de usuario y métodos de modelo de página para controlar la adición, la eliminación y el análisis de mensajes (promedio de palabras por mensaje).
  • La clase Message (Data/Message.cs) describe un mensaje con dos propiedades: Id (clave) y Text (mensaje). Se necesita la propiedad Text, que está limitada a 200 caracteres.
  • Los mensajes se almacenan en la base de datos en memoria de Entity Framework†.
  • La aplicación contiene una capa de acceso a datos (DAL) en su clase de contexto de base de datos, AppDbContext (Data/AppDbContext.cs).
  • Si la base de datos está vacía al inicio de una aplicación, el almacén de mensajes se inicializa con tres mensajes.
  • La aplicación incluye un elemento /SecurePage al que solo puede acceder un usuario autenticado.

†En el tema de EF, Pruebas con InMemory, se explica cómo usar una base de datos en memoria con las pruebas con MSTest. En este tema se usa el marco de pruebas xUnit. Los conceptos y las implementaciones de prueba de diferentes marcos de pruebas son similares, pero no idénticos.

Aunque la aplicación no usa el patrón del repositorio y no es un buen ejemplo del patrón de unidad de trabajo (UoW), Razor Pages admite estos patrones de desarrollo. Para obtener más información, vea Diseño del nivel de persistencia de infraestructura y Lógica del controlador de pruebas (el ejemplo implementa el patrón del repositorio).

Organización de la aplicación de prueba

La aplicación de prueba es una aplicación de consola dentro del directorio tests/RazorPagesProject.Tests.

Directorio de la aplicación de prueba Descripción
AuthTests Contiene métodos de prueba para:
  • Acceder a una página segura mediante un usuario no autenticado.
  • Acceder a una página segura mediante un usuario autenticado con una simulación AuthenticationHandler<TOptions>.
  • Obtener un perfil de usuario de GitHub y comprobar el inicio de sesión de usuario del perfil.
BasicTests Contiene un método de prueba para el enrutamiento y el tipo de contenido.
IntegrationTests Contiene las pruebas de integración de la página Index que usa la clase WebApplicationFactory personalizada.
Helpers/Utilities
  • Utilities.cs contiene el método InitializeDbForTests que se usa para propagar la base de datos con datos de prueba.
  • HtmlHelpers.cs proporciona un método para devolver un elemento IHtmlDocument de AngleSharp para que lo usen los métodos de prueba.
  • HttpClientExtensions.cs proporciona sobrecargas para que SendAsync envíe solicitudes al SUT.

El marco de pruebas es xUnit. Las pruebas de integración se llevan a cabo mediante el elemento Microsoft.AspNetCore.TestHost, que incluye TestServer. Dado que el paquete Microsoft.AspNetCore.Mvc.Testing se usa para configurar el host de prueba y el servidor de pruebas, los paquetes TestHost y TestServer no requieren referencias de paquete directas en el archivo de proyecto ni la configuración de desarrollador de la aplicación de prueba en la aplicación de prueba.

Las pruebas de integración suelen requerir un pequeño conjunto de datos en la base de datos antes de la ejecución de la prueba. Por ejemplo, una prueba de eliminación consiste en la eliminación de un registro de base de datos, por lo que la base de datos debe tener al menos un registro para que la solicitud de eliminación se realice correctamente.

La aplicación de ejemplo propaga la base de datos con tres mensajes en Utilities.cs que las pruebas pueden usar al ejecutarse:

public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}

El contexto de la base de datos del SUT se registra en su método Startup.ConfigureServices. La devolución de llamada builder.ConfigureServices de la aplicación de prueba se ejecuta después de que se ejecute el código Startup.ConfigureServices de la aplicación. Para usar otra base de datos para las pruebas, el contexto de la base de datos de la aplicación debe reemplazarse en builder.ConfigureServices. Para obtener más información, vea la sección Personalización de WebApplicationFactory.

En el caso de sistemas a prueba que todavía usan el host de web, la devolución de llamada builder.ConfigureServices de la aplicación de prueba se ejecuta antes que el código Startup.ConfigureServices del sistema a prueba. La devolución de llamada builder.ConfigureTestServices de la aplicación de prueba se ejecuta después.

Recursos adicionales

En este artículo se da por hecho un conocimiento básico de las pruebas unitarias. Si no está familiarizado con los conceptos de las pruebas, consulte el artículo Pruebas unitarias en .NET Core y .NET Standard y su contenido vinculado.

Vea o descargue el código de ejemplo (cómo descargarlo)

La aplicación de ejemplo es una aplicación de Razor Pages que da por hecho un conocimiento básico de Razor Pages. Si no está familiarizado con Razor Pages, consulte los siguientes artículos:

Para probar SPA, se recomienda una herramienta como Playwright for .NET, que puede automatizar un explorador.

Introducción a las pruebas de integración

Las pruebas de integración evalúan los componentes de una aplicación en un nivel más amplio que las pruebas unitarias. Las pruebas unitarias se usan para probar componentes de software aislados, como métodos de clase individuales. Las pruebas de integración confirman que dos o más componentes de una aplicación funcionan juntos para generar un resultado esperado, lo que posiblemente incluya a todos los componentes necesarios para procesar por completo una solicitud.

Estas pruebas más amplias se usan para probar la infraestructura de la aplicación y todo el marco, lo que a menudo incluye los siguientes componentes:

  • Base de datos
  • Sistema de archivos
  • Dispositivos de red
  • Canalización de solicitud-respuesta

Las pruebas unitarias usan componentes fabricados, conocidos como emulaciones u objetos ficticios, en lugar de componentes de las infraestructura.

A diferencia de las pruebas unitarias, las pruebas de integración:

  • Usan los componentes reales que emplea la aplicación en producción.
  • Necesitan más código y procesamiento de datos.
  • Tardan más en ejecutarse.

Por lo tanto, limite el uso de pruebas de integración a los escenarios de infraestructura más importantes. Si un comportamiento se puede probar mediante una prueba unitaria o una prueba de integración, opte por la prueba unitaria.

En las conversaciones de las pruebas de integración, el proyecto probado suele denominarse Sistema a prueba o "SUT" para abreviar. En este artículo se usa "SUT" para referirse a la aplicación de ASP.NET Core que se va a probar.

No escriba pruebas de integración para cada permutación de datos y acceso a archivos con bases de datos y sistemas de archivos. Independientemente de cuántas ubicaciones de una aplicación interactúen con bases de datos y sistemas de archivos, un conjunto centrado de pruebas de integración de lectura, escritura, actualización y eliminación suele ser capaz de probar adecuadamente los componentes de sistema de archivos y base de datos. Use pruebas unitarias para las pruebas rutinarias de lógica de métodos que interactúan con estos componentes. En las pruebas unitarias, el uso de simulaciones o emulaciones de infraestructura provoca una ejecución más rápida de esas pruebas.

Pruebas de integración de ASP.NET Core

Para las pruebas de integración en ASP.NET Core se necesita lo siguiente:

  • Un proyecto de prueba, que se usa para contener y ejecutar las pruebas. El proyecto de prueba tiene una referencia al SUT.
  • El proyecto de prueba crea un host web de prueba para el SUT y usa un cliente de servidor de pruebas para controlar las solicitudes y las respuestas con el SUT.
  • Se usa un ejecutor de pruebas para ejecutar las pruebas y notificar los resultados de estas.

Las pruebas de integración siguen una secuencia de eventos que incluye los pasos de prueba normales Arrange, Act y Assert:

  1. Se configura el host web del SUT.
  2. Se crea un cliente de servidor de pruebas para enviar solicitudes a la aplicación.
  3. Se ejecuta el paso de prueba Arrange: la aplicación de prueba prepara una solicitud.
  4. Se ejecuta el paso de prueba Act: el cliente envía la solicitud y recibe la respuesta.
  5. Se ejecuta el paso de prueba Assert: la respuesta real se valida como correcta o errónea en función de una respuesta esperada.
  6. El proceso continúa hasta que se ejecutan todas las pruebas.
  7. Se notifican los resultados de la prueba.

Normalmente, el host web de prueba se configura de manera diferente al host web normal de la aplicación para las series de pruebas. Por ejemplo, puede usarse una base de datos diferente u otra configuración de aplicación para las pruebas.

Los componentes de infraestructura, como el host web de prueba y el servidor de pruebas en memoria (TestServer), se proporcionan o se administran mediante el paquete Microsoft.AspNetCore.Mvc.Testing. El uso de este paquete simplifica la creación y ejecución de pruebas.

El paquete Microsoft.AspNetCore.Mvc.Testing controla las siguientes tareas:

  • Copia el archivo de dependencias ( .deps) del SUT en el directorio bin del proyecto de prueba.
  • Establece la raíz de contenido en la raíz de proyecto del SUT de modo que se puedan encontrar archivos estáticos y páginas o vistas cuando se ejecuten las pruebas.
  • Proporciona la clase WebApplicationFactory para simplificar el arranque del SUT con TestServer.

En la documentación de las pruebas unitarias se explica cómo configurar un proyecto de prueba y un ejecutor de pruebas, además de incluirse instrucciones detalladas sobre cómo ejecutar pruebas y recomendaciones sobre cómo asignar nombres a las pruebas y las clases de prueba.

Separe las pruebas unitarias de las pruebas de integración en proyectos diferentes. Separación de las pruebas:

  • Ayuda a garantizar que los componentes de pruebas de infraestructura no se incluyan accidentalmente en las pruebas unitarias.
  • Permite controlar qué conjunto de pruebas se ejecutan.

Prácticamente no hay ninguna diferencia entre la configuración de las pruebas de aplicaciones Razor Pages y MVC. La única diferencia es cómo se asigna nombre a las pruebas. En una aplicación Razor Pages, las pruebas de puntos de conexión de página normalmente se nombran según la clase del modelo de página (por ejemplo, IndexPageTests para probar la integración de los componentes de la página Index). En una aplicación de MVC, las pruebas suelen organizarse por clases de controlador y denominarse según los controladores que prueban (por ejemplo, HomeControllerTests para probar la integración de componentes del controlador Home).

Requisitos previos de la aplicación de prueba

El proyecto de prueba debe:

Estos requisitos previos se pueden observar en la aplicación de ejemplo. Inspeccione el archivo tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. La aplicación de ejemplo usa el marco de pruebas xUnit y la biblioteca de análisis AngleSharp, así que la aplicación de ejemplo también hace referencia a:

En las aplicaciones que usan xunit.runner.visualstudio 2.4.2 o una versión posterior, el proyecto de prueba debe hacer referencia al paquete Microsoft.NET.Test.Sdk.

También se usa Entity Framework Core en las pruebas. Consulte el archivo del proyecto en GitHub.

Entorno del SUT

Si el entorno del SUT no está establecido, el valor predeterminado del entorno es Desarrollo.

Pruebas básicas con el valor predeterminado WebApplicationFactory

Exponga la clase Program definida implícitamente al proyecto de prueba de una de las maneras siguientes:

  • Exponga los tipos internos de la aplicación web al proyecto de prueba. Esto se puede hacer en el archivo del proyecto del SUT (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Haga que la clase Program sea pública mediante una declaración de clase parcial:

    var builder = WebApplication.CreateBuilder(args);
    // ... Configure services, routes, etc.
    app.Run();
    + public partial class Program { }
    

    La aplicación de ejemplo usa el enfoque de clase parcial Program.

WebApplicationFactory<TEntryPoint> se usa para crear un elemento TestServer para pruebas de integración. TEntryPoint es la clase de punto de entrada del SUT, normalmente Program.cs.

Las clases de prueba implementan una interfaz de accesorio de clase (IClassFixture) para indicar que la clase contiene pruebas y proporcionan instancias de objeto compartidas en las pruebas de la clase.

La siguiente clase de prueba, BasicTests, usa WebApplicationFactory para arrancar el SUT y proporcionar un valor HttpClient a un método de prueba, Get_EndpointsReturnSuccessAndCorrectContentType. El método comprueba que el código de estado de la respuesta es correcto (200-299) y que el encabezado Content-Type es text/html; charset=utf-8 para varias páginas de la aplicación.

CreateClient() crea una instancia de HttpClient que sigue automáticamente los redireccionamientos y controla las cookies.

public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

De forma predeterminada, las cookies no esenciales no se conservan entre solicitudes si está habilitada la directiva de consentimiento del Reglamento general de protección de datos. Para conservar las cookies no esenciales, como las usadas por el proveedor TempData, márquelas como esenciales en las pruebas. Para obtener instrucciones sobre cómo marcar una cookie como esencial, consulta Cookies esenciales.

AngleSharp frente a Application Parts para las comprobaciones antifalsificación

En este artículo se usa el analizador AngleSharp para controlar las comprobaciones antifalsificación cargando páginas y analizando el HTML. Para probar los puntos de conexión de las vistas de controlador y Razor Pages en un nivel inferior, sin preocuparse de cómo se representan en el explorador, considere la posibilidad de usar Application Parts. El enfoque de elementos de aplicación inserta un controlador o una instancia de Razor Pages en la aplicación que se puede usar para realizar solicitudes JSON para obtener los valores necesarios. Para obtener más información, consulte el blog sobre pruebas de integración en recursos de ASP.NET Core protegidos con antifalsificación mediante elementos de aplicación y el repositorio de GitHub asociado de Martin Costello.

Personalización de WebApplicationFactory

La configuración de host web se puede crear independientemente de las clases de prueba al heredar de WebApplicationFactory<TEntryPoint> para crear una o más fábricas personalizadas:

  1. Herede de WebApplicationFactory y reemplace ConfigureWebHost. IWebHostBuilder permite la configuración de la colección de servicios con IWebHostBuilder.ConfigureServices

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    

    El método InitializeDbForTests realiza la propagación de la base de datos de la aplicación de ejemplo. El método se describe en la sección Ejemplo de pruebas de integración: organización de la aplicación de prueba.

    El contexto de la base de datos del SUT se registra en Program.cs. La devolución de llamada builder.ConfigureServices de la aplicación de prueba se ejecuta después de que se ejecute el código Program.cs de la aplicación. Para usar una base de datos diferente para las pruebas a la base de datos de la aplicación, el contexto de la base de datos de la aplicación debe reemplazarse en builder.ConfigureServices.

    La aplicación de ejemplo busca el descriptor de servicio del contexto de la base de datos y usa el descriptor para quitar el registro del servicio. Luego, la fábrica agrega una nueva instancia de ApplicationDbContext que usa una base de datos en memoria para las pruebas.

    Para conectarse a otra base de datos, cambie la DbConnection. Para usar una base de datos de prueba de SQL Server:

  1. Use la instancia de CustomWebApplicationFactory personalizada en las clases de prueba. En el ejemplo siguiente se usa la fábrica en la clase IndexPageTests:

    public class IndexPageTests :
        IClassFixture<CustomWebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<Program>
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<Program> factory)
        {
            _factory = factory;
            _client = factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
        }
    

    El cliente de la aplicación de ejemplo está configurado para evitar que HttpClient siga los redireccionamientos. Como se explica más adelante en la sección Autenticación ficticia, esto permite que las pruebas comprueben el resultado de la primera respuesta de la aplicación. La primera respuesta es un redireccionamiento en muchas de estas pruebas con un encabezado Location.

  2. Una prueba típica usa los métodos auxiliares y HttpClient para procesar la solicitud y la respuesta:

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
    

Cualquier solicitud POST al SUT debe satisfacer la comprobación antifalsificación que realiza automáticamente el sistema antifalsificación de protección de datos de la aplicación. Para organizar la solicitud POST de una prueba, la aplicación de prueba debe:

  1. Realizar una solicitud para la página.
  2. Analizar la cookie antifalsificación y solicitar el token de validación de la respuesta.
  3. Realizar la solicitud POST con la cookie antifalsificación y el token de validación de la solicitud aplicados.

Los métodos de extensión auxiliares SendAsync (Helpers/HttpClientExtensions.cs) y el método auxiliar GetDocumentAsync (Helpers/HtmlHelpers.cs) de la aplicación de ejemplo usan el analizador AngleSharp para controlar la comprobación antifalsificación con los métodos siguientes:

  • GetDocumentAsync: recibe HttpResponseMessage y devuelve IHtmlDocument. GetDocumentAsync usa una fábrica que prepara una respuesta virtual basada en la instancia de HttpResponseMessage original. Para obtener más información, vea la documentación de AngleSharp.
  • Métodos de extensión SendAsync para que HttpClient redacte un elemento HttpRequestMessage y llame SendAsync(HttpRequestMessage) a fin de enviar solicitudes al SUT. Las sobrecargas de SendAsync aceptan el formulario HTML (IHtmlFormElement) y lo siguiente:
    • Botón Enviar del formulario (IHtmlElement)
    • Colección de valores del formulario (IEnumerable<KeyValuePair<string, string>>)
    • Botón Enviar (IHtmlElement) y valores del formulario (IEnumerable<KeyValuePair<string, string>>)

AngleSharp es una biblioteca de análisis de terceros que se usa con fines de demostración en este artículo y en la aplicación de ejemplo. AngleSharp no es compatible con las pruebas de integración de aplicaciones ASP.NET Core ni necesario. Se pueden usar otros analizadores, como Html Agility Pack (HAP). Otro enfoque consiste en escribir código para controlar el token de comprobación de solicitudes y la cookie antifalsificación del sistema antifalsificación directamente. Consulte AngleSharp frente a Application Parts para las comprobaciones antifalsificación en este artículo para obtener más información.

El proveedor de base de datos en memoria de EF-Core se puede usar para pruebas básicas y limitadas, pero el proveedor SQLite es la opción que se recomienda para las pruebas en memoria.

Consulte Extensión del inicio con filtros de inicio que muestra cómo configurar el middleware mediante IStartupFilter, que es útil cuando una prueba requiere un servicio personalizado o middleware.

Personalización del cliente con WithWebHostBuilder

Cuando se requiere configuración adicional en un método de prueba, WithWebHostBuilder crea una nueva instancia de WebApplicationFactory con un elemento IWebHostBuilder que se personaliza aún más mediante la configuración.

El código de ejemplo llama a WithWebHostBuilder para reemplazar los servicios configurados por códigos auxiliares de prueba. Para más información y uso de ejemplo, consulte Inserción de servicios ficticios en este artículo.

El método de prueba Post_DeleteMessageHandler_ReturnsRedirectToRoot de la aplicación de ejemplo muestra el uso de WithWebHostBuilder. Esta prueba realiza una eliminación de registros en la base de datos al desencadenar un envío de formulario en el SUT.

Dado que otra prueba en la clase IndexPageTests realiza una operación que elimina todos los registros de la base de datos y puede ejecutarse antes que el método Post_DeleteMessageHandler_ReturnsRedirectToRoot, la base de datos se vuelve a propagar en este método de prueba para asegurarse de que un registro está presente para que lo elimine el SUT. La selección del primer botón de eliminación del formulario messages en el SUT se simula en la solicitud al SUT:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Opciones de cliente

Consulte la página WebApplicationFactoryClientOptions para ver los valores predeterminados y las opciones disponibles al crear instancias de HttpClient.

Cree la clase WebApplicationFactoryClientOptions y pásela al método CreateClient():

public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program>
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

NOTA: Para evitar advertencias de redireccionamiento HTTPS en los registros al usar middleware de redirección HTTPS, establezca BaseAddress = new Uri("https://localhost")

Inserción de servicios ficticios

Los servicios se pueden invalidar en una prueba con una llamada a ConfigureTestServices en el generador de hosts. Para limitar el ámbito de los servicios invalidados a la propia prueba, el método WithWebHostBuilder se usa para recuperar un generador de hosts. Esto se puede ver en las siguientes pruebas:

El SUT de ejemplo incluye un servicio con ámbito que devuelve una cita. La cita se inserta en un campo oculto de la página Index cuando se solicita esta.

Services/IQuoteService.cs:

public interface IQuoteService
{
    Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

Program.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.cs:

public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

    public async Task OnGetAsync()
    {
        Messages = await _db.GetMessagesAsync();

        Quote = await _quoteService.GenerateQuote();
    }

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

El siguiente marcado se genera cuando se ejecuta la aplicación SUT:

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">

Para probar el servicio y la inserción de citas en una prueba de integración, la prueba inserta un servicio ficticio en el SUT. El servicio ficticio reemplaza el elemento QuoteService de la aplicación por un servicio proporcionado por la aplicación de prueba, denominado TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

Se llama a ConfigureTestServices y se registra el servicio con ámbito:

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

El marcado producido durante la ejecución de la prueba refleja el texto de la cita proporcionado por TestQuoteService, por lo que la aserción pasa:

<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">

Autenticación ficticia

Las pruebas de la clase AuthTests comprueban que un punto de conexión seguro:

  • Redirige a un usuario no autenticado a la página de inicio de sesión de la aplicación.
  • Devuelve el contenido de un usuario autenticado.

En el SUT, la página /SecurePage utiliza una convención AuthorizePage para aplicar un elemento AuthorizeFilter a la página. Para más información, consulte las convenciones de autorización de Razor Pages.

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});

En la prueba Get_SecurePageRedirectsAnUnauthenticatedUser, un elemento WebApplicationFactoryClientOptions se establece para no permitir redireccionamientos configurando AllowAutoRedirect en false:

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}

Al no permitir que el cliente siga el redireccionamiento, se pueden realizar las siguientes comprobaciones:

  • El código de estado devuelto por el SUT se puede comprobar con el resultado HttpStatusCode.Redirect esperado, no el código de estado final después del redireccionamiento a la página de inicio de sesión, que sería HttpStatusCode.OK.
  • Se comprueba el valor del encabezado Location en los encabezados de respuesta para confirmar que empieza con http://localhost/Identity/Account/Login, no la respuesta de la página de inicio de sesión final, donde el encabezado Location no estaría presente.

La aplicación de prueba puede simular un elemento AuthenticationHandler<TOptions> en ConfigureTestServices para probar aspectos de autenticación y autorización. Un escenario mínimo devuelve un elemento AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

Se llama a TestAuthHandler para autenticar a un usuario cuando el esquema de autenticación está establecido en TestScheme, donde AddAuthentication está registrado para ConfigureTestServices. Es importante que el esquema de TestScheme coincida con el que espera la aplicación. En caso contrario, la autenticación no funcionará.

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(defaultScheme: "TestScheme")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "TestScheme", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Para obtener más información sobre WebApplicationFactoryClientOptions, vea la sección Opciones de cliente.

Pruebas básicas de middleware de autenticación

Consulte este repositorio de GitHub para ver las pruebas básicas de middleware de autenticación. Contiene un servidor de prueba que es específico del escenario de prueba.

Establecimiento del entorno

Establezca el entorno en el generador de aplicaciones personalizadas:

public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbContextOptions<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}

Cómo deduce la infraestructura de prueba la ruta de acceso raíz del contenido de la aplicación

El constructor WebApplicationFactory deduce la ruta de acceso raíz del contenido de la aplicación buscando un elemento WebApplicationFactoryContentRootAttribute en el ensamblado que contiene las pruebas de integración con una clave igual a TEntryPoint del ensamblado System.Reflection.Assembly.FullName. En caso de que no se encuentre la clave correcta, WebApplicationFactory vuelve a buscar un archivo de solución ( .sln) y anexa el nombre de ensamblado TEntryPoint al directorio de la solución. El directorio raíz de la aplicación (la ruta de acceso raíz del contenido) se usa para detectar los archivos de contenido y las vistas.

Deshabilitación de la creación de instantáneas

La creación de instantáneas hace que las pruebas se ejecuten en un directorio diferente al de salida. Si las pruebas se basan en la carga de archivos relativos a Assembly.Location y detecta algún problema, puede que tenga que deshabilitar la copia de propiedades reemplazadas.

Para deshabilitar dicha copia al usar xUnit, cree un archivo xunit.runner.json en el directorio del proyecto de prueba, con las opción de configuración correcta:

{
  "shadowCopy": false
}

Eliminación de objetos

Una vez ejecutadas las pruebas de la implementación IClassFixture, TestServer y HttpClient se eliminan cuando xUnit quita WebApplicationFactory. Si los objetos cuya instancia ha creado el desarrollador requieren eliminación, quítelos de la implementación IClassFixture. Para obtener más información, vea Implementar un método Dispose.

Ejemplo de pruebas de integración

La aplicación de ejemplo se compone de dos aplicaciones:

Aplicación Directorio del proyecto Descripción
Aplicación de mensajes (el SUT) src/RazorPagesProject Permite a un usuario agregar, analizar mensajes y eliminarlos todos o uno solo.
Probar la aplicación tests/RazorPagesProject.Tests Se usa para probar la integración del SUT.

Las pruebas se pueden ejecutar con las características de prueba integradas de un IDE, como Visual Studio. Si usa Visual Studio Code o la línea de comandos, ejecute el comando siguiente en un símbolo del sistema en el directorio tests/RazorPagesProject.Tests:

dotnet test

Organización de aplicación de mensajes (SUT)

El sistema a prueba es un sistema de mensajes de Razor Pages con las siguientes características:

  • La página Index de la aplicación (Pages/Index.cshtml y Pages/Index.cshtml.cs) proporciona una interfaz de usuario y métodos de modelo de página para controlar la adición, la eliminación y el análisis de mensajes (promedio de palabras por mensaje).
  • La clase Message (Data/Message.cs) describe un mensaje con dos propiedades: Id (clave) y Text (mensaje). Se necesita la propiedad Text, que está limitada a 200 caracteres.
  • Los mensajes se almacenan en la base de datos en memoria de Entity Framework†.
  • La aplicación contiene una capa de acceso a datos (DAL) en su clase de contexto de base de datos, AppDbContext (Data/AppDbContext.cs).
  • Si la base de datos está vacía al inicio de una aplicación, el almacén de mensajes se inicializa con tres mensajes.
  • La aplicación incluye un elemento /SecurePage al que solo puede acceder un usuario autenticado.

†En el artículo de EF, Pruebas con InMemory, se explica cómo usar una base de datos en memoria con las pruebas con MSTest. En este tema se usa el marco de pruebas xUnit. Los conceptos y las implementaciones de prueba de diferentes marcos de pruebas son similares, pero no idénticos.

Aunque la aplicación no usa el patrón del repositorio y no es un buen ejemplo del patrón de unidad de trabajo (UoW), Razor Pages admite estos patrones de desarrollo. Para obtener más información, vea Diseño del nivel de persistencia de infraestructura y Lógica del controlador de pruebas (el ejemplo implementa el patrón del repositorio).

Organización de la aplicación de prueba

La aplicación de prueba es una aplicación de consola dentro del directorio tests/RazorPagesProject.Tests.

Directorio de la aplicación de prueba Descripción
AuthTests Contiene métodos de prueba para:
  • Acceder a una página segura mediante un usuario no autenticado.
  • Acceder a una página segura mediante un usuario autenticado con una simulación AuthenticationHandler<TOptions>.
  • Obtener un perfil de usuario de GitHub y comprobar el inicio de sesión de usuario del perfil.
BasicTests Contiene un método de prueba para el enrutamiento y el tipo de contenido.
IntegrationTests Contiene las pruebas de integración de la página Index que usa la clase WebApplicationFactory personalizada.
Helpers/Utilities
  • Utilities.cs contiene el método InitializeDbForTests que se usa para propagar la base de datos con datos de prueba.
  • HtmlHelpers.cs proporciona un método para devolver un elemento IHtmlDocument de AngleSharp para que lo usen los métodos de prueba.
  • HttpClientExtensions.cs proporciona sobrecargas para que SendAsync envíe solicitudes al SUT.

El marco de pruebas es xUnit. Las pruebas de integración se llevan a cabo mediante el elemento Microsoft.AspNetCore.TestHost, que incluye TestServer. Dado que el paquete Microsoft.AspNetCore.Mvc.Testing se usa para configurar el host de prueba y el servidor de pruebas, los paquetes TestHost y TestServer no requieren referencias de paquete directas en el archivo de proyecto ni la configuración de desarrollador de la aplicación de prueba en la aplicación de prueba.

Las pruebas de integración suelen requerir un pequeño conjunto de datos en la base de datos antes de la ejecución de la prueba. Por ejemplo, una prueba de eliminación consiste en la eliminación de un registro de base de datos, por lo que la base de datos debe tener al menos un registro para que la solicitud de eliminación se realice correctamente.

La aplicación de ejemplo propaga la base de datos con tres mensajes en Utilities.cs que las pruebas pueden usar al ejecutarse:

public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}

El contexto de la base de datos del SUT se registra en Program.cs. La devolución de llamada builder.ConfigureServices de la aplicación de prueba se ejecuta después de que se ejecute el código Program.cs de la aplicación. Para usar otra base de datos para las pruebas, el contexto de la base de datos de la aplicación debe reemplazarse en builder.ConfigureServices. Para obtener más información, vea la sección Personalización de WebApplicationFactory.

Recursos adicionales