Python: pruebas de unidad
Tags: spanish, python, unittesting, tutorial
Published: 2024-03-10 20:53:12

---
title: Python: pruebas de unidad
published: true
description: Unit tests en Python
tags: #spanish #python #unittesting #tutorial
cover_image: https://upload.wikimedia.org/wikipedia/commons/2/25/Peral1888.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2024-03-10 18:29 +0000
---
(El submarino en la imagen es el *Peral*, uno de los primeros submarinos del mundo, diseñado por el español Isaac Peral. Se movía mediante energía eléctrica.)

Las pruebas de unidad básicamente nos permiten comprobar que nuestro código funciona. Se trata de unas pruebas de caja negra, es decir, se comprueba si para unas ciertas entradas las salidas de nuestro código son las correctas.

Python, conocido por traer "pilas incluidas", tiene el módulo `unittest` en su librería estándar.

Supongamos que tenemos la clase **Submarino**, que mantiene dos coordenadas *x* e *y* para guardar su posición.

```python
import math


class Submarino:
    def __init__(self, x=0.0, y=0.0):
        self.__x = x
        self.__y = y
    ...
    
    @property
    def x(self):
        return self.__x
    
    @property
    def y(self):
        return self.__y
        
    def __str__(self):
        return f"Sub({self.x: 5.2f}, {self.y: 5.2f})"
...
```

Esta clase es sencilla: su inicializador acepta dos parámetros *x* e *y* que por defecto se inicializan a 0. Las coordenadas se guardan como *__x* e *__y*, pero son accesibles desde el exterior como *x* e *y*, de forma transparente al usuario de la clase, gracias a los métodos *x()* e *y()* y sus decoradores `@property`. El método *str()* devuelve las coordeadas formateadas, por ejemplo en 0, 0 nos devolvería: `Sub( 0.00,  0.00)`.

