Saltar a contenido

A4.A Qué es una prueba y cómo se escribe

Para quién es esto

Es material avanzado: da por sabidas las unidades 1 a 6, en especial excepciones, clases e interfaces. Si aún no has visto cómo se crea un proyecto de pruebas, mira 1.4 Pruebas con xUnit.

Una prueba automática es un programa pequeño que ejecuta tu código con unos datos y comprueba que el resultado es el esperado. Se escribe una vez y se ejecuta cuantas veces haga falta: cada vez que cambias algo, en segundos sabes si sigue funcionando todo lo anterior.

La receta: preparar, actuar, comprobar

Casi toda prueba tiene tres partes (el patrón AAA, de Arrange, Act, Assert):

Parte Qué hace Ejemplo
Preparar (arrange) Crea los objetos y datos de partida var carrito = new Carrito();
Actuar (act) Ejecuta lo que se prueba, una sola cosa int total = carrito.Total();
Comprobar (assert) Compara lo obtenido con lo esperado Assert.Equal(0, total);

Dos consejos: una prueba comprueba una idea (por eso su nombre dice qué comprueba: ElTotalSumaPrecioPorCantidad), y las pruebas son independientes entre sí: no deben depender de que otra se haya ejecutado antes.

El marco de pruebas de C#: xUnit

En .NET los marcos de pruebas más usados son xUnit, NUnit y MSTest. Esta web usa xUnit, que es el que crea por defecto dotnet new xunit. Una prueba es un método marcado con [Fact] dentro de una clase pública, y las comprobaciones son métodos de Assert:

Comprobación Significa
Assert.Equal(esperado, obtenido) Son iguales (¡el esperado va primero!)
Assert.True(condicion) / Assert.False(...) La condición es cierta / falsa
Assert.Null(x) / Assert.NotNull(x) Es / no es null
Assert.Throws<T>(() => ...) El código lanza esa excepción (A4.B)
Assert.Contains(x, coleccion) La colección contiene el elemento

Un detalle de xUnit que lo diferencia de otros marcos: crea una instancia nueva de la clase de pruebas para cada [Fact]. Por eso un campo o el constructor de la clase hacen de «preparación previa» (lo que otros marcos llaman SetUp) sin que lo pidas.

Un ejemplo: un carrito de la compra

La clase que se prueba (precios en céntimos, para no usar decimales) y su proyecto de pruebas, que se ha ejecutado con dotnet test:

Carrito.cs:

public class Carrito
{
    private readonly List<(string Producto, int Precio, int Cantidad)> lineas = [];

    // Los precios van en céntimos
    public void Agregar(string producto, int precio, int cantidad)
    {
        if (cantidad <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(cantidad), "la cantidad debe ser positiva");
        }
        lineas.Add((producto, precio, cantidad));
    }

    public int Total() => lineas.Sum(l => l.Precio * l.Cantidad);

    public int Unidades => lineas.Sum(l => l.Cantidad);
}

CarritoTests.cs:

using Xunit;

public class CarritoTests
{
    [Fact]
    public void UnCarritoVacioTotalizaCero()
    {
        var carrito = new Carrito();                   // preparar

        int total = carrito.Total();                   // actuar

        Assert.Equal(0, total);                        // comprobar
    }

    [Fact]
    public void ElTotalSumaPrecioPorCantidad()
    {
        var carrito = new Carrito();
        carrito.Agregar("café", 120, 2);
        carrito.Agregar("tostada", 250, 1);

        Assert.Equal(490, carrito.Total());
    }

    [Fact]
    public void CuentaLasUnidades()
    {
        var carrito = new Carrito();
        carrito.Agregar("café", 120, 2);
        carrito.Agregar("tostada", 250, 1);

        Assert.Equal(3, carrito.Unidades);
    }
}

Resultado de dotnet test:

Con error: 0, Superado: 3, Omitido: 0, Total: 3

Las tres pruebas pasan. Cada una tiene un nombre que se lee como una frase y una sola comprobación principal.

Cuando una prueba falla

Una prueba que falla es buena noticia: te dice dónde está el problema. Aquí se ha escrito a propósito una comprobación con un valor esperado equivocado (120 en vez de 240):

Carrito.cs:

public class Carrito
{
    private readonly List<(string Producto, int Precio, int Cantidad)> lineas = [];

    // Los precios van en céntimos
    public void Agregar(string producto, int precio, int cantidad)
    {
        if (cantidad <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(cantidad), "la cantidad debe ser positiva");
        }
        lineas.Add((producto, precio, cantidad));
    }

    public int Total() => lineas.Sum(l => l.Precio * l.Cantidad);

    public int Unidades => lineas.Sum(l => l.Cantidad);
}

CarritoFalloTests.cs:

using Xunit;

public class CarritoFalloTests
{
    [Fact]
    public void ElTotalEstaMalCalculadoAPropositoEnLaPrueba()
    {
        var carrito = new Carrito();
        carrito.Agregar("café", 120, 2);

        Assert.Equal(120, carrito.Total());      // el esperado correcto sería 240
    }
}

Resultado de dotnet test:

Con error CarritoFalloTests.ElTotalEstaMalCalculadoAPropositoEnLaPrueba
    Assert.Equal() Failure: Values differ
    Expected: 120
    Actual:   240
Con error: 1, Superado: 0, Omitido: 0, Total: 1

El mensaje dice qué esperaba (Expected) y qué obtuvo (Actual). Lee siempre los dos: la mitad de las veces el fallo está en la prueba y no en el código.

Errores frecuentes

Error Cómo evitarlo
Poner Assert.Equal(obtenido, esperado) al revés El esperado va primero: los mensajes de fallo salen al revés si no
Varias ideas en una sola prueba Una prueba, una idea
Pruebas que dependen de otra o del orden Cada prueba prepara lo suyo (xUnit ya crea una instancia nueva por prueba)
Nombres como Prueba1 Nombres que digan qué se comprueba
Pruebas sin ninguna comprobación Toda prueba termina en un Assert
Probar la implementación en vez del comportamiento Comprueba qué devuelve o qué cambia, no cómo lo hace por dentro

Para practicar

Los ejercicios A4.1 a A4.4 usan estas ideas. Para ver la misma idea escrita en los otros lenguajes: Pasar de uno a otro.