Las pruebas unitarias son la base de cualquier software confiable. En Python, el módulo unittest es la herramienta nativa para crear y ejecutar pruebas de forma estructurada y profesional. Si deseas escribir código Python de calidad, dominar unittest es un paso obligatorio.
En esta guía completa, aprenderás desde los conceptos fundamentales hasta técnicas avanzadas de pruebas con unittest, incluyendo mocks, fixtures y mejores prácticas que todo desarrollador Python debe conocer.
¿Qué Son las Pruebas Unitarias?
Las pruebas unitarias son pequeños programas que verifican el comportamiento de unidades individuales de tu código — normalmente funciones o métodos. Cada prueba aísla una parte específica del sistema y valida si produce el resultado esperado para ciertas entradas.
A diferencia de las pruebas de integración, que verifican cómo diferentes módulos funcionan juntos, las pruebas unitarias se centran en componentes aislados. Este enfoque permite detectar errores tempranamente, facilita la refactorización y documenta el comportamiento esperado del código.
El desarrollo guiado por pruebas (TDD) lleva este concepto al siguiente nivel: primero escribes la prueba que falla, luego implementas el código mínimo para que pase y finalmente refactorizas. Este ciclo conocido como "Red-Green-Refactor" es ampliamente adoptado en la industria.
¿Por Qué Usar unittest?
Python incluye el módulo unittest en su biblioteca estándar desde la versión 2.1. Inspirado en JUnit (el framework de pruebas de Java), ofrece una estructura robusta y madura para crear pruebas. Estas son las razones para considerarlo:
- Cero dependencias externas — ya viene instalado con Python, sin necesidad de pip install
- Integración con IDEs — PyCharm, VS Code y otras herramientas detectan y ejecutan pruebas unittest de forma nativa
- Compatibilidad con CI/CD — GitHub Actions, GitLab CI y Jenkins tienen soporte directo
- Módulo mock incorporado — desde Python 3.3,
unittest.mockforma parte de la biblioteca estándar - Descubrimiento automático de pruebas — el ejecutor encuentra archivos y métodos siguiendo convenciones de nomenclatura
- Informes detallados — salida enriquecida con conteo de éxitos, fallos y errores
Aunque pytest es otra herramienta popular, unittest sigue siendo la opción ideal para proyectos que priorizan estabilidad y ausencia de dependencias externas. Proyectos importantes como CPython, Django y Plone utilizan unittest extensivamente.
Estructura Básica de una Prueba con unittest
unittest sigue el patrón xUnit, donde las pruebas se organizan en clases que heredan de unittest.TestCase. Cada método dentro de la clase representa una prueba individual. La estructura mínima es:
import unittest
def sumar(a, b):
return a + b
class TestSumar(unittest.TestCase):
def test_suma_positivos(self):
self.assertEqual(sumar(2, 3), 5)
def test_suma_negativos(self):
self.assertEqual(sumar(-1, -1), -2)
if name == 'main':
unittest.main()
El método unittest.main() descubre y ejecuta todos los métodos que comienzan con test_ dentro de las clases que heredan de TestCase. Puedes ejecutar este archivo directamente con python mi_prueba.py.
Métodos de Aserción (Assert Methods)
Los métodos de aserción son el corazón de las pruebas. unittest ofrece docenas de métodos para verificar diferentes condiciones:
self.assertEqual(a, b) # a == b
self.assertNotEqual(a, b) # a != b
self.assertTrue(x) # x es True
self.assertFalse(x) # x es False
self.assertIs(a, b) # a is b
self.assertIsNot(a, b) # a is not b
self.assertIsNone(x) # x is None
self.assertIsNotNone(x) # x is not None
self.assertIn(a, b) # a in b
self.assertNotIn(a, b) # a not in b
self.assertIsInstance(a, b) # isinstance(a, b)
self.assertRaises(Exc, func) # func lanza Exc
self.assertAlmostEqual(a, b) # a ≈ b (para floats)
Para probar excepciones, la forma más elegante es usar el administrador de contexto:
with self.assertRaises(ValueError):
int('abc')
También puedes verificar el mensaje de la excepción:
with self.assertRaises(ValueError) as ctx:
int('abc')
self.assertEqual(str(ctx.exception), "invalid literal for int() with base 10: 'abc'")
Fixtures: setUp y tearDown
Los fixtures son métodos que se ejecutan antes y después de cada prueba para preparar y limpiar recursos. unittest ofrece cuatro niveles de fixtures:
class TestBaseDeDatos(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Se ejecuta una vez antes de todas las pruebas
cls.conexion = conectar_base_datos()
@classmethod
def tearDownClass(cls):
# Se ejecuta una vez después de todas las pruebas
cls.conexion.cerrar()
def setUp(self):
# Se ejecuta antes de cada prueba
self.cursor = self.conexion.cursor()
self.conexion.iniciar_transaccion()
def tearDown(self):
# Se ejecuta después de cada prueba
self.conexion.rollback()
self.cursor.cerrar()
El uso correcto de los fixtures elimina la duplicación y garantiza que cada prueba comience en un estado limpio y predecible. Usa setUp para crear objetos, abrir archivos o conectar bases de datos, y tearDown para liberar todos los recursos asignados.
Descubrimiento y Ejecución de Pruebas
unittest posee un descubridor automático que localiza y ejecuta pruebas siguiendo convenciones:
# Ejecuta todas las pruebas en el directorio actual
python -m unittest discover
Ejecuta pruebas en un directorio específico
python -m unittest discover tests/
Ejecuta pruebas con un patrón de nombre específico
python -m unittest discover -p "*_test.py"
Ejecuta un módulo de prueba específico
python -m unittest tests/test_calculadora.py
Ejecuta una clase específica
python -m unittest tests.test_calculadora.TestSumar
Ejecuta un método específico
python -m unittest tests.test_calculadora.TestSumar.test_suma_positivos
El descubrimiento automático busca archivos que coincidan con el patrón test*.py en todo el directorio especificado. Esta funcionalidad es especialmente útil en proyectos grandes con cientos de archivos de prueba.
Mocks y Patching con unittest.mock
En aplicaciones reales, tu código a menudo depende de servicios externos — APIs HTTP, bases de datos, sistemas de archivos. Probar estas interacciones directamente es lento, frágil e impráctico. Aquí es donde entran los mocks.
El módulo unittest.mock permite reemplazar partes de tu sistema con objetos simulados durante las pruebas. El uso más común es a través del decorador @patch:
from unittest.mock import patch
import requests
def buscar_usuario(api_url, usuario_id):
respuesta = requests.get(f'{api_url}/usuarios/{usuario_id}')
return respuesta.json()
class TestBuscarUsuario(unittest.TestCase):
@patch('requests.get')
def test_retorna_datos_del_usuario(self, mock_get):
mock_get.return_value.json.return_value = {
'id': 1, 'nombre': 'Ana García'
}
resultado = buscar_usuario('https://api.ejemplo.com', 1)
self.assertEqual(resultado['nombre'], 'Ana García')
mock_get.assert_called_once_with(
'https://api.ejemplo.com/usuarios/1'
)
El decorador patch reemplaza temporalmente el objeto original por el mock durante la prueba. Después de la ejecución, el objeto original se restaura automáticamente. También puedes usar patch como administrador de contexto:
def test_con_context_manager(self):
with patch('requests.get') as mock_get:
mock_get.return_value.status_code = 200
resultado = buscar_usuario('https://api.ejemplo.com', 1)
self.assertIsNotNone(resultado)
El mock ofrece métodos potentes para verificar interacciones:
assert_called()— verifica si el mock fue llamadoassert_called_once()— verifica si fue llamado exactamente una vezassert_called_with(args)— verifica la última llamada con argumentos específicosassert_any_call(args)— verifica si hubo alguna llamada con esos argumentoscall_count— propiedad que devuelve el número total de llamadas
Para clases, usa patch.object:
class TestApiClient(unittest.TestCase):
@patch.object(ApiClient, 'enviar_request')
def test_envio_de_datos(self, mock_enviar):
cliente = ApiClient()
cliente.enviar_datos({'clave': 'valor'})
mock_enviar.assert_called_once()
Organizando Pruebas en Suites
En proyectos grandes, puedes agrupar pruebas en suites para ejecución selectiva:
def suite():
suite = unittest.TestSuite()
suite.addTest(TestSumar('test_suma_positivos'))
suite.addTest(TestRestar('test_resta_negativos'))
return suite
if name == 'main':
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite())
También puedes crear suites modulares combinando pruebas de diferentes módulos:
import tests.test_calculadora
import tests.test_usuario
def suite_completa():
loader = unittest.TestLoader()
suite = unittest.TestSuite()
suite.addTests(loader.loadTestsFromModule(tests.test_calculadora))
suite.addTests(loader.loadTestsFromModule(tests.test_usuario))
return suite
Saltar Pruebas y Fallos Esperados
unittest permite saltar pruebas condicionalmente o marcar fallos esperados:
class TestAvanzado(unittest.TestCase):
@unittest.skip("Funcionalidad aún no implementada")
def test_nuevo_recurso(self):
pass
@unittest.skipIf(sys.version_info < (3, 10), "Requiere Python 3.10+")
def test_match_case(self):
...
@unittest.skipUnless(os.name == 'posix', "Solo Unix")
def test_comando_shell(self):
...
@unittest.expectedFailure
def test_error_conocido(self):
self.assertEqual(calcular_algo(), valor_esperado)
Estos decoradores son útiles para mantener una suite de pruebas funcional incluso cuando parte del sistema está en desarrollo o depende de condiciones específicas del entorno.
Mejores Prácticas en Pruebas Unitarias
Escribir buenas pruebas es una habilidad que va más allá de la sintaxis de unittest. Estas son las prácticas más importantes:
1. Pruebas Independientes y Aisladas
Cada prueba debe ser completamente independiente de las demás. Una prueba nunca debe depender del resultado o del estado dejado por otra prueba. Usa mocks para aislar el código de dependencias externas.
2. Nomenclatura Clara
El nombre de la prueba debe describir exactamente qué se está verificando y cuál es el resultado esperado: test_cuando_saldo_insuficiente_debe_lanzar_excepcion es mucho más informativo que test_retiro.
3. Una Afirmación por Concepto
Cada método de prueba debe verificar un único comportamiento. Si la prueba falla, sabrás exactamente qué está roto sin necesidad de depurar múltiples aserciones.
4. Usa Mocks con Moderación
Los mocks son herramientas poderosas, pero el uso excesivo puede hacer que las pruebas sean frágiles y difíciles de mantener. Prefiere probar el comportamiento real siempre que sea posible y usa mocks solo para aislar dependencias lentas o impredecibles como APIs externas y bases de datos.
5. Escribe Pruebas Antes del Código (TDD)
El desarrollo guiado por pruebas te obliga a pensar en el diseño de la API antes de la implementación, lo que resulta en un código más limpio y modular. El ciclo "Red-Green-Refactor" es una práctica consolidada.
6. Mantén las Pruebas Rápidas
Una suite de pruebas que tarda horas en ejecutarse desalienta su ejecución frecuente. Diseña las pruebas para que sean rápidas — segundos, no minutos. Las pruebas lentas deben moverse a una categoría separada de pruebas de integración.
7. Cubre los Casos Límite
No pruebes solo el "camino feliz". Prueba entradas vacías, valores extremos, tipos inesperados y condiciones de error. Es en estos bordes donde se esconden la mayoría de los errores.
unittest vs pytest: ¿Cuál Elegir?
Esta es una de las preguntas más comunes entre los desarrolladores Python. Ambos frameworks tienen méritos y la elección depende del contexto de tu proyecto.
unittest es la opción correcta cuando necesitas cero dependencias, trabajas en proyectos conservadores que priorizan la estabilidad o contribuyes a proyectos de la Python Software Foundation. Frameworks importantes como Django usan unittest internamente, y el ecosistema de herramientas de CI tiene soporte nativo.
pytest brilla por su simplicidad y productividad. Su sintaxis funcional (sin necesidad de clases) y sus potentes fixtures reducen drásticamente el boilerplate. El ecosistema de plugins es vasto y los mensajes de error son excepcionalmente claros.
En la práctica, muchos proyectos usan ambos: escriben pruebas con pytest y utilizan unittest.mock para simular dependencias. Puedes leer más sobre pytest en el artículo sobre pruebas automatizadas con pytest.
Midiendo la Cobertura de Pruebas
La calidad de las pruebas no se mide solo por la cantidad, sino por la cobertura. coverage.py es la herramienta más utilizada para medir qué líneas de tu código se ejecutan durante las pruebas:
# Instalación
pip install coverage
Ejecución con coverage
coverage run -m unittest discover
Informe en terminal
coverage report
Informe HTML detallado
coverage html
El informe de cobertura muestra el porcentaje de líneas ejecutadas por archivo, ayudando a identificar código no probado. Sin embargo, recuerda: el 100% de cobertura no garantiza el 100% de calidad. Es más importante tener pruebas significativas que cubran comportamientos críticos que simplemente buscar el número máximo.
Ejemplo Práctico: Probando una API REST
Vamos a consolidar todo lo que hemos aprendido con un ejemplo completo de prueba para una clase que consume una API REST:
import unittest
from unittest.mock import patch, Mock
import requests
class ClienteAPI:
BASE_URL = 'https://api.ejemplo.com/v1'
def __init__(self, token):
self.token = token
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {token}'
})
def buscar_usuario(self, usuario_id):
respuesta = self.session.get(
f'{self.BASE_URL}/usuarios/{usuario_id}'
)
respuesta.raise_for_status()
return respuesta.json()
def crear_usuario(self, datos):
respuesta = self.session.post(
f'{self.BASE_URL}/usuarios',
json=datos
)
respuesta.raise_for_status()
return respuesta.json()
class TestClienteAPI(unittest.TestCase):
def setUp(self):
self.cliente = ClienteAPI('token_valido')
@patch('requests.Session.get')
def test_buscar_usuario_exitoso(self, mock_get):
mock_get.return_value.json.return_value = {
'id': 1, 'nombre': 'Carlos'
}
mock_get.return_value.raise_for_status = Mock()
resultado = self.cliente.buscar_usuario(1)
self.assertEqual(resultado['nombre'], 'Carlos')
mock_get.assert_called_once_with(
'https://api.ejemplo.com/v1/usuarios/1'
)
@patch('requests.Session.get')
def test_buscar_usuario_error_404(self, mock_get):
mock_get.return_value.raise_for_status.side_effect = \
requests.exceptions.HTTPError('404 Not Found')
with self.assertRaises(requests.exceptions.HTTPError):
self.cliente.buscar_usuario(999)
@patch('requests.Session.post')
def test_crear_usuario_datos_validos(self, mock_post):
mock_post.return_value.json.return_value = {
'id': 2, 'nombre': 'Marina'
}
mock_post.return_value.raise_for_status = Mock()
datos = {'nombre': 'Marina', 'email': '[email protected]'}
resultado = self.cliente.crear_usuario(datos)
self.assertEqual(resultado['id'], 2)
mock_post.assert_called_once_with(
'https://api.ejemplo.com/v1/usuarios',
json=datos
)
def test_token_asignado_correctamente(self):
self.assertEqual(
self.cliente.session.headers['Authorization'],
'Bearer token_valido'
)
if name == 'main':
unittest.main()
Este ejemplo demuestra en la práctica los conceptos de setUp, mocks con patch, aserciones y pruebas de diferentes escenarios — éxito, error y validación de estado.
Integración con CI/CD
Las pruebas unitarias son aún más poderosas cuando se ejecutan automáticamente en pipelines de integración continua. Aquí tienes un ejemplo de configuración para GitHub Actions:
# .github/workflows/pruebas.yml
name: Pruebas Python
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- name: Configurar Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Instalar dependencias
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Ejecutar pruebas
run: python -m unittest discover -v
- name: Medir cobertura
run: |
pip install coverage
coverage run -m unittest discover
coverage report
Con esta configuración, tus pruebas se ejecutan automáticamente en múltiples versiones de Python en cada push o pull request, garantizando que los cambios no rompan funcionalidades existentes.
Conclusión
El módulo unittest es una herramienta madura, confiable y poderosa para pruebas unitarias en Python. En esta guía, has aprendido desde la estructura básica de una prueba hasta técnicas avanzadas como mocks, fixtures y organización de suites de prueba.
Dominar las pruebas unitarias es esencial para cualquier desarrollador profesional. No solo previenen regresiones, sino que también sirven como documentación viva del comportamiento esperado del sistema. Invierte tiempo en aprender y aplicar estas técnicas — tu yo del futuro (y tu equipo) te lo agradecerán.
Para continuar tus estudios, consulta la guía completa sobre decoradores en Python, un recurso fundamental para entender cómo @patch y otros decoradores funcionan internamente.
La documentación oficial del módulo unittest es la referencia definitiva para consultas detalladas. La guía oficial de mock también es lectura obligatoria. Para un enfoque práctico complementario, recomiendo el tutorial de Real Python sobre pruebas. La herramienta coverage.py ayuda a medir la eficacia de tus pruebas. El libro Test-Driven Development with Python de Harry Percival es un recurso excelente para aprender TDD en la práctica. Consulta también la Guía del Mochilero para Pruebas en Python para una visión general práctica. Por último, revisa la documentación de doctest para conocer otro enfoque de prueba integrado en Python.