Pruebas unitarias con unittest
Introducción
Las pruebas unitarias son una práctica fundamental en el desarrollo de software profesional que consiste en verificar que partes individuales del código (unidades) funcionan correctamente. En Python, el módulo unittest
forma parte de la biblioteca estándar y proporciona un marco completo para crear y ejecutar pruebas. Dominar las pruebas unitarias te permitirá crear código más robusto, facilitar el mantenimiento y refactorización, y detectar errores tempranamente. En este artículo, aprenderás a implementar pruebas unitarias eficaces utilizando el módulo unittest
de Python.
El concepto de prueba unitaria
Una prueba unitaria verifica que un componente específico del código (normalmente una función o método) produce el resultado esperado para una entrada determinada. Estas pruebas son:
- Automatizadas: Se ejecutan sin intervención manual
- Independientes: No dependen de otras pruebas
- Repetibles: Producen el mismo resultado cada vez que se ejecutan
- Específicas: Comprueban una única funcionalidad
El módulo unittest
El módulo unittest
está inspirado en JUnit (framework de pruebas para Java) y sigue un enfoque orientado a objetos para definir y ejecutar pruebas.
Componentes principales de unittest
- TestCase: Clase base para crear pruebas
- TestSuite: Colección de casos de prueba
- TestRunner: Ejecutor de pruebas
- Assertions: Métodos para verificar resultados
Creando tu primera prueba unitaria
Vamos a crear una función simple y luego escribir pruebas para ella:
# calculadora.py
def suma(a, b):
return a + b
def resta(a, b):
return a - b
def multiplica(a, b):
return a * b
def divide(a, b):
if b == 0:
raise ValueError("No se puede dividir por cero")
return a / b
Ahora, creamos un archivo para las pruebas:
# test_calculadora.py
import unittest
from calculadora import suma, resta, multiplica, divide
class TestCalculadora(unittest.TestCase):
def test_suma(self):
self.assertEqual(suma(3, 5), 8)
self.assertEqual(suma(-1, 1), 0)
self.assertEqual(suma(-1, -1), -2)
def test_resta(self):
self.assertEqual(resta(5, 3), 2)
self.assertEqual(resta(1, 5), -4)
self.assertEqual(resta(-1, -1), 0)
def test_multiplica(self):
self.assertEqual(multiplica(3, 5), 15)
self.assertEqual(multiplica(-1, 5), -5)
self.assertEqual(multiplica(-1, -1), 1)
def test_divide(self):
self.assertEqual(divide(6, 3), 2)
self.assertEqual(divide(5, 2), 2.5)
self.assertEqual(divide(-1, 1), -1)
# Comprobamos que se lanza la excepción correcta
with self.assertRaises(ValueError):
divide(5, 0)
if __name__ == '__main__':
unittest.main()
Ejecutando las pruebas
Puedes ejecutar las pruebas de varias formas:
-
Desde la línea de comandos:
python -m unittest test_calculadora.py
-
Ejecutando el archivo directamente (gracias a la línea
if __name__ == '__main__'
):python test_calculadora.py
-
Descubrimiento automático de pruebas:
python -m unittest discover
Métodos de aserción
Las aserciones son el corazón de las pruebas unitarias. unittest
proporciona varios métodos para verificar resultados:
# Igualdad
self.assertEqual(valor_actual, valor_esperado)
self.assertNotEqual(valor_actual, valor_no_esperado)
# Booleanos
self.assertTrue(expresion)
self.assertFalse(expresion)
# Identidad
self.assertIs(objeto_actual, objeto_esperado)
self.assertIsNot(objeto_actual, objeto_no_esperado)
# Nulos
self.assertIsNone(valor)
self.assertIsNotNone(valor)
# Membresía
self.assertIn(elemento, coleccion)
self.assertNotIn(elemento, coleccion)
# Tipos
self.assertIsInstance(objeto, clase)
self.assertNotIsInstance(objeto, clase)
# Excepciones
with self.assertRaises(ExcepcionEsperada):
codigo_que_debe_lanzar_excepcion()
Organización de las pruebas
Métodos setUp y tearDown
Estos métodos se ejecutan antes y después de cada prueba, respectivamente:
import unittest
import tempfile
import os
class TestManejadorArchivos(unittest.TestCase):
def setUp(self):
# Se ejecuta antes de cada prueba
self.archivo_temp = tempfile.NamedTemporaryFile(delete=False)
self.nombre_archivo = self.archivo_temp.name
self.archivo_temp.close()
def tearDown(self):
# Se ejecuta después de cada prueba
os.unlink(self.nombre_archivo)
def test_escribir_archivo(self):
with open(self.nombre_archivo, 'w') as f:
f.write('Hola mundo')
with open(self.nombre_archivo, 'r') as f:
contenido = f.read()
self.assertEqual(contenido, 'Hola mundo')
setUpClass y tearDownClass
Estos métodos de clase se ejecutan una vez antes y después de todas las pruebas:
import unittest
import sqlite3
class TestBaseDatos(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Se ejecuta una vez antes de todas las pruebas
cls.conexion = sqlite3.connect(':memory:')
cls.cursor = cls.conexion.cursor()
cls.cursor.execute('CREATE TABLE usuarios (id INTEGER, nombre TEXT)')
@classmethod
def tearDownClass(cls):
# Se ejecuta una vez después de todas las pruebas
cls.conexion.close()
def test_insertar_usuario(self):
self.cursor.execute('INSERT INTO usuarios VALUES (1, "Ana")')
self.cursor.execute('SELECT * FROM usuarios WHERE id = 1')
usuario = self.cursor.fetchone()
self.assertEqual(usuario, (1, "Ana"))
Subclases de pruebas
Puedes organizar pruebas relacionadas en diferentes subclases:
class TestOperacionesBasicas(unittest.TestCase):
# Pruebas para suma y resta
class TestOperacionesAvanzadas(unittest.TestCase):
# Pruebas para multiplicación y división
Saltar pruebas
A veces necesitarás desactivar temporalmente una prueba:
import unittest
class TestCalculadora(unittest.TestCase):
@unittest.skip("Demostrando cómo saltar una prueba")
def test_suma(self):
self.assertEqual(suma(3, 5), 8)
@unittest.skipIf(sys.version_info.minor < 10, "Requiere Python 3.10+")
def test_funcion_nueva(self):
# Código que usa características de Python 3.10+
pass
Mock objects
Para pruebas más avanzadas, especialmente cuando hay dependencias externas, puedes usar la biblioteca unittest.mock
:
from unittest import TestCase, mock
import requests
from mi_aplicacion import obtener_datos_clima
class TestClima(TestCase):
@mock.patch('mi_aplicacion.requests.get')
def test_obtener_datos_clima(self, mock_get):
# Configuramos el comportamiento del mock
mock_response = mock.Mock()
mock_response.json.return_value = {"temperatura": 25, "humedad": 60}
mock_response.status_code = 200
mock_get.return_value = mock_response
# Ejecutamos la función que queremos probar
resultado = obtener_datos_clima("Madrid")
# Verificamos que la función llamó correctamente a requests.get
mock_get.assert_called_once_with('https://api.clima.ejemplo/Madrid')
# Verificamos el resultado
self.assertEqual(resultado, {"temperatura": 25, "humedad": 60})
Buenas prácticas
- Nombres descriptivos: Usa nombres que indiquen claramente qué se está probando.
- Una aserción por prueba: Idealmente, cada prueba debería verificar un único comportamiento.
- Independencia: Cada prueba debe funcionar por sí sola.
- Cobertura de código: Intenta que tus pruebas cubran todas las ramas del código.
- Prueba casos límite: No solo el caso "feliz", sino también valores extremos y errores.
- Mantén las pruebas simples: Las pruebas complicadas pueden tener sus propios errores.
Resumen
Las pruebas unitarias con unittest
son una herramienta poderosa para garantizar la calidad del código Python. Al crear pruebas automatizadas, puedes verificar que tu código funciona correctamente y detectar problemas rápidamente cuando realizas cambios. La estructura orientada a objetos de unittest
proporciona una forma organizada y flexible de definir pruebas, desde las más simples hasta las más complejas. Implementar pruebas unitarias es una inversión que se traduce en código más robusto y mantenible a largo plazo. En el próximo artículo, aprenderemos sobre la documentación de código con docstrings, otro aspecto fundamental del desarrollo profesional en Python.