Pruebas unitarias de Razor Pages en ASP.NET Core

ASP.NET Core admite pruebas unitarias de aplicaciones de Razor Pages. Las pruebas de la capa de acceso a datos (DAL) y de los modelos de página ayudan a garantizar lo siguiente:

  • Las partes de una aplicación de Razor Pages funcionan de forma independiente y conjunta como una unidad durante la creación de una aplicación.
  • Las clases y los métodos tienen ámbitos de responsabilidad restringidos.
  • Hay documentación adicional disponible sobre cómo debe comportarse la aplicación.
  • Se detectan regresiones —que son errores que salen a la luz a raíz de las actualizaciones de código— en los procesos de compilación e implementación automatizados.

En este tema se da por hecho que el usuario posee conocimientos básicos de las pruebas unitarias y las aplicaciones de Razor Pages. Si no está familiarizado con los conceptos de prueba o aplicaciones de Razor Pages, vea los siguientes temas:

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

El proyecto de ejemplo se compone de dos aplicaciones:

Aplicación Carpeta del proyecto Descripción
Aplicación de mensajes src/RazorPagesTestSample Permite a un usuario agregar un mensaje, eliminar un mensaje, eliminar todos los mensajes y analizar mensajes (hallar la media de palabras por mensaje).
Probar la aplicación tests/RazorPagesTestSample.Tests Sirve para realizar una prueba unitaria del modelo de página Index y la DAL de la aplicación de mensajes.

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 siguiente comando en un símbolo del sistema en la carpeta tests/RazorPagesTestSample.Tests:

dotnet test

Organización de la aplicación de mensajes

