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ícate

Introducció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):

  1. Arrange: Preparar los datos y condiciones necesarias para la prueba.
  2. Act: Ejecutar la funcionalidad que queremos probar.
  3. 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 o TearDown explícitos, sino que utiliza el constructor y el patrón IDisposable.
  • 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:
  1. Haz clic derecho en la solución
  2. Selecciona "Agregar" → "Nuevo proyecto"
  3. Busca "xUnit" en el cuadro de búsqueda
  4. 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:
  1. Haz clic derecho en el proyecto de pruebas
  2. Selecciona "Administrar paquetes NuGet"
  3. 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:
  1. Haz clic derecho en "Referencias" o "Dependencias" en el proyecto de pruebas
  2. Selecciona "Agregar referencia"
  3. 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:

  1. Arrange: Creamos una instancia de Calculadora y definimos los valores de entrada.
  2. Act: Llamamos al método Sumar con los valores preparados.
  3. 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.

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende CSharp online

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.

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

Accede GRATIS a CSharp y certifícate

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.