La lección anterior ensambló la API completa de BiblioTech sobre el dominio, la persistencia y la inyección de dependencias ya construidos. Antes de desplegar nada (Lección 5), hace falta verificar que esa API funciona de verdad de principio a fin, no solo que cada pieza suelta pasaba sus pruebas unitarias por separado. Esta lección amplía la batería de pruebas del Módulo 8 con pruebas de integración sobre los endpoints, repasa las técnicas de depuración más útiles en Visual Studio y VS Code, introduce el registro de eventos (logging) con ILogger para diagnosticar problemas ya en producción, y cierra con un checklist de calidad concreto que debe superarse antes de avanzar a la Lección 5.

Contenido

  1. Pruebas unitarias frente a pruebas de integración
  2. Pruebas de integración con WebApplicationFactory
  3. Depuración en Visual Studio y VS Code: breakpoints y watch
  4. Depuración de código asíncrono
  5. Logging básico con ILogger
  6. Checklist de calidad antes de desplegar

  1. Pruebas unitarias frente a pruebas de integración

Las pruebas unitarias del Módulo 8 (Lección 4) verificaban una unidad aislada de código —un método de Prestamo, o Biblioteca.PrestarLibroAsync con un mock de IRepositorioBiblioteca— sin necesitar nada externo. Una prueba de integración verifica, en cambio, que varias piezas reales trabajando juntas producen el resultado esperado: en el caso de BiblioTech, que una petición HTTP real, a través del enrutamiento real de ASP.NET Core, con el contenedor de DI real resolviendo las dependencias reales, produce la respuesta correcta.

Prueba unitaria (Módulo 8) Prueba de integración (esta lección)
Qué verifica Un método aislado Varios componentes reales trabajando juntos
Dependencias Sustituidas por mocks (IRepositorioBiblioteca con Moq) Reales (o una versión de prueba controlada de la base de datos)
Velocidad Milisegundos Más lenta (arranca parte de la aplicación real)
Qué detecta Errores de lógica dentro de una unidad Errores de "cableado": rutas mal registradas, DI mal configurada, serialización JSON incorrecta

Ninguna sustituye a la otra: las pruebas unitarias siguen siendo la base más rápida y numerosa (Módulo 8), y las pruebas de integración añaden una capa de confianza distinta, más cercana a cómo un cliente real usará la API.

  1. Pruebas de integración con WebApplicationFactory

ASP.NET Core ofrece WebApplicationFactory<TEntryPoint>, una clase pensada para arrancar la aplicación completa en memoria, sin necesitar un puerto de red real ni un despliegue, y obtener un HttpClient (el mismo tipo ya usado en el Módulo 5) que dirige sus peticiones directamente a esa aplicación en memoria:

dotnet add BiblioTech.Pruebas package Microsoft.AspNetCore.Mvc.Testing
using Microsoft.AspNetCore.Mvc.Testing;

public class ApiPruebasIntegracion : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _cliente;

    public ApiPruebasIntegracion(WebApplicationFactory<Program> fabrica)
    {
        _cliente = fabrica.CreateClient(); // HttpClient apuntando a la app en memoria
    }

    [Fact]
    public async Task GetLibros_DevuelveCodigo200()
    {
        // Act
        HttpResponseMessage respuesta = await _cliente.GetAsync("/libros");

        // Assert
        respuesta.EnsureSuccessStatusCode(); // lanza si el codigo no es 2xx
    }

    [Fact]
    public async Task PostPrestamos_ConLibroNoExistente_Devuelve400()
    {
        // Arrange
        SolicitudPrestamo solicitud = new SolicitudPrestamo("000-00-000-0000-0", 1);

        // Act
        HttpResponseMessage respuesta = await _cliente.PostAsJsonAsync("/prestamos", solicitud);

        // Assert
        Assert.Equal(HttpStatusCode.BadRequest, respuesta.StatusCode);
    }
}

IClassFixture<WebApplicationFactory<Program>> le dice a xUnit que comparta una única instancia de la aplicación en memoria entre todas las pruebas de esta clase, en vez de arrancarla de nuevo en cada método (mucho más rápido). PostAsJsonAsync serializa solicitud a JSON automáticamente con System.Text.Json (Módulo 5), exactamente igual que haría un cliente real contra la API desplegada. Estas pruebas complementan, sin sustituir, las pruebas unitarias de Moq de 08-04: verifican que el endpoint HTTP realmente enruta, deserializa y responde como se espera, algo que un mock de IRepositorioBiblioteca no puede comprobar por sí solo.

  1. Depuración en Visual Studio y VS Code: breakpoints y watch

