CSharp
Tutorial CSharp: Testing unitario con xUnit
Aprende a configurar y escribir pruebas unitarias con xUnit en .NET para mejorar la calidad y mantenimiento de tu código con ejemplos prácticos.
Aprende CSharp y certifícateIntroducción al testing
El testing o pruebas de software es una práctica fundamental en el desarrollo de aplicaciones que nos permite verificar que nuestro código funciona correctamente. Cuando escribimos código, necesitamos asegurarnos de que hace exactamente lo que esperamos y que sigue funcionando correctamente a medida que realizamos cambios o añadimos nuevas características.
Las pruebas unitarias son un tipo específico de pruebas que se centran en verificar el correcto funcionamiento de unidades individuales de código, generalmente métodos o funciones. Estas pruebas son la base de una estrategia de testing efectiva y nos ayudan a detectar problemas temprano en el ciclo de desarrollo.
¿Por qué necesitamos pruebas unitarias?
Las pruebas unitarias ofrecen varios beneficios clave para los desarrolladores:
- Detección temprana de errores: Identifican problemas antes de que lleguen a producción.
- Facilitan el refactoring: Permiten modificar el código con confianza, sabiendo que las pruebas detectarán si algo deja de funcionar.
- Documentación viva: Actúan como ejemplos de cómo debe usarse el código.
- Diseño mejorado: Escribir código testeable nos lleva a crear componentes más modulares y con menor acoplamiento.
Características de buenas pruebas unitarias
Una prueba unitaria efectiva debe ser:
- Automatizada: Se ejecuta sin intervención manual.
- Rápida: Toma milisegundos en completarse.
- Consistente: Produce el mismo resultado cada vez que se ejecuta.
- Aislada: No depende de factores externos como bases de datos o servicios web.
- Legible: Cualquier desarrollador puede entender qué está probando y por qué.
El patrón AAA
La mayoría de las pruebas unitarias siguen el patrón AAA (Arrange-Act-Assert):
- Arrange: Preparar los datos y condiciones necesarias para la prueba.
- Act: Ejecutar la funcionalidad que queremos probar.
- Assert: Verificar que el resultado es el esperado.
Veamos un ejemplo sencillo en C#:
// Arrange
var calculadora = new Calculadora();
int a = 5;
int b = 3;
// Act
int resultado = calculadora.Sumar(a, b);
// Assert
Assert.Equal(8, resultado);
Frameworks de testing en .NET
En el ecosistema .NET existen varios frameworks para realizar pruebas unitarias:
- MSTest: El framework de pruebas integrado en Visual Studio.
- NUnit: Un framework de pruebas popular y maduro.
- xUnit: Un framework más moderno con un enfoque en la simplicidad y extensibilidad.
En este curso nos centraremos en xUnit, que es ampliamente utilizado en proyectos .NET modernos y es el framework de pruebas oficial para el desarrollo de .NET Core y .NET 5+.
xUnit vs otros frameworks
xUnit tiene algunas diferencias clave respecto a otros frameworks:
- Usa atributos como
[Fact]
y[Theory]
en lugar de[TestMethod]
o[Test]
. - No tiene métodos
Setup
oTearDown
explícitos, sino que utiliza el constructor y el patrónIDisposable
. - Ejecuta cada prueba en una nueva instancia de la clase de prueba, lo que ayuda a mantener las pruebas aisladas.
- Tiene un sistema de extensibilidad más moderno.
Tipos de pruebas
Aunque nos centraremos en pruebas unitarias, es importante conocer otros tipos de pruebas:
- Pruebas de integración: Verifican que diferentes componentes funcionan correctamente juntos.
- Pruebas de sistema: Prueban el sistema completo de extremo a extremo.
- Pruebas de aceptación: Verifican que el sistema cumple con los requisitos del usuario.
Las pruebas unitarias son la base de la pirámide de pruebas, donde deberíamos tener muchas pruebas unitarias, menos pruebas de integración y aún menos pruebas de sistema.
Herramientas complementarias
Además del framework de pruebas, existen herramientas que facilitan el testing:
- Mocking frameworks como Moq o NSubstitute, que permiten crear objetos simulados para aislar el código bajo prueba.
- Herramientas de cobertura de código como Coverlet, que miden qué porcentaje de nuestro código está cubierto por pruebas.
- Herramientas de análisis de mutaciones como Stryker.NET, que evalúan la calidad de nuestras pruebas.
Enfoque práctico
En las siguientes secciones, aprenderemos a configurar xUnit en nuestro proyecto y escribiremos nuestras primeras pruebas unitarias. Comenzaremos con ejemplos sencillos y progresivamente abordaremos escenarios más complejos.
El objetivo no es solo aprender la sintaxis de xUnit, sino desarrollar una mentalidad de testing que nos ayude a escribir código más robusto y mantenible. Las pruebas unitarias no son una carga adicional, sino una inversión que nos ahorrará tiempo y problemas en el futuro.
Configuración de xUnit
Para comenzar a utilizar xUnit en nuestros proyectos de C#, necesitamos configurar correctamente nuestro entorno de desarrollo. xUnit es un framework de pruebas moderno para .NET que facilita la escritura y ejecución de pruebas unitarias de manera eficiente.
Creación de un proyecto de pruebas
Lo primero que necesitamos es crear un proyecto específico para nuestras pruebas. La convención habitual es crear un proyecto separado del código de producción, siguiendo un patrón de nomenclatura como [NombreProyecto].Tests
.
Podemos crear un proyecto de pruebas xUnit de dos formas:
- Usando Visual Studio:
- Haz clic derecho en la solución
- Selecciona "Agregar" → "Nuevo proyecto"
- Busca "xUnit" en el cuadro de búsqueda
- Selecciona "Proyecto de pruebas xUnit (.NET Core)"
- Usando la línea de comandos:
dotnet new xunit -n MiProyecto.Tests
Estructura de carpetas recomendada
Una estructura de carpetas bien organizada facilita el mantenimiento de las pruebas:
MiSolucion/
├── MiProyecto/ # Proyecto principal
│ └── [Código fuente]
└── MiProyecto.Tests/ # Proyecto de pruebas
├── [Clases de prueba]
└── [Carpetas por funcionalidad]
Es recomendable organizar las pruebas de manera que reflejen la estructura del código que están probando. Por ejemplo, si tienes una clase Calculadora
en el namespace MiProyecto.Matematicas
, deberías tener una clase CalculadoraTests
en MiProyecto.Tests.Matematicas
.
Instalación de paquetes NuGet
Para utilizar xUnit, necesitamos instalar los paquetes NuGet correspondientes. Si has creado el proyecto usando las plantillas, estos paquetes ya estarán instalados. De lo contrario, puedes instalarlos manualmente:
- Usando Visual Studio:
- Haz clic derecho en el proyecto de pruebas
- Selecciona "Administrar paquetes NuGet"
- Busca e instala los siguientes paquetes:
xunit
xunit.runner.visualstudio
Microsoft.NET.Test.Sdk
- Usando la línea de comandos:
dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Microsoft.NET.Test.Sdk
Referenciando el proyecto a probar
Para que nuestras pruebas puedan acceder al código que queremos probar, necesitamos agregar una referencia al proyecto principal:
- Usando Visual Studio:
- Haz clic derecho en "Referencias" o "Dependencias" en el proyecto de pruebas
- Selecciona "Agregar referencia"
- Marca el proyecto principal y haz clic en "Aceptar"
- Usando la línea de comandos:
dotnet add MiProyecto.Tests/MiProyecto.Tests.csproj reference MiProyecto/MiProyecto.csproj
Archivo de proyecto (.csproj)
Después de realizar los pasos anteriores, tu archivo .csproj
del proyecto de pruebas debería tener un aspecto similar a este:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MiProyecto\MiProyecto.csproj" />
</ItemGroup>
</Project>
Configuración de Visual Studio Code
Si estás utilizando Visual Studio Code en lugar de Visual Studio, puedes instalar algunas extensiones útiles:
- .NET Core Test Explorer: Permite ejecutar y depurar pruebas directamente desde VS Code.
- C# Dev Kit: Proporciona soporte mejorado para C# y .NET.
Después de instalar estas extensiones, puedes configurar .NET Core Test Explorer añadiendo lo siguiente a tu settings.json
:
{
"dotnet-test-explorer.testProjectPath": "**/*.Tests.csproj",
"dotnet-test-explorer.autoWatch": true
}
Configuración para ejecución continua de pruebas
Para una experiencia de desarrollo más ágil, puedes configurar la ejecución automática de pruebas cada vez que guardas un archivo:
dotnet watch test
Este comando monitoriza los cambios en tu código y ejecuta las pruebas automáticamente, lo que acelera el ciclo de desarrollo.
Configuración de pruebas en CI/CD
Si utilizas sistemas de integración continua como GitHub Actions, Azure DevOps o Jenkins, puedes configurar la ejecución de pruebas como parte de tu pipeline:
Para GitHub Actions, un ejemplo básico sería:
name: .NET Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 6.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
Verificación de la configuración
Para comprobar que todo está correctamente configurado, podemos crear una prueba simple:
using Xunit;
namespace MiProyecto.Tests
{
public class PruebaInicial
{
[Fact]
public void PruebaDeConfiguracion()
{
// Esta prueba simplemente verifica que xUnit está configurado correctamente
Assert.True(true);
}
}
}
Ejecuta esta prueba para verificar que todo funciona:
- En Visual Studio: Abre el Explorador de pruebas (Prueba > Explorador de pruebas) y haz clic en "Ejecutar todas".
- En línea de comandos: Ejecuta
dotnet test
desde la carpeta del proyecto de pruebas.
Si la prueba se ejecuta correctamente, significa que xUnit está configurado adecuadamente y estamos listos para comenzar a escribir pruebas más significativas.
Configuración de cobertura de código
Para medir la cobertura de código de nuestras pruebas, podemos añadir la herramienta Coverlet:
dotnet add package coverlet.collector
Y luego ejecutar las pruebas con el siguiente comando:
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov /p:CoverletOutput=./lcov.info
Esto generará un informe de cobertura que podemos visualizar con herramientas como ReportGenerator o extensiones de VS Code.
Con esta configuración básica, ya estamos listos para comenzar a escribir pruebas unitarias efectivas con xUnit y aprovechar todas sus características para mejorar la calidad de nuestro código.
Primeros tests con [Fact]
Una vez configurado xUnit en nuestro proyecto, podemos comenzar a escribir nuestras primeras pruebas unitarias. En xUnit, el atributo [Fact]
es el bloque fundamental para crear pruebas simples que verifican un comportamiento específico.
Anatomía de una prueba con [Fact]
Una prueba básica con xUnit tiene la siguiente estructura:
using Xunit;
public class MiClaseTests
{
[Fact]
public void MiMetodo_Escenario_ResultadoEsperado()
{
// Arrange - Preparar
// Act - Actuar
// Assert - Verificar
}
}
Analicemos los elementos clave:
- El atributo
[Fact]
indica a xUnit que este método es una prueba unitaria. - El método debe ser
public
y no devolver ningún valor (void
). - El nombre del método debe ser descriptivo, siguiendo la convención
MetodoQuePruebo_EscenarioQuePruebo_ResultadoEsperado
.
Creando nuestra primera prueba real
Vamos a crear una clase Calculadora
simple y escribir pruebas para ella:
// En el proyecto principal (MiProyecto)
public class Calculadora
{
public int Sumar(int a, int b)
{
return a + b;
}
}
Ahora, escribamos una prueba para el método Sumar
:
// En el proyecto de pruebas (MiProyecto.Tests)
using Xunit;
public class CalculadoraTests
{
[Fact]
public void Sumar_DosNumerosPositivos_DevuelveSuma()
{
// Arrange
var calculadora = new Calculadora();
int a = 5;
int b = 3;
// Act
int resultado = calculadora.Sumar(a, b);
// Assert
Assert.Equal(8, resultado);
}
}
Esta prueba sigue el patrón AAA (Arrange-Act-Assert) que mencionamos anteriormente:
- Arrange: Creamos una instancia de
Calculadora
y definimos los valores de entrada. - Act: Llamamos al método
Sumar
con los valores preparados. - Assert: Verificamos que el resultado es el esperado (8).
Aserciones básicas en xUnit
xUnit proporciona una variedad de métodos de aserción en la clase Assert
para verificar diferentes condiciones:
// Verificar igualdad
Assert.Equal(esperado, actual);
// Verificar que dos objetos son el mismo (referencia)
Assert.Same(esperado, actual);
// Verificar que una condición es verdadera
Assert.True(condicion);
// Verificar que una condición es falsa
Assert.False(condicion);
// Verificar que un objeto es null
Assert.Null(objeto);
// Verificar que un objeto no es null
Assert.NotNull(objeto);
// Verificar que una colección contiene un elemento
Assert.Contains(elemento, coleccion);
// Verificar que una acción lanza una excepción
Assert.Throws<TipoExcepcion>(() => metodo());
Probando diferentes escenarios
Es importante probar múltiples escenarios para cada método. Ampliemos nuestras pruebas para la calculadora:
[Fact]
public void Sumar_NumeroPositivoYNegativo_DevuelveSuma()
{
// Arrange
var calculadora = new Calculadora();
int a = 5;
int b = -3;
// Act
int resultado = calculadora.Sumar(a, b);
// Assert
Assert.Equal(2, resultado);
}
[Fact]
public void Sumar_DosNumerosNegativos_DevuelveSuma()
{
// Arrange
var calculadora = new Calculadora();
int a = -5;
int b = -3;
// Act
int resultado = calculadora.Sumar(a, b);
// Assert
Assert.Equal(-8, resultado);
}
Probando excepciones
Supongamos que añadimos un método de división a nuestra calculadora que debe lanzar una excepción cuando intentamos dividir por cero:
// En la clase Calculadora
public double Dividir(int a, int b)
{
if (b == 0)
throw new DivideByZeroException("No se puede dividir por cero");
return (double)a / b;
}
Podemos probar este comportamiento con xUnit:
[Fact]
public void Dividir_DividirPorCero_LanzaExcepcion()
{
// Arrange
var calculadora = new Calculadora();
int a = 10;
int b = 0;
// Act & Assert (combinados para pruebas de excepciones)
var excepcion = Assert.Throws<DivideByZeroException>(() => calculadora.Dividir(a, b));
// Opcionalmente, podemos verificar el mensaje de la excepción
Assert.Contains("No se puede dividir por cero", excepcion.Message);
}
Probando un validador de correo electrónico
Veamos otro ejemplo con un validador de correo electrónico:
// En el proyecto principal
public class ValidadorEmail
{
public bool EsEmailValido(string email)
{
if (string.IsNullOrWhiteSpace(email))
return false;
// Verificación simple con una expresión regular básica
return System.Text.RegularExpressions.Regex.IsMatch(
email,
@"^[^@\s]+@[^@\s]+\.[^@\s]+$"
);
}
}
Ahora escribamos pruebas para este validador:
public class ValidadorEmailTests
{
[Fact]
public void EsEmailValido_EmailConFormatoCorrecto_DevuelveTrue()
{
// Arrange
var validador = new ValidadorEmail();
string email = "usuario@dominio.com";
// Act
bool resultado = validador.EsEmailValido(email);
// Assert
Assert.True(resultado);
}
[Fact]
public void EsEmailValido_EmailSinArroba_DevuelveFalse()
{
// Arrange
var validador = new ValidadorEmail();
string email = "usuariodominio.com";
// Act
bool resultado = validador.EsEmailValido(email);
// Assert
Assert.False(resultado);
}
[Fact]
public void EsEmailValido_EmailVacio_DevuelveFalse()
{
// Arrange
var validador = new ValidadorEmail();
string email = "";
// Act
bool resultado = validador.EsEmailValido(email);
// Assert
Assert.False(resultado);
}
[Fact]
public void EsEmailValido_EmailNull_DevuelveFalse()
{
// Arrange
var validador = new ValidadorEmail();
string email = null;
// Act
bool resultado = validador.EsEmailValido(email);
// Assert
Assert.False(resultado);
}
}
Compartiendo configuración entre pruebas
Si necesitamos la misma instancia para varias pruebas, podemos utilizar el constructor de la clase de prueba:
public class CalculadoraTests
{
private readonly Calculadora _calculadora;
public CalculadoraTests()
{
// Este código se ejecuta antes de cada prueba
_calculadora = new Calculadora();
}
[Fact]
public void Sumar_DosNumerosPositivos_DevuelveSuma()
{
// Arrange ya está hecho en el constructor
// Act
int resultado = _calculadora.Sumar(5, 3);
// Assert
Assert.Equal(8, resultado);
}
[Fact]
public void Sumar_NumeroPositivoYNegativo_DevuelveSuma()
{
// Act
int resultado = _calculadora.Sumar(5, -3);
// Assert
Assert.Equal(2, resultado);
}
}
Limpieza de recursos
Si nuestras pruebas utilizan recursos que deben ser liberados (como conexiones a bases de datos o archivos), podemos implementar IDisposable
:
public class RecursoTests : IDisposable
{
private readonly RecursoExterno _recurso;
public RecursoTests()
{
_recurso = new RecursoExterno();
}
public void Dispose()
{
// Este código se ejecuta después de cada prueba
_recurso.Dispose();
}
[Fact]
public void MiPrueba()
{
// Usar _recurso...
}
}
Ejecutando pruebas específicas
Podemos ejecutar pruebas específicas de varias maneras:
- En Visual Studio: Haz clic derecho en una prueba en el Explorador de pruebas y selecciona "Ejecutar".
- En línea de comandos: Usa filtros para ejecutar pruebas específicas:
dotnet test --filter "FullyQualifiedName=MiProyecto.Tests.CalculadoraTests.Sumar_DosNumerosPositivos_DevuelveSuma"
Buenas prácticas para pruebas con [Fact]
- Mantén las pruebas pequeñas y enfocadas: Cada prueba debe verificar una sola cosa.
- Usa nombres descriptivos: El nombre debe indicar qué se está probando, bajo qué condiciones y qué resultado se espera.
- Sigue el patrón AAA: Arrange-Act-Assert hace que las pruebas sean más legibles y mantenibles.
- Evita dependencias entre pruebas: Cada prueba debe poder ejecutarse de forma independiente.
- Prueba casos límite: No solo pruebes el "camino feliz", sino también casos extremos y errores.
- Mantén las pruebas rápidas: Las pruebas lentas desalientan su ejecución frecuente.
Con estos conceptos básicos, ya estás listo para comenzar a escribir pruebas unitarias efectivas con xUnit. En la siguiente fase, exploraremos pruebas parametrizadas con el atributo [Theory]
, que nos permitirá probar múltiples escenarios con menos código.
Ejercicios de esta lección Testing unitario con xUnit
Evalúa tus conocimientos de esta lección Testing unitario con xUnit con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
CRUD en C# de modelo Customer sobre una lista
Arrays y listas
Objetos
Excepciones
Eventos
Lambdas
Diccionarios en C#
Variables y constantes
Tipos de datos
Herencia
Operadores
Uso de consultas LINQ
Clases y encapsulación
Uso de consultas LINQ
Excepciones
Control de flujo
Eventos
Diccionarios
Tipos de datos
Conjuntos, colas y pilas
Lambdas
Conjuntos, colas y pilas
Uso de async y await
Tareas
Constructores y destructores
Operadores
Arrays y listas
Polimorfismo
Polimorfismo
Variables y constantes
Proyecto colecciones y LINQ en C#
Clases y encapsulación
Creación de proyecto C#
Uso de async y await
Funciones
Delegados
Delegados
Constructores y destructores
Objetos
Control de flujo
Funciones
Tareas
Proyecto sintaxis en C#
Herencia C Sharp
OOP en C Sharp
Diccionarios
Introducción a C#
Todas las lecciones de CSharp
Accede a todas las lecciones de CSharp y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A C#
Introducción Y Entorno
Creación De Proyecto C#
Introducción Y Entorno
Variables Y Constantes
Sintaxis
Tipos De Datos
Sintaxis
Operadores
Sintaxis
Control De Flujo
Sintaxis
Funciones
Sintaxis
Estructuras De Control Iterativo
Sintaxis
Interpolación De Strings
Sintaxis
Estructuras De Control Condicional
Sintaxis
Manejo De Valores Nulos
Sintaxis
Clases Y Encapsulación
Programación Orientada A Objetos
Objetos
Programación Orientada A Objetos
Constructores Y Destructores
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
Genéricos
Programación Orientada A Objetos
Métodos Virtuales Y Sobrecarga
Programación Orientada A Objetos
Clases Abstractas
Programación Orientada A Objetos
Interfaces
Programación Orientada A Objetos
Propiedades Y Encapsulación
Programación Orientada A Objetos
Métodos De Extensión
Programación Orientada A Objetos
Clases Y Objetos
Programación Orientada A Objetos
Clases Parciales
Programación Orientada A Objetos
Miembros Estáticos
Programación Orientada A Objetos
Tuplas Y Tipos Anónimos
Programación Orientada A Objetos
Arrays Y Listas
Colecciones Y Linq
Diccionarios
Colecciones Y Linq
Conjuntos, Colas Y Pilas
Colecciones Y Linq
Uso De Consultas Linq
Colecciones Y Linq
Linq Avanzado
Colecciones Y Linq
Colas Y Pilas
Colecciones Y Linq
Conjuntos
Colecciones Y Linq
Linq Básico
Colecciones Y Linq
Delegados Funcionales
Programación Funcional
Records
Programación Funcional
Expresiones Lambda
Programación Funcional
Linq Funcional
Programación Funcional
Fundamentos De La Programación Funcional
Programación Funcional
Pattern Matching
Programación Funcional
Testing Unitario Con Xunit
Testing
Excepciones
Excepciones
Delegados
Programación Asíncrona
Eventos
Programación Asíncrona
Lambdas
Programación Asíncrona
Uso De Async Y Await
Programación Asíncrona
Tareas
Programación Asíncrona
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la importancia y beneficios de las pruebas unitarias en el desarrollo de software.
- Aprender a configurar un proyecto de pruebas con xUnit en el entorno .NET.
- Escribir pruebas unitarias básicas utilizando el atributo [Fact] siguiendo el patrón AAA.
- Conocer las aserciones básicas que ofrece xUnit para validar resultados.
- Aplicar buenas prácticas para mantener pruebas legibles, rápidas y aisladas.