Nos falta algo muy importante: una forma de mover el submarino. Vamos a recurrir a las [coordenadas polares](https://es.wikipedia.org/wiki/Coordenadas_polares). Se trata de moverse una distancia en una determinada dirección. Esta dirección viene dada por un ángulo desde el punto actual. En resumen, es necesario cambiar el ángulo a radianes y aplicar las siguientes ecuaciones:

```
x = distancia * cos(angulo)
y = distancia * sin(angulo)
```

En **Python** tenemos la conversión a radianes mediante la función *radians()* en el módulo **math**. Esta conversión será necesaria porque los ángulos en las funciones trigonométricas se manejan en radianes, en lugar de en grados. También en el mismo módulo tenemos *sin()* y *cos()*. Así, partiendo del código anterior:

```python
class Submarino:
    def __init__(self):
        self.__x = 0.0
        self.__y = 0.0
    ...
    
    @property
    def x(self):
        return self.__x
    
    @property
    def y(self):
        return self.__y
    
    def avanza(self, angulo, distancia):
        angulo = math.radians(angulo)
        self.__x = distancia * math.cos(angulo)
        self.__y = distancia * math.sin(angulo)
    ...
    
    def __str__(self):
        return f"Sub({self.x: 5.2f}, {self.y: 5.2f})"
...
```

De acuerdo, ¿lo hemos hecho bien? ¿Cómo podemos estar seguros? Sí, podríamos crear un objeto de la clase **Submarino**, moverlo y ver si las coordenadas tienen sentido. Pero lo ideal sería que tuviésemos un sistema lo más automatizado posible, de manera que cuando lo utilicemos, nos avise de si hay algún error. Este sistema existe, y como ya mencionamos está en la librería estándar en el módulo `unittest`.


Básicamente, crearemos una clase que derive de **unittest.TestCase**. Dentro de esta clase crearemos los métodos necesarios: aquellos que comiencen por `test`, serán ejecutados automáticamente. Adicionalmente, podemos crear un método *setUp()*, que se ejecutará **antes** de cada uno de los tests. Normalmente, se utiliza para realizar inicializaciones repetitivas de código.

Supongamos que queremos crear un *test* que compruebe que, efectivamente, un submarino creado se posiciona inicialmente en 0, 0.

```python
import unittest
from submarino import Submarino


class Tests(unittest.TestCase):
    def setUp(self):
        self.sub = Submarino()
    ...
    
    def test_init(self):
        self.assertEqual(0, self.sub.x)
        self.assertEqual(0, self.sub.y)
    ...
...
```

En el método *setUp()*, creamos un submarino como un atributo *self.sub*. Dado que *setUp()* se ejecutará siempre antes de cada test, nuestro submarino siempre debe de estar en la posición `(0, 0)`, es decir, *x == 0* e *y == 0*.

Al derivar nuestra clase **Tests** de **unittest.TestCase**, disponemos de todos los métodos *assertXXX()* que podamos necesitar. Es importante recordar que en los parámetros se pasa primero el valor esperado, y a continuación, el calculado. En caso de que la comparación sea correcta, no sucede nada. Pero si es incorrecta, entonces el programa se para.

Los métodos de aserción disponibles son los siguientes:

|Método                   |Explicación                   |
|-------------------------|------------------------------|
|assertTrue(cond)         |Comprueba si cond es True     |
|assertFalse(con)         |Comprueba si cond es False    |
|assertEqual(x, y)        |Comprueba x == y              |
|assertNotEqual(x, y)     |Comprueba x != y              |
|assertIsNone(x)          |Comprueba si x es None        |
|assertIsNotNone(x)       |Comprueba si x no es None     |
|assertIn(x, y)           |Comprueba x in y              |
|assertNotIn(x, y)        |Comprueba x not in y          |
|assertIsInstance(x, y)   |Comprueba isinstance(x, y)    |
|assertNotIsInstance(x, y)|Comprueba not isinstance(x, y)|        

Todos estos métodos aceptan un parámetro *msg* a mayores, que en caso de que no se cumpla la comprobación, es utilizado para indicar por qué.

```python
self.assertTrue(0, self.sub.x, "el submarino no está en x == 0")
```

En cualquier caso, lo interesante ahora es comprobar si el método *avanza()* realmente funciona. Si es así, entonces deberíamos poder predecir cuáles son las coordenadas de destino tras una llamada a avanza. Vamos a comprobar los valores siguiendo la tabla a continuación.

|Ángulo      |Nombre      |Resultado tras avanzar 100|
|------------|------------|--------------------------|
|0           |este        |x=100,  y=0               |
|180         |oeste       |x=-100, y=0               |
|90          |norte       |x=100,  y=0               |
|270         |sur         |x=-100, y=0               |

Es razonable pensar que si estos movimientos básicos son correctos, el resto también lo serán.

```python
class Tests(unittest.TestCase):
    # Más cosas...

    def test_mov_este(self):
        self.sub.avanza(0, 100)
        self.assertEqual(100, self.sub.x)
        self.assertEqual(0, self.sub.y)
    ...
...
```

El movimiento hacia el este es sencillo de comprobar, ya que los resultados de los cálculos son números enteros. El resto de los casos tiene una dificultad añadida: tras los cálculos, es posible que el número real resultante no sea exactamente el esperado. Por ejemplo, un movimiento de 100 unidades hacia el norte desde x=0, y=0, debería darnos como resultado x=0, y=100. Podemos establecer que si dos números reales son iguales hasta el cuarto decimal, el valor sigue siendo suficientemente bueno. Para ello, podemos utilizar la función de comparación `math.isclose(x, y, abs_tol=0)`. Lo que hace esta función es comparar *x* e *y* aplicando una tolerancia dada por el tercer parámetro. Por ejemplo, en el caso que estábamos comentando, hasta el cuarto decimal, estaríamos hablando de, por ejemplo: `math.isclose(0, self.sub.x, abs_tol=0.00001)`. Para hacer las verificaciones, podemos recurrir a **TestCase**.*assertTrue()*.

```python
class Tests(unittest.TestCase):
    # Más cosas...

    def test_mov_oeste(self):
        self.sub.avanza(180, 100)
        self.assertEqual(-100, self.sub.x)
        self.assertTrue(math.isclose(0, self.sub.y, abs_tol=0.1))
    ...

    def test_mov_norte(self):
        self.sub.avanza(90, 100)
        self.assertEqual(100, self.sub.y)
        self.assertTrue(math.isclose(0, self.sub.x, abs_tol=0.00001))
    ...
    
    def test_mov_sur(self):
        self.sub.avanza(270, 100)
        self.assertEqual(-100, self.sub.y)
        self.assertTrue(math.isclose(0, self.sub.x, abs_tol=0.00001))
    ...
...

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

Ahora solo hace falta ejecutar el programa para que nos dé los resultados de los tests. Suponiendo que hayamos guardado la clase **Submarino** en *submarino.py*, y los *tests* en *tests_submarino.py*, la salida es la siguiente:

```bash
$ python tests_submarino.py
......
----------------------------------------------------------------------
Ran 6 tests in 0.000s

OK
```

Dado que no hay problemas, no se muestra ningún error. En caso contrario, se indicaría la línea en la que se produjo el error, y el motivo que lo provocó (la condición que no se verificó). Nótese que cada punto justo al comienzo de la ejecución es un *test* ejecutado, y que además, nos dice que ha ejecutado 6 *tests*. Así, seis tests correctos se muestran como "......". Si hubiera fallado el último, veríamos en su lugar ".....F".

Las pruebas de unidad son una poderosa herramienta: cada vez que hagamos cualquier modificación a la clase **Submarino**, podemos ejecutar los *tests* para comprobar que la funcionalidad sigue intacta. Las modificaciones se deben a corrección de errores y nueva funcionalidad. En ambos casos debemos crear *tests* que demuesntren que no hay errores.

- Corrección de un error: se debe crear un *test* que compruebe que ese *bug* no se sigue repitiendo. Por ejemplo, podría ser que al moverse hacia el noreste se produjese un error. En ese caso, crearíamos un test de forma que el submarino se moviese al noreste y comprobaríamos que las coordenadas son las correctas.

- Adición de funcionalidad: se debe crear un *test* que compruebe que la nueva funcionalidad es correcta. Por ejemplo, el submarino podría tener la posibilidad de sumergirse. Deberíamos comprobar que al llamar al método *sumerge()*, la profundidad (un nuevo atributo), cambia adecuadamente.

Por último, una posibilidad muy interesante es la de crear los *tests* **antes** que la funcionalidad. Por ejemplo, en lugar de crear el nuevo atributo *profundidad*, y el método *sumerge()*, podemos empezar por crear el método test_inmersion(), llamar al método y comprobar que la nueva funcionalidad es la adecuada. La diferencia es sutil, pero muy interesante: en lugar de continuar con el papel del desarrollador de la clase **Submarino**, nos colocamos sin querer en el papel del programador que emplea la clase **Submarino**, con lo que nos concentramos en cómo debería ofrecerse la funcionalidad, en lugar de crear la funcionalidad de la manera más simple posible desde el punto de vista del desarrollo.