Cuando una prueba falla o el comportamiento observado no coincide con el esperado, la depuración permite detener la ejecución en un punto concreto e inspeccionar el estado del programa en ese instante, en vez de adivinar qué ocurre a partir de mensajes de consola:

Herramienta Qué hace Cómo se usa
Breakpoint (punto de interrupción) Detiene la ejecución justo antes de ejecutar una línea concreta Clic en el margen izquierdo del editor, junto al número de línea
Ventana Watch (inspección) Muestra el valor actual de una variable o expresión mientras el programa está detenido Escribir la expresión (prestamo.FechaDevolucion, catalogo.Count) en el panel de Watch
Step Over / Step Into / Step Out Avanza la ejecución línea a línea, entrando o no en las llamadas a otros métodos Teclas de función (F10/F11/Mayús+F11 en Visual Studio; equivalentes en VS Code)
Pila de llamadas (Call Stack) Muestra la cadena completa de métodos que llevó hasta el punto actual Panel dedicado, visible automáticamente al detenerse en un breakpoint

Un flujo típico de depuración sobre POST /prestamos/{id}/devolucion (09-03, apartado 6): colocar un breakpoint en la línea prestamo.RegistrarDevolucion();, lanzar la API en modo depuración (F5 en Visual Studio o VS Code), hacer la petición real con Swagger o curl, y cuando la ejecución se detenga, inspeccionar en el Watch el valor de prestamo.FechaPrestamo y politica.CalcularSancion(...) antes de que se ejecuten, para confirmar que los datos que llegan son los esperados.

  1. Depuración de código asíncrono

Depurar métodos async/await (Módulo 4) tiene una particularidad: al hacer Step Into sobre un await, la ejecución puede "saltar" de forma que parezca que el hilo cambia, porque efectivamente puede reanudarse en un hilo distinto tras la espera (Módulo 6, multihilo). Algunas prácticas ayudan a que esto no confunda:

  • Colocar el breakpoint después del await (por ejemplo, justo donde se usa el resultado de ObtenerMetadatosPorIsbnAsync) para inspeccionar el valor ya resuelto, en vez de intentar seguir la espera misma paso a paso.
  • Revisar la pila de llamadas con atención tras un await: puede mostrarse más corta de lo esperado, porque parte de la pila "anterior" a la espera ya no existe de la misma forma una vez que el método se reanuda.
  • Para el Task.Delay(1000) de PrestarLibroAsync (08-04), un breakpoint justo después de esa línea permite confirmar que la ejecución efectivamente esperó, sin necesidad de depurar la espera en sí misma paso a paso.

  1. Logging básico con ILogger

Los breakpoints son la herramienta adecuada mientras se desarrolla y se puede reproducir el problema localmente. En producción (Lección 5), no hay forma de detener la ejecución con un breakpoint: la herramienta equivalente es el logging, mensajes que el programa escribe de forma continua describiendo lo que va haciendo, para poder revisarlos después si algo falla. ASP.NET Core inyecta automáticamente un ILogger<T> en cualquier clase que lo declare por constructor (08-03, inyección por constructor):

app.MapPost("/prestamos/{id}/devolucion", (
    int id, Biblioteca biblioteca, IPoliticaSancion politica, ILogger<Program> logger) =>
{
    Prestamo? prestamo = biblioteca.Prestamos.ElementAtOrDefault(id);

    if (prestamo is null)
    {
        logger.LogWarning("Intento de devolucion de un prestamo inexistente: {Id}", id);
        return Results.NotFound($"No existe el prestamo #{id}.");
    }

    prestamo.RegistrarDevolucion();
    decimal sancion = prestamo.CalcularSancion(politica);

    logger.LogInformation(
        "Devolucion registrada para el prestamo {Id}. Sancion aplicada: {Sancion:C}", id, sancion);

    biblioteca.GuardarCatalogo();
    return Results.Ok(new { prestamo.FechaDevolucion, Sancion = sancion });
});
Nivel Cuándo usarlo
LogInformation Sucesos normales que interesa poder rastrear después (un préstamo se registró, una devolución se procesó)
LogWarning Algo inesperado pero no grave: una petición con datos que no se encontraron, un reintento
LogError Un fallo real que impidió completar la operación (una excepción capturada, un servicio externo caído)

Los marcadores {Id} y {Sancion:C} en el mensaje no son interpolación de cadenas de C# ($"..."): son parámetros con nombre que el proveedor de logging estructura por separado del texto, lo que permite después buscar o filtrar registros por Id sin tener que analizar el texto del mensaje. Por defecto, ASP.NET Core escribe estos mensajes en la consola durante el desarrollo; en producción (Lección 5) se configuran proveedores adicionales (ficheros, servicios de monitorización) sin cambiar ni una línea de las llamadas a logger.LogInformation(...).

  1. Checklist de calidad antes de desplegar