La aplicación de mensajes 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, eliminación y análisis de mensajes (hallar la media 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 DAL en su clase de contexto de base de datos, AppDbContext (Data/AppDbContext.cs). Los métodos de DAL se marcan como virtual, lo que permite realizar simulaciones en ellos para usarlos en las pruebas.
  • Si la base de datos está vacía al inicio de una aplicación, el almacén de mensajes se inicializa con tres mensajes. Estos mensajes inicializados también se usan en las pruebas.

†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 de ejemplo no usa el patrón del repositorio y no es un ejemplo eficaz del patrón Unit of Work (UoW), Razor Pages admite estos patrones de desarrollo. Para más información, vea Diseño del nivel de persistencia de infraestructura y Lógica del controlador de pruebas en ASP.NET Core (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 de la carpeta tests/RazorPagesTestSample.Tests.

Carpeta de aplicación de prueba Descripción
UnitTests
  • DataAccessLayerTest.cs contiene las pruebas unitarias de la DAL.
  • IndexPageTests.cs contiene las pruebas unitarias del modelo de página Index.
Utilities Contiene el método TestDbContextOptions empleado para crear nuevas opciones de contexto de base de datos para cada prueba unitaria de DAL, de modo que la base de datos se restablezca a su condición de línea base en cada prueba.

El marco de pruebas es xUnit. El marco de trabajo de simulación de objetos es Moq.

Pruebas unitarias de la capa de acceso a datos (DAL)

La aplicación de mensajes tiene una DAL con cuatro métodos contenidos en la clase AppDbContext (src/RazorPagesTestSample/Data/AppDbContext.cs). Cada método tiene una o dos pruebas unitarias en la aplicación de prueba.

Método de DAL Función
GetMessagesAsync Obtiene un elemento List<Message> de la base de datos ordenada por la propiedad Text.
AddMessageAsync Agrega un elemento Message a la base de datos.
DeleteAllMessagesAsync Elimina todas las entradas Message de la base de datos.
DeleteMessageAsync Elimina todas las entradas Message de la base de datos según Id.

Las pruebas unitarias de la DAL requieren DbContextOptions al crear un elemento AppDbContext para cada prueba. Un método para crear el elemento DbContextOptions de cada prueba es usar un elemento DbContextOptionsBuilder:

var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
    .UseInMemoryDatabase("InMemoryDb");

using (var db = new AppDbContext(optionsBuilder.Options))
{
    // Use the db here in the unit test.
}

El inconveniente de este método es que cada prueba recibe la base de datos en el estado en el que la dejó la prueba anterior. Esto puede ser problemático al intentar escribir pruebas unitarias atómicas que no interfieran entre sí. Para forzar que AppDbContext use un nuevo contexto de base de datos en cada prueba, proporcione una instancia de DbContextOptions que esté basada en un nuevo proveedor de servicios. La aplicación de prueba muestra cómo llevar esto a cabo con el método TestDbContextOptions de su clase Utilities (tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs):

public static DbContextOptions<AppDbContext> TestDbContextOptions()
{
    // Create a new service provider to create a new in-memory database.
    var serviceProvider = new ServiceCollection()
        .AddEntityFrameworkInMemoryDatabase()
        .BuildServiceProvider();

    // Create a new options instance using an in-memory database and 
    // IServiceProvider that the context should resolve all of its 
    // services from.
    var builder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb")
        .UseInternalServiceProvider(serviceProvider);

    return builder.Options;
}

El uso de DbContextOptions en las pruebas unitarias de DAL permite que cada prueba se ejecute de forma atómica con una instancia de base de datos completamente nueva:

using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
    // Use the db here in the unit test.
}

Cada método de prueba de la clase DataAccessLayerTest (UnitTests/DataAccessLayerTest.cs) sigue un patrón Organización-Acción-Aserción similar:

  1. Organización: la base de datos se configura para la prueba y/o el resultado previsto se define.
  2. Acción: la prueba se ejecuta.
  3. Aserción: se realizan aserciones para determinar si el resultado de la prueba es correcto.

Por ejemplo, el método DeleteMessageAsync es responsable de quitar un mensaje individual identificado por su Id (src/RazorPagesTestSample/Data/AppDbContext.cs):

public async virtual Task DeleteMessageAsync(int id)
{
    var message = await Messages.FindAsync(id);

    if (message != null)
    {
        Messages.Remove(message);
        await SaveChangesAsync();
    }
}

Hay dos pruebas para este método: una comprueba que el método elimina un mensaje cuando el mensaje está presente en la base de datos y la otra, que la base de datos no cambia si el Id del mensaje que se quiere eliminar no existe. Aquí mostramos el método DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound:

[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var seedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(seedMessages);
        await db.SaveChangesAsync();
        var recId = 1;
        var expectedMessages = 
            seedMessages.Where(message => message.Id != recId).ToList();

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

En primer lugar, el método lleva a cabo el paso de organización, donde se prepara todo para el paso de acción. Los mensajes de inicialización se obtienen y se conservan en seedMessages. Los mensajes de inicialización se guardan en la base de datos. El mensaje con un Id de 1 se establece para eliminarse. Cuando el método DeleteMessageAsync se ejecuta, los mensajes esperados deben incluir todos los mensajes, excepto el que tenga un Id de 1. La variable expectedMessages representa el resultado esperado.

// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages = 
    seedMessages.Where(message => message.Id != recId).ToList();

El método actúa: El método DeleteMessageAsync se ejecuta pasando un recId de 1:

// Act
await db.DeleteMessageAsync(recId);

Por último, el método obtiene el elemento Messages del contexto y lo compara con el elemento expectedMessages, afirmando que los dos son iguales:

// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

Para comparar que los dos elementos List<Message> son iguales:

  • Los mensajes se ordenan por Id.
  • Los pares de mensajes se comparan en la propiedad Text.

Un método de prueba similar, DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound, comprueba el resultado de intentar eliminar un mensaje que no existe. En este caso, los mensajes esperados en la base de datos deben ser iguales a los mensajes reales después de que el método DeleteMessageAsync se ejecute. No debe haber ningún cambio en el contenido de la base de datos:

[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var expectedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(expectedMessages);
        await db.SaveChangesAsync();
        var recId = 4;

        // Act
        try
        {
            await db.DeleteMessageAsync(recId);
        }
        catch
        {
            // recId doesn't exist
        }

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

Pruebas unitarias de los métodos del modelo de página

Otro conjunto de pruebas unitarias es responsable de las pruebas de los métodos del modelo de página. En la aplicación de mensajes, los modelos de página Index se encuentran en la clase IndexModel en src/RazorPagesTestSample/Pages/Index.cshtml.cs.

Método del modelo de página Función
OnGetAsync Obtiene los mensajes de la DAL de la interfaz de usuario mediante el método GetMessagesAsync.
OnPostAddMessageAsync Si ModelState es válido, se llama a AddMessageAsync para agregar un mensaje a la base de datos.
OnPostDeleteAllMessagesAsync Llama a DeleteAllMessagesAsync para eliminar todos los mensajes de la base de datos.
OnPostDeleteMessageAsync Ejecuta DeleteMessageAsync para eliminar un mensaje con el Id especificado.
OnPostAnalyzeMessagesAsync Si hay uno o más mensajes en la base de datos, calcula la media de palabras por mensaje.

Los métodos del modelo de página se comprueban con siete pruebas en la clase IndexPageTests (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs). Las pruebas usan el conocido patrón Organización-Aserción-Acción. Estas pruebas se centran en lo siguiente:

  • Determinar si los métodos siguen el comportamiento correcto cuando ModelState no es válido.
  • Confirmar que los métodos generan el elemento IActionResult correcto.
  • Comprobar que las asignaciones de valores de propiedad se realizan correctamente.

Este grupo de pruebas suele simular los métodos de DAL para generar los datos esperados en el paso de acción, en el que un método de modelo de página se ejecuta. Por ejemplo, el método GetMessagesAsync de AppDbContext se simula para generar una salida. Cuando un método de modelo de página ejecuta este método, la simulación devuelve el resultado. Los datos no provienen de la base de datos. Esto crea condiciones de prueba de confianza y confiables para usar la DAL en las pruebas del modelo de página.

La prueba OnGetAsync_PopulatesThePageModel_WithAListOfMessages muestra cómo se simula el método GetMessagesAsync para el modelo de página:

var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(
    db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var pageModel = new IndexModel(mockAppDbContext.Object);

Cuando el método OnGetAsync se ejecuta en el paso de acción, llama al método GetMessagesAsync del modelo de página.

Paso de acción de prueba unitaria (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs):

// Act
await pageModel.OnGetAsync();

Método OnGetAsync del modelo de página IndexPage (src/RazorPagesTestSample/Pages/Index.cshtml.cs):

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

El método GetMessagesAsync de la DAL no devuelve el resultado de esta llamada al método. La versión simulada del método devuelve el resultado.

En el paso Assert, los mensajes reales (actualMessages) se asignan desde la propiedad Messages del modelo de página. También se realiza una comprobación de tipo cuando los mensajes se asignan. Las propiedades Text de los mensajes esperados y reales se comparan. La prueba afirma que las dos instancias de List<Message> contienen los mismos mensajes.

// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

Otras pruebas de este grupo crean objetos de modelo de página que incluyen DefaultHttpContext, ModelStateDictionary, un elemento ActionContext para establecer PageContext, un elemento ViewDataDictionary y un elemento PageContext. Todos ellos resultan útiles para realizar pruebas. Por ejemplo, la aplicación de mensajes establece un error de ModelState con AddModelError para comprobar si se devuelve un elemento PageResult válido cuando OnPostAddMessageAsync se ejecuta:

[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
    // Arrange
    var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb");
    var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
    var expectedMessages = AppDbContext.GetSeedingMessages();
    mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
    var httpContext = new DefaultHttpContext();
    var modelState = new ModelStateDictionary();
    var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
    var modelMetadataProvider = new EmptyModelMetadataProvider();
    var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
    var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
    var pageContext = new PageContext(actionContext)
    {
        ViewData = viewData
    };
    var pageModel = new IndexModel(mockAppDbContext.Object)
    {
        PageContext = pageContext,
        TempData = tempData,
        Url = new UrlHelper(actionContext)
    };
    pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");

    // Act
    var result = await pageModel.OnPostAddMessageAsync();

    // Assert
    Assert.IsType<PageResult>(result);
}

Recursos adicionales

ASP.NET Core admite pruebas unitarias de aplicaciones de Razor Pages. Las pruebas de la capa de acceso a datos (DAL) y de los modelos de página ayudan a garantizar lo siguiente:

  • Las partes de una aplicación de Razor Pages funcionan de forma independiente y conjunta como una unidad durante la creación de una aplicación.
  • Las clases y los métodos tienen ámbitos de responsabilidad restringidos.
  • Hay documentación adicional disponible sobre cómo debe comportarse la aplicación.
  • Se detectan regresiones —que son errores que salen a la luz a raíz de las actualizaciones de código— en los procesos de compilación e implementación automatizados.

En este tema se da por hecho que el usuario posee conocimientos básicos de las pruebas unitarias y las aplicaciones de Razor Pages. Si no está familiarizado con los conceptos de prueba o aplicaciones de Razor Pages, vea los siguientes temas:

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

El proyecto de ejemplo se compone de dos aplicaciones:

Aplicación Carpeta del proyecto Descripción
Aplicación de mensajes src/RazorPagesTestSample Permite a un usuario agregar un mensaje, eliminar un mensaje, eliminar todos los mensajes y analizar mensajes (hallar la media de palabras por mensaje).
Probar la aplicación tests/RazorPagesTestSample.Tests Sirve para realizar una prueba unitaria del modelo de página Index y la DAL de la aplicación de mensajes.

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 siguiente comando en un símbolo del sistema en la carpeta tests/RazorPagesTestSample.Tests:

dotnet test

Organización de la aplicación de mensajes

La aplicación de mensajes 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, eliminación y análisis de mensajes (hallar la media 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 DAL en su clase de contexto de base de datos, AppDbContext (Data/AppDbContext.cs). Los métodos de DAL se marcan como virtual, lo que permite realizar simulaciones en ellos para usarlos en las pruebas.
  • Si la base de datos está vacía al inicio de una aplicación, el almacén de mensajes se inicializa con tres mensajes. Estos mensajes inicializados también se usan en las pruebas.

†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 de ejemplo no usa el patrón del repositorio y no es un ejemplo eficaz del patrón Unit of Work (UoW), Razor Pages admite estos patrones de desarrollo. Para más información, vea Diseño del nivel de persistencia de infraestructura y Lógica del controlador de pruebas en ASP.NET Core (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 de la carpeta tests/RazorPagesTestSample.Tests.

Carpeta de aplicación de prueba Descripción
UnitTests
  • DataAccessLayerTest.cs contiene las pruebas unitarias de la DAL.
  • IndexPageTests.cs contiene las pruebas unitarias del modelo de página Index.
Utilities Contiene el método TestDbContextOptions empleado para crear nuevas opciones de contexto de base de datos para cada prueba unitaria de DAL, de modo que la base de datos se restablezca a su condición de línea base en cada prueba.

El marco de pruebas es xUnit. El marco de trabajo de simulación de objetos es Moq.

Pruebas unitarias de la capa de acceso a datos (DAL)

La aplicación de mensajes tiene una DAL con cuatro métodos contenidos en la clase AppDbContext (src/RazorPagesTestSample/Data/AppDbContext.cs). Cada método tiene una o dos pruebas unitarias en la aplicación de prueba.

Método de DAL Función
GetMessagesAsync Obtiene un elemento List<Message> de la base de datos ordenada por la propiedad Text.
AddMessageAsync Agrega un elemento Message a la base de datos.
DeleteAllMessagesAsync Elimina todas las entradas Message de la base de datos.
DeleteMessageAsync Elimina todas las entradas Message de la base de datos según Id.

Las pruebas unitarias de la DAL requieren DbContextOptions al crear un elemento AppDbContext para cada prueba. Un método para crear el elemento DbContextOptions de cada prueba es usar un elemento DbContextOptionsBuilder:

var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
    .UseInMemoryDatabase("InMemoryDb");

using (var db = new AppDbContext(optionsBuilder.Options))
{
    // Use the db here in the unit test.
}

El inconveniente de este método es que cada prueba recibe la base de datos en el estado en el que la dejó la prueba anterior. Esto puede ser problemático al intentar escribir pruebas unitarias atómicas que no interfieran entre sí. Para forzar que AppDbContext use un nuevo contexto de base de datos en cada prueba, proporcione una instancia de DbContextOptions que esté basada en un nuevo proveedor de servicios. La aplicación de prueba muestra cómo llevar esto a cabo con el método TestDbContextOptions de su clase Utilities (tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs):

public static DbContextOptions<AppDbContext> TestDbContextOptions()
{
    // Create a new service provider to create a new in-memory database.
    var serviceProvider = new ServiceCollection()
        .AddEntityFrameworkInMemoryDatabase()
        .BuildServiceProvider();

    // Create a new options instance using an in-memory database and 
    // IServiceProvider that the context should resolve all of its 
    // services from.
    var builder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb")
        .UseInternalServiceProvider(serviceProvider);

    return builder.Options;
}

El uso de DbContextOptions en las pruebas unitarias de DAL permite que cada prueba se ejecute de forma atómica con una instancia de base de datos completamente nueva:

using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
    // Use the db here in the unit test.
}

Cada método de prueba de la clase DataAccessLayerTest (UnitTests/DataAccessLayerTest.cs) sigue un patrón Organización-Acción-Aserción similar:

  1. Organización: la base de datos se configura para la prueba y/o el resultado previsto se define.
  2. Acción: la prueba se ejecuta.
  3. Aserción: se realizan aserciones para determinar si el resultado de la prueba es correcto.

Por ejemplo, el método DeleteMessageAsync es responsable de quitar un mensaje individual identificado por su Id (src/RazorPagesTestSample/Data/AppDbContext.cs):

public async virtual Task DeleteMessageAsync(int id)
{
    var message = await Messages.FindAsync(id);

    if (message != null)
    {
        Messages.Remove(message);
        await SaveChangesAsync();
    }
}

Hay dos pruebas para este método: una comprueba que el método elimina un mensaje cuando el mensaje está presente en la base de datos y la otra, que la base de datos no cambia si el Id del mensaje que se quiere eliminar no existe. Aquí mostramos el método DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound:

[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var seedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(seedMessages);
        await db.SaveChangesAsync();
        var recId = 1;
        var expectedMessages = 
            seedMessages.Where(message => message.Id != recId).ToList();

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

En primer lugar, el método lleva a cabo el paso de organización, donde se prepara todo para el paso de acción. Los mensajes de inicialización se obtienen y se conservan en seedMessages. Los mensajes de inicialización se guardan en la base de datos. El mensaje con un Id de 1 se establece para eliminarse. Cuando el método DeleteMessageAsync se ejecuta, los mensajes esperados deben incluir todos los mensajes, excepto el que tenga un Id de 1. La variable expectedMessages representa el resultado esperado.

// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages = 
    seedMessages.Where(message => message.Id != recId).ToList();

El método actúa: El método DeleteMessageAsync se ejecuta pasando un recId de 1:

// Act
await db.DeleteMessageAsync(recId);

Por último, el método obtiene el elemento Messages del contexto y lo compara con el elemento expectedMessages, afirmando que los dos son iguales:

// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

Para comparar que los dos elementos List<Message> son iguales:

  • Los mensajes se ordenan por Id.
  • Los pares de mensajes se comparan en la propiedad Text.

Un método de prueba similar, DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound, comprueba el resultado de intentar eliminar un mensaje que no existe. En este caso, los mensajes esperados en la base de datos deben ser iguales a los mensajes reales después de que el método DeleteMessageAsync se ejecute. No debe haber ningún cambio en el contenido de la base de datos:

[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var expectedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(expectedMessages);
        await db.SaveChangesAsync();
        var recId = 4;

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

Pruebas unitarias de los métodos del modelo de página

Otro conjunto de pruebas unitarias es responsable de las pruebas de los métodos del modelo de página. En la aplicación de mensajes, los modelos de página Index se encuentran en la clase IndexModel en src/RazorPagesTestSample/Pages/Index.cshtml.cs.

Método del modelo de página Función
OnGetAsync Obtiene los mensajes de la DAL de la interfaz de usuario mediante el método GetMessagesAsync.
OnPostAddMessageAsync Si ModelState es válido, se llama a AddMessageAsync para agregar un mensaje a la base de datos.
OnPostDeleteAllMessagesAsync Llama a DeleteAllMessagesAsync para eliminar todos los mensajes de la base de datos.
OnPostDeleteMessageAsync Ejecuta DeleteMessageAsync para eliminar un mensaje con el Id especificado.
OnPostAnalyzeMessagesAsync Si hay uno o más mensajes en la base de datos, calcula la media de palabras por mensaje.

Los métodos del modelo de página se comprueban con siete pruebas en la clase IndexPageTests (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs). Las pruebas usan el conocido patrón Organización-Aserción-Acción. Estas pruebas se centran en lo siguiente:

  • Determinar si los métodos siguen el comportamiento correcto cuando ModelState no es válido.
  • Confirmar que los métodos generan el elemento IActionResult correcto.
  • Comprobar que las asignaciones de valores de propiedad se realizan correctamente.

Este grupo de pruebas suele simular los métodos de DAL para generar los datos esperados en el paso de acción, en el que un método de modelo de página se ejecuta. Por ejemplo, el método GetMessagesAsync de AppDbContext se simula para generar una salida. Cuando un método de modelo de página ejecuta este método, la simulación devuelve el resultado. Los datos no provienen de la base de datos. Esto crea condiciones de prueba de confianza y confiables para usar la DAL en las pruebas del modelo de página.

La prueba OnGetAsync_PopulatesThePageModel_WithAListOfMessages muestra cómo se simula el método GetMessagesAsync para el modelo de página:

var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(
    db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var pageModel = new IndexModel(mockAppDbContext.Object);

Cuando el método OnGetAsync se ejecuta en el paso de acción, llama al método GetMessagesAsync del modelo de página.

Paso de acción de prueba unitaria (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs):

// Act
await pageModel.OnGetAsync();

Método OnGetAsync del modelo de página IndexPage (src/RazorPagesTestSample/Pages/Index.cshtml.cs):

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

El método GetMessagesAsync de la DAL no devuelve el resultado de esta llamada al método. La versión simulada del método devuelve el resultado.

En el paso Assert, los mensajes reales (actualMessages) se asignan desde la propiedad Messages del modelo de página. También se realiza una comprobación de tipo cuando los mensajes se asignan. Las propiedades Text de los mensajes esperados y reales se comparan. La prueba afirma que las dos instancias de List<Message> contienen los mismos mensajes.

// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

Otras pruebas de este grupo crean objetos de modelo de página que incluyen DefaultHttpContext, ModelStateDictionary, un elemento ActionContext para establecer PageContext, un elemento ViewDataDictionary y un elemento PageContext. Todos ellos resultan útiles para realizar pruebas. Por ejemplo, la aplicación de mensajes establece un error de ModelState con AddModelError para comprobar si se devuelve un elemento PageResult válido cuando OnPostAddMessageAsync se ejecuta:

[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
    // Arrange
    var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb");
    var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
    var expectedMessages = AppDbContext.GetSeedingMessages();
    mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
    var httpContext = new DefaultHttpContext();
    var modelState = new ModelStateDictionary();
    var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
    var modelMetadataProvider = new EmptyModelMetadataProvider();
    var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
    var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
    var pageContext = new PageContext(actionContext)
    {
        ViewData = viewData
    };
    var pageModel = new IndexModel(mockAppDbContext.Object)
    {
        PageContext = pageContext,
        TempData = tempData,
        Url = new UrlHelper(actionContext)
    };
    pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");

    // Act
    var result = await pageModel.OnPostAddMessageAsync();

    // Assert
    Assert.IsType<PageResult>(result);
}

Recursos adicionales