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 5. La unidad 1 ya te enseñó una primera prueba; aquí aprendes a escribirlas con método.

Una prueba automática es un programa corto que ejecuta tu código con unos datos y comprueba el resultado por ti. Lo que compruebas a mano una vez, la prueba lo comprueba todas las veces, en segundos, cada vez que cambias algo. Si rompes algo sin querer, lo sabes al momento y no dentro de una semana.

La receta: preparar, actuar, comprobar

Casi todas las pruebas siguen tres pasos (Arrange, Act, Assert):

Paso Qué haces En el ejemplo
Preparar Creas los objetos y los datos Un carrito nuevo
Actuar Llamas al código que se prueba agregar('tarta', 1800, 2)
Comprobar Dices qué esperas El total es 3600

Una buena prueba comprueba una sola cosa, tiene un nombre que dice lo que debe ocurrir y no depende de otra prueba.

El marco de pruebas de Python

Python trae unittest en su biblioteca estándar. Una clase que hereda de unittest.TestCase agrupa pruebas, y cada método cuyo nombre empieza por test es una. Las comprobaciones son assertEqual, assertTrue... Se ejecuta con python -m unittest -v. Otra opción muy usada es pytest (unidad 1), que hace lo mismo con assert simples.

Un ejemplo: un carrito de la compra

Primero, el código que se prueba. Los precios van en céntimos (enteros) para evitar errores de decimales:

carrito.py

"""El código que se prueba: un carrito de la compra.
Los precios van en céntimos (números enteros) para evitar errores de redondeo con decimales."""


class Carrito:
    def __init__(self):
        self._lineas = []

    def agregar(self, producto, precio, cantidad):
        if cantidad <= 0:
            raise ValueError("la cantidad debe ser mayor que cero")
        if precio < 0:
            raise ValueError("el precio no puede ser negativo")
        self._lineas.append((producto, precio, cantidad))

    @property
    def total(self):
        return sum(precio * cantidad for _, precio, cantidad in self._lineas)

    @property
    def unidades(self):
        return sum(cantidad for _, _, cantidad in self._lineas)

    @property
    def vacio(self):
        return not self._lineas

    def con_descuento(self, porcentaje):
        """El total después de aplicar un descuento de 0 a 100 por ciento."""
        if not 0 <= porcentaje <= 100:
            raise ValueError("el porcentaje debe estar entre 0 y 100")
        return self.total - self.total * porcentaje // 100

Y sus tres primeras pruebas:

test_carrito.py

import unittest

from carrito import Carrito


# unittest viene con Python: cada método que empieza por «test» es una prueba; setUp se ejecuta antes de CADA una.
class PruebasCarrito(unittest.TestCase):
    def setUp(self):
        self.carrito = Carrito()  # un carrito nuevo antes de CADA prueba: así no dependen unas de otras

    def test_un_carrito_nuevo_esta_vacio_y_su_total_es_0(self):
        self.assertTrue(self.carrito.vacio)
        self.assertEqual(self.carrito.total, 0)

    def test_agregar_un_producto_suma_su_importe(self):
        self.carrito.agregar("tarta", 1800, 2)
        self.assertEqual(self.carrito.total, 3600)
        self.assertEqual(self.carrito.unidades, 2)

    def test_con_varios_productos_el_total_es_la_suma_de_los_importes(self):
        self.carrito.agregar("tarta", 1800, 2)
        self.carrito.agregar("galleta", 100, 12)
        self.assertEqual(self.carrito.total, 4800)


if __name__ == "__main__":
    unittest.main()

El método setUp se ejecuta antes de cada prueba. Por eso el carrito se vuelve a crear: ninguna prueba hereda lo que hizo la anterior.

Al ejecutar las pruebas:

test_agregar_un_producto_suma_su_importe (test_carrito.PruebasCarrito.test_agregar_un_producto_suma_su_importe) ... ok
test_con_varios_productos_el_total_es_la_suma_de_los_importes (test_carrito.PruebasCarrito.test_con_varios_productos_el_total_es_la_suma_de_los_importes) ... ok
test_un_carrito_nuevo_esta_vacio_y_su_total_es_0 (test_carrito.PruebasCarrito.test_un_carrito_nuevo_esta_vacio_y_su_total_es_0) ... ok
----------------------------------------------------------------------
Ran 3 tests
OK

Cada línea acaba en ok si la prueba pasó, y el resumen dice Ran 3 tests y OK.

Cuando una prueba falla

Para ver un fallo de verdad, rompí el código a propósito: cambié precio × cantidad por precio + cantidad en el cálculo del total. Esto es lo que se ve:

FAIL: test_agregar_un_producto_suma_su_importe (...)
AssertionError: 1802 != 3600

Un buen mensaje de fallo dice qué prueba falló, qué se esperaba (3600) y qué se obtuvo (1802). Con eso suele bastar para encontrar el error sin depurar.

Una prueba que nunca ha fallado no demuestra nada

Cuando escribas una prueba, rómpela una vez a propósito (cambia el resultado esperado o el código) y comprueba que falla. Así sabes que de verdad está mirando algo.

Errores frecuentes

Error Cómo evitarlo
Pruebas que dependen unas de otras (el orden importa) Prepara de cero antes de cada prueba
Nombres como test1, test2 Describe el comportamiento: «agregar un producto suma su importe»
Comprobar muchas cosas distintas en una sola prueba Una idea por prueba: si falla, sabes qué falló
Copiar en la prueba el mismo cálculo que hace el código Escribe el resultado esperado a mano (3600), no precio * cantidad

Para practicar

Los ejercicios A4.1 y A4.4 piden escribir una función con sus pruebas y una preparación previa. Para ver la misma idea en los otros lenguajes: Pasar de uno a otro.