Antes de pasar a la Lección 5 (Despliegue), este checklist reúne lo verificado en esta lección con lo ya visto en 08-05:

  • [ ] Todas las pruebas unitarias del Módulo 8 (Prestamo, Biblioteca.PrestarLibroAsync) están en verde.
  • [ ] Las nuevas pruebas de integración del apartado 2 sobre los endpoints principales están en verde.
  • [ ] El analizador de código del proyecto no reporta advertencias sin justificar (dotnet build sin warnings, o cada uno explícitamente revisado).
  • [ ] La checklist de revisión de 08-05 (nomenclatura, dependencias inyectadas, nulabilidad consistente) se ha aplicado sobre el código nuevo de 09-03.
  • [ ] Los endpoints que pueden fallar por causas esperadas (préstamo no disponible, socio no encontrado, metadatos externos no disponibles) devuelven un código HTTP y un mensaje claros, no un error 500 genérico.
  • [ ] Se ha añadido logging (ILogger) al menos en los puntos donde algo puede fallar por causas externas (persistencia, consulta de metadatos).

Superar este checklist no es una formalidad burocrática: es la confirmación concreta de que la API está lista para el paso siguiente, mucho más arriesgado si algo de esto faltara: publicarla fuera del entorno de desarrollo.

Errores Comunes y Consejos

  • Confundir una prueba de integración con una prueba manual: una prueba de integración debe poder ejecutarse automáticamente con dotnet test, igual que una prueba unitaria; probar la API a mano con Swagger es útil durante el desarrollo, pero no sustituye a una prueba automatizada que se repita en cada cambio.
  • Usar interpolación de cadenas ($"...") en llamadas a ILogger: pierde la estructura de los parámetros con nombre (apartado 5), dificultando buscar o filtrar registros después; usa siempre los marcadores {NombreParametro} con los valores como argumentos adicionales.
  • Depurar en producción con breakpoints: no es una opción real (no hay forma de "detener" un servidor en producción sin interrumpir a los usuarios); el logging del apartado 5 es la herramienta pensada exactamente para ese escenario.
  • Consejo: si una prueba de integración falla de forma intermitente (a veces pasa, a veces no), sospecha primero de dependencias de tiempo real (como el Task.Delay de PrestarLibroAsync) o de estado compartido entre pruebas que no se limpia correctamente entre una y otra.

Ejercicios

  1. Escribe una prueba de integración PostSocios_ConSocioValido_Devuelve201 que haga un POST a /socios con un Socio válido y compruebe que la respuesta tiene código 201 Created.

  2. Añade una llamada a logger.LogError en el catch (InvalidOperationException ex) del endpoint POST /prestamos de 09-03, registrando el mensaje de la excepción antes de devolver Results.Conflict(ex.Message).

Soluciones

[Fact]
public async Task PostSocios_ConSocioValido_Devuelve201()
{
    // Arrange
    Socio socio = new Socio(99, "Marta Ruiz");

    // Act
    HttpResponseMessage respuesta = await _cliente.PostAsJsonAsync("/socios", socio);

    // Assert
    Assert.Equal(HttpStatusCode.Created, respuesta.StatusCode);
}
try
{
    await biblioteca.PrestarLibroAsync(libro, socio);
}
catch (InvalidOperationException ex)
{
    logger.LogError(ex, "Fallo al registrar el prestamo para el ISBN {Isbn}", solicitud.Isbn);
    return Results.Conflict(ex.Message);
}

(Nota: esta versión requiere añadir ILogger<Program> logger como parámetro adicional del endpoint, igual que en el ejemplo del apartado 5; logger.LogError(ex, ...) registra además la excepción completa, no solo el mensaje.)

Conclusión

Esta lección ha ampliado la confianza en BiblioTech más allá de las pruebas unitarias del Módulo 8: ahora existen también pruebas de integración con WebApplicationFactory que verifican los endpoints reales de principio a fin, se han repasado las técnicas de depuración —incluida la particularidad de depurar código asíncrono—, se ha añadido logging con ILogger para diagnosticar problemas una vez desplegada la aplicación, y se ha fijado un checklist de calidad concreto. Con todos esos puntos superados, BiblioTech está lista, por fin, para el paso que cierra el proyecto y el curso entero: la lección final, Despliegue, publicará esta API, la contenedorizará con Docker, y configurará su comportamiento por entorno para producción.

Curso de Programación en C#

Módulo 1: Introducción a C#

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados