Compartir comentarios
Las respuestas se generan en base a la documentación.

Escribe pruebas con Testcontainers

Las pruebas existentes utilizan una base de datos SQLite en memoria. Aunque es conveniente, esto no coincide con el comportamiento de producción. Puedes reemplazarla con una instancia real de Microsoft SQL Server gestionada por Testcontainers.

Agrega dependencias

Cambia al directorio del proyecto de pruebas y agrega el proveedor de Entity Framework para SQL Server y el módulo MSSQL de Testcontainers:

$ cd tests/RazorPagesProject.Tests
$ dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 7.0.0
$ dotnet add package Testcontainers.MsSql --version 3.0.0
Note

Testcontainers para .NET ofrece una gama de módulos que siguen las configuraciones de mejores prácticas.

Crea la clase de prueba

Crea un archivo MsSqlTests.cs en el directorio IntegrationTests. Esta clase gestiona el ciclo de vida del contenedor SQL Server y contiene una clase de prueba anidada.

using System.Data.Common;
using System.Net;
using AngleSharp.Html.Dom;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using RazorPagesProject.Data;
using RazorPagesProject.Tests.Helpers;
using Testcontainers.MsSql;
using Xunit;

namespace RazorPagesProject.Tests.IntegrationTests;

public sealed class MsSqlTests : IAsyncLifetime
{
    private readonly MsSqlContainer _msSqlContainer = new MsSqlBuilder().Build();

    public Task InitializeAsync()
    {
        return _msSqlContainer.StartAsync();
    }

    public Task DisposeAsync()
    {
        return _msSqlContainer.DisposeAsync().AsTask();
    }

    public sealed class IndexPageTests : IClassFixture<MsSqlTests>, IDisposable
    {
        private readonly WebApplicationFactory<Program> _webApplicationFactory;

        private readonly HttpClient _httpClient;

        public IndexPageTests(MsSqlTests fixture)
        {
            var clientOptions = new WebApplicationFactoryClientOptions();
            clientOptions.AllowAutoRedirect = false;

            _webApplicationFactory = new CustomWebApplicationFactory(fixture);
            _httpClient = _webApplicationFactory.CreateClient(clientOptions);
        }

        public void Dispose()
        {
            _webApplicationFactory.Dispose();
        }

        [Fact]
        public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
        {
            // Arrange
            var defaultPage = await _httpClient.GetAsync("/")
                .ConfigureAwait(false);

            var document = await HtmlHelpers.GetDocumentAsync(defaultPage)
                .ConfigureAwait(false);

            // Act
            var form = (IHtmlFormElement)document.QuerySelector("form[id='messages']");
            var submitButton = (IHtmlButtonElement)document.QuerySelector("button[id='deleteAllBtn']");

            var response = await _httpClient.SendAsync(form, submitButton)
                .ConfigureAwait(false);

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

        private sealed class CustomWebApplicationFactory : WebApplicationFactory<Program>
        {
            private readonly string _connectionString;

            public CustomWebApplicationFactory(MsSqlTests fixture)
            {
                _connectionString = fixture._msSqlContainer.GetConnectionString();
            }

            protected override void ConfigureWebHost(IWebHostBuilder builder)
            {
                builder.ConfigureServices(services =>
                {
                    services.Remove(services.SingleOrDefault(service => typeof(DbContextOptions<ApplicationDbContext>) == service.ServiceType));
                    services.Remove(services.SingleOrDefault(service => typeof(DbConnection) == service.ServiceType));
                    services.AddDbContext<ApplicationDbContext>((_, option) => option.UseSqlServer(_connectionString));
                });
            }
        }
    }
}

Entiende la estructura de la prueba

Ciclo de vida del contenedor con IAsyncLifetime

La clase externa MsSqlTests implementa IAsyncLifetime. xUnit llama a InitializeAsync() justo después de crear la instancia de la clase, lo que inicia el contenedor de SQL Server. Después de que se completan todas las pruebas, DisposeAsync() detiene y elimina el contenedor.

private readonly MsSqlContainer _msSqlContainer = new MsSqlBuilder().Build();

MsSqlBuilder().Build() crea un contenedor de Microsoft SQL Server preconfigurado. Los módulos de Testcontainers siguen las mejores prácticas, por lo que no necesitas configurar puertos, contraseñas ni estrategias de espera de inicio por ti mismo.

Clase de prueba anidada con IClassFixture

La clase IndexPageTests está anidada dentro de MsSqlTests e implementa IClassFixture<MsSqlTests>. Esto le da a la clase de prueba acceso al campo privado del contenedor y crea una jerarquía limpia en el explorador de pruebas.

Custom WebApplicationFactory

En lugar de utilizar la fábrica basada en SQLite, la CustomWebApplicationFactory anidada recupera la cadena de conexión del contenedor SQL Server en ejecución y la pasa a UseSqlServer():

private sealed class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
    private readonly string _connectionString;

    public CustomWebApplicationFactory(MsSqlTests fixture)
    {
        _connectionString = fixture._msSqlContainer.GetConnectionString();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            services.Remove(services.SingleOrDefault(service => typeof(DbContextOptions<ApplicationDbContext>) == service.ServiceType));
            services.Remove(services.SingleOrDefault(service => typeof(DbConnection) == service.ServiceType));
            services.AddDbContext<ApplicationDbContext>((_, option) => option.UseSqlServer(_connectionString));
        });
    }
}

Esta fábrica:

  1. Elimina el registro existente de DbContextOptions<ApplicationDbContext>
  2. Elimina el registro existente de DbConnection
  3. Agrega un nuevo ApplicationDbContext configurado con la cadena de conexión de SQL Server del contenedor gestionado por Testcontainers
Note

La imagen Docker de Microsoft SQL Server no es compatible con dispositivos ARM, como los Mac con Apple Silicon. Puedes usar el módulo SqlEdge o Testcontainers Cloud como alternativas.