Pruebas unitarias y de integración de las aplicaciones de API mínimas

Por Fiyaz Bin Hasan y Rick Anderson

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.

El código de ejemplo de GitHub proporciona un ejemplo de pruebas unitarias y de integración en una aplicación de API mínima.

Tipos de implementación de IResult

Los tipos de implementación IResult públicos del espacio de nombres Microsoft.AspNetCore.Http.HttpResults pueden usarse para efectuar pruebas unitarias de controladores de ruta mínimos al usar métodos con nombre en lugar de lambdas.

El siguiente código usa la clase NotFound<TValue>:

[Fact]
public async Task GetTodoReturnsNotFoundIfNotExists()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    // Act
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert
    Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

    var notFoundResult = (NotFound) result.Result;

    Assert.NotNull(notFoundResult);
}

El siguiente código usa la clase Ok<TValue>:

[Fact]
public async Task GetTodoReturnsTodoFromDatabase()
{
    // Arrange
    await using var context = new MockDb().CreateDbContext();

    context.Todos.Add(new Todo
    {
        Id = 1,
        Title = "Test title",
        Description = "Test description",
        IsDone = false
    });

    await context.SaveChangesAsync();

    // Act
    var result = await TodoEndpointsV1.GetTodo(1, context);

    //Assert
    Assert.IsType<Results<Ok<Todo>, NotFound>>(result);

    var okResult = (Ok<Todo>)result.Result;

    Assert.NotNull(okResult.Value);
    Assert.Equal(1, okResult.Value.Id);
}

Recursos adicionales