Introducción: ¿Por qué son necesarias las pruebas en C#?
\nEn el desarrollo moderno con C# (.NET), la calidad del código no es un lujo, sino una necesidad. Las pruebas no solo permiten detectar errores antes de llegar a producción, sino que también sirven como documentación viva y aceleran la refactorización. Existen tres niveles principales de pruebas que forman la "pirámide de pruebas": Unit (unitarias), Integration (de integración) y E2E (de extremo a extremo). En este artículo analizaremos cada uno de ellos con ejemplos de código real en C#.
\n\n1. Pruebas unitarias: El fundamento de la calidad
\nLas pruebas unitarias verifican un fragmento de código aislado (método, clase) sin dependencias externas (bases de datos, sistema de archivos, API). En el ecosistema .NET, tres frameworks son populares: xUnit (recomendado para proyectos nuevos), NUnit y MSTest.
\n\nPrincipios fundamentales (FIRST)
\n- \n
- Fast — rápidas (milisegundos). \n
- Isolated — aisladas (usamos mocks). \n
- Repeatable — reproducibles. \n
- Self-validating — con autoverificación (Assert). \n
- Timely — oportunas. \n
Ejemplo de prueba unitaria con xUnit y Moq
\nSupongamos que tenemos un servicio para calcular descuentos:
\npublic interface IDiscountService\n{\n decimal CalculateDiscount(decimal price, bool isLoyaltyMember);\n}\n\npublic class DiscountService : IDiscountService\n{\n public decimal CalculateDiscount(decimal price, bool isLoyaltyMember)\n {\n if (isLoyaltyMember) return price * 0.9m; // 10% de descuento\n return price;\n }\n}\nPrueba usando Moq (para simular dependencias) y xUnit:
\nusing Xunit;\nusing Moq;\n\npublic class DiscountServiceTests\n{\n [Fact]\n public void CalculateDiscount_WhenLoyaltyMember_ReturnsDiscountedPrice()\n {\n // Arrange\n var service = new DiscountService();\n var price = 100m;\n\n // Act\n var result = service.CalculateDiscount(price, true);\n\n // Assert\n Assert.Equal(90m, result);\n }\n\n [Theory]\n [InlineData(100, false, 100)]\n [InlineData(200, true, 180)]\n public void CalculateDiscount_MultipleCases_ReturnsCorrectValues(decimal price, bool isLoyalty, decimal expected)\n {\n var service = new DiscountService();\n var result = service.CalculateDiscount(price, isLoyalty);\n Assert.Equal(expected, result);\n }\n}\nConsejo: Use el atributo [Fact] para pruebas individuales y [Theory] + [InlineData] para pruebas parametrizadas.
2. Pruebas de integración: Verificación de la conexión de componentes
\nLas pruebas de integración verifican cómo varios módulos funcionan juntos — por ejemplo, un controlador, un servicio y una base de datos real (o su sustituto, como SQLite en memoria). En ASP.NET Core se utiliza WebApplicationFactory para esto.
Configuración de WebApplicationFactory
\nCreemos una clase de prueba que levante un host de prueba web:
\nusing Microsoft.AspNetCore.Mvc.Testing;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing MyApp.Data;\n\npublic class CustomWebApplicationFactory : WebApplicationFactory<Program>\n{\n protected override void ConfigureWebHost(IWebHostBuilder builder)\n {\n builder.ConfigureServices(services =>\n {\n // Eliminamos el DbContext real\n var descriptor = services.SingleOrDefault(\n d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));\n if (descriptor != null) services.Remove(descriptor);\n\n // Agregamos base de datos en memoria\n services.AddDbContext<AppDbContext>(options =>\n