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
- Pruebas unitarias frente a pruebas de integración
- Pruebas de integración con
WebApplicationFactory - Depuración en Visual Studio y VS Code: breakpoints y watch
- Depuración de código asíncrono
- Logging básico con
ILogger - Checklist de calidad antes de desplegar
- 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.
- Pruebas de integración con
WebApplicationFactory
WebApplicationFactoryASP.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:
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.
- 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.
- 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 deObtenerMetadatosPorIsbnAsync) 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)dePrestarLibroAsync(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.
- Logging básico con
ILogger
ILoggerLos 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(...).
- 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 buildsin 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 aILogger: 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.DelaydePrestarLibroAsync) o de estado compartido entre pruebas que no se limpia correctamente entre una y otra.
Ejercicios
-
Escribe una prueba de integración
PostSocios_ConSocioValido_Devuelve201que haga unPOSTa/socioscon unSocioválido y compruebe que la respuesta tiene código201 Created. -
Añade una llamada a
logger.LogErroren elcatch (InvalidOperationException ex)del endpointPOST /prestamosde 09-03, registrando el mensaje de la excepción antes de devolverResults.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#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
