Python unittest: Escribe y ejecuta pruebas unitarias (Guía completa)
Updated on
Escribes una función que procesa datos de usuarios. Funciona cuando la pruebas manualmente con unas pocas entradas. Luego, un compañero cambia una función auxiliar de la que depende, y tu código devuelve resultados incorrectos en silencio durante tres semanas antes de que alguien lo note. El bug llega a producción. Los registros de clientes se corrompen. La causa raíz fue un cambio de una sola línea que nadie verificó contra el comportamiento existente. Este es exactamente el modo de fallo que las pruebas unitarias evitan. Python incluye unittest en la librería estándar: un framework de pruebas completo que detecta regresiones antes de que lleguen a producción, con cero dependencias de terceros.
¿Qué es unittest y por qué usarlo?
unittest es el framework de pruebas integrado de Python, modelado a partir de JUnit de Java. Forma parte de la librería estándar desde Python 2.1, lo que significa que cualquier instalación de Python ya lo trae. Sin pip install. Sin gestión de dependencias. Escribes clases de test, defines métodos de test y los ejecutas desde la línea de comandos.
Las pruebas unitarias verifican que piezas individuales del código (funciones, métodos, clases) se comporten correctamente en aislamiento. Cuando cada unidad funciona por sí sola, integrarlas tiene muchas menos probabilidades de producir bugs ocultos.
Esto es lo que unittest ofrece de fábrica:
- Clases de casos de prueba (Test case) con descubrimiento automático de tests
- Métodos de aserción ricos (igualdad, veracidad, excepciones, warnings)
- Hooks setUp/tearDown tanto a nivel de método como de clase
- Suites de pruebas para organizar y agrupar tests
- Mocking vía
unittest.mock(añadido en Python 3.3) - Runner de tests por línea de comandos con controles de verbosidad
Tu primera prueba unitaria
Empieza con una función a probar y una clase de test correspondiente.
# calculator.py
def add(a, b):
return a + b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b# test_calculator.py
import unittest
from calculator import add, divide
class TestCalculator(unittest.TestCase):
def test_add_positive_numbers(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative_numbers(self):
self.assertEqual(add(-1, -1), -2)
def test_add_zero(self):
self.assertEqual(add(0, 0), 0)
def test_divide_normal(self):
self.assertEqual(divide(10, 2), 5.0)
def test_divide_by_zero_raises(self):
with self.assertRaises(ValueError):
divide(10, 0)
if __name__ == "__main__":
unittest.main()Ejecútalo:
python -m unittest test_calculator.py -vSalida:
test_add_negative_numbers (test_calculator.TestCalculator) ... ok
test_add_positive_numbers (test_calculator.TestCalculator) ... ok
test_add_zero (test_calculator.TestCalculator) ... ok
test_divide_by_zero_raises (test_calculator.TestCalculator) ... ok
test_divide_normal (test_calculator.TestCalculator) ... ok
----------------------------------------------------------------------
Ran 5 tests in 0.001s
OKCada método cuyo nombre empieza con test_ se detecta y ejecuta automáticamente. La clase hereda de unittest.TestCase, que proporciona todos los métodos de aserción.
Referencia de métodos de aserción
unittest.TestCase incluye un conjunto completo de métodos de aserción. Cada uno produce un mensaje de fallo claro cuando la comprobación no pasa.
Aserciones de igualdad e identidad
| Method | Checks | Example |
|---|---|---|
assertEqual(a, b) | a == b | self.assertEqual(result, 42) |
assertNotEqual(a, b) | a != b | self.assertNotEqual(result, 0) |
assertIs(a, b) | a is b | self.assertIs(singleton, instance) |
assertIsNot(a, b) | a is not b | self.assertIsNot(obj1, obj2) |
assertIsNone(x) | x is None | self.assertIsNone(result) |
assertIsNotNone(x) | x is not None | self.assertIsNotNone(user) |
Aserciones booleanas y de pertenencia
| Method | Checks | Example |
|---|---|---|
assertTrue(x) | bool(x) is True | self.assertTrue(is_valid) |
assertFalse(x) | bool(x) is False | self.assertFalse(has_errors) |
assertIn(a, b) | a in b | self.assertIn("admin", roles) |
assertNotIn(a, b) | a not in b | self.assertNotIn("deleted", status) |
assertIsInstance(a, b) | isinstance(a, b) | self.assertIsInstance(result, dict) |
Aserciones numéricas y de colecciones
| Method | Checks | Example |
|---|---|---|
assertAlmostEqual(a, b) | round(a-b, 7) == 0 | self.assertAlmostEqual(0.1 + 0.2, 0.3) |
assertGreater(a, b) | a > b | self.assertGreater(len(results), 0) |
assertLess(a, b) | a < b | self.assertLess(latency, 1.0) |
assertCountEqual(a, b) | Mismos elementos, cualquier orden | self.assertCountEqual([3,1,2], [1,2,3]) |
assertListEqual(a, b) | Las listas son iguales | self.assertListEqual(result, expected) |
assertDictEqual(a, b) | Los diccionarios son iguales | self.assertDictEqual(config, defaults) |
Aserciones de excepciones y warnings
import unittest
import warnings
class TestExceptions(unittest.TestCase):
def test_raises_value_error(self):
"""assertRaises checks that the exception is raised."""
with self.assertRaises(ValueError):
int("not_a_number")
def test_raises_with_message(self):
"""assertRaisesRegex checks both the exception and its message."""
with self.assertRaisesRegex(ValueError, "invalid literal"):
int("not_a_number")
def test_warns_deprecation(self):
"""assertWarns checks that a warning is issued."""
with self.assertWarns(DeprecationWarning):
warnings.warn("old function", DeprecationWarning)Prefiere siempre aserciones específicas en lugar de assertTrue. En vez de self.assertTrue(result == 42), usa self.assertEqual(result, 42). La versión específica produce un mensaje de fallo claro como 42 != 41, mientras que assertTrue solo dice False is not true.
setUp y tearDown: Fixtures de test
La mayoría de los tests necesitan algún estado inicial: una conexión a base de datos, un archivo temporal o un objeto preconfigurado. Los métodos setUp y tearDown se ejecutan antes y después de cada método de test, dando a cada test un punto de partida limpio.
import unittest
import os
import tempfile
class TestFileProcessor(unittest.TestCase):
def setUp(self):
"""Runs before EACH test method."""
self.test_dir = tempfile.mkdtemp()
self.test_file = os.path.join(self.test_dir, "data.txt")
with open(self.test_file, "w") as f:
f.write("line1\nline2\nline3\n")
def tearDown(self):
"""Runs after EACH test method."""
os.remove(self.test_file)
os.rmdir(self.test_dir)
def test_read_lines(self):
with open(self.test_file, "r") as f:
lines = f.readlines()
self.assertEqual(len(lines), 3)
def test_file_exists(self):
self.assertTrue(os.path.exists(self.test_file))
def test_first_line_content(self):
with open(self.test_file, "r") as f:
first_line = f.readline().strip()
self.assertEqual(first_line, "line1")Cada método de test recibe su propia llamada a setUp. Si test_read_lines modifica el archivo, test_first_line_content seguirá viendo el contenido original porque setUp lo recrea.
setUpClass y tearDownClass: Preparación única
Algunos recursos son costosos de crear: conexiones a bases de datos, fixtures grandes de datos, procesos de servidor. Usa setUpClass y tearDownClass para crearlos una sola vez para toda la clase de test.
import unittest
import sqlite3
class TestDatabase(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Runs ONCE before all tests in this class."""
cls.conn = sqlite3.connect(":memory:")
cls.cursor = cls.conn.cursor()
cls.cursor.execute("""
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
""")
cls.cursor.executemany(
"INSERT INTO users (name, email) VALUES (?, ?)",
[
("Alice", "alice@example.com"),
("Bob", "bob@example.com"),
("Charlie", "charlie@example.com"),
],
)
cls.conn.commit()
@classmethod
def tearDownClass(cls):
"""Runs ONCE after all tests in this class."""
cls.conn.close()
def test_user_count(self):
self.cursor.execute("SELECT COUNT(*) FROM users")
count = self.cursor.fetchone()[0]
self.assertEqual(count, 3)
def test_find_user_by_email(self):
self.cursor.execute(
"SELECT name FROM users WHERE email = ?",
("bob@example.com",),
)
name = self.cursor.fetchone()[0]
self.assertEqual(name, "Bob")
def test_unique_emails(self):
with self.assertRaises(sqlite3.IntegrityError):
self.cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
("Duplicate", "alice@example.com"),
)| Hook | Runs | Decorator | Use Case |
|---|---|---|---|
setUp | Antes de cada método de test | None | Crear objetos nuevos, resetear estado |
tearDown | Después de cada método de test | None | Limpiar archivos, resetear mocks |
setUpClass | Una vez antes de todos los tests de la clase | @classmethod | Conexiones a DB, fixtures costosas |
tearDownClass | Una vez después de todos los tests de la clase | @classmethod | Cerrar conexiones, borrar recursos compartidos |
Mocking con unittest.mock
Las aplicaciones reales dependen de bases de datos, APIs, sistemas de archivos y servicios de red. No quieres que tus pruebas unitarias llamen a una API de producción ni requieran una base de datos en ejecución. unittest.mock sustituye esas dependencias por reemplazos controlados.
Objeto Mock básico
from unittest.mock import Mock
# Create a mock object
api_client = Mock()
# Configure return values
api_client.get_user.return_value = {"id": 1, "name": "Alice"}
# Use it like a real object
user = api_client.get_user(user_id=1)
print(user) # {'id': 1, 'name': 'Alice'}
# Verify it was called correctly
api_client.get_user.assert_called_once_with(user_id=1)Patching con @patch
El decorador @patch reemplaza un objeto en un módulo específico durante la ejecución de un test. Esta es la herramienta principal para aislar unidades de sus dependencias.
# user_service.py
import requests
def get_user_name(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status()
return response.json()["name"]# test_user_service.py
import unittest
from unittest.mock import patch, Mock
from user_service import get_user_name
class TestUserService(unittest.TestCase):
@patch("user_service.requests.get")
def test_get_user_name_success(self, mock_get):
"""Mock the requests.get call to avoid hitting the real API."""
mock_response = Mock()
mock_response.json.return_value = {"id": 1, "name": "Alice"}
mock_response.raise_for_status.return_value = None
mock_get.return_value = mock_response
result = get_user_name(1)
self.assertEqual(result, "Alice")
mock_get.assert_called_once_with("https://api.example.com/users/1")
@patch("user_service.requests.get")
def test_get_user_name_http_error(self, mock_get):
"""Verify that HTTP errors propagate correctly."""
import requests
mock_get.return_value.raise_for_status.side_effect = (
requests.exceptions.HTTPError("404 Not Found")
)
with self.assertRaises(requests.exceptions.HTTPError):
get_user_name(999)Patching como context manager
import unittest
from unittest.mock import patch
class TestConfig(unittest.TestCase):
def test_reads_environment_variable(self):
with patch.dict("os.environ", {"DATABASE_URL": "sqlite:///test.db"}):
import os
self.assertEqual(os.environ["DATABASE_URL"], "sqlite:///test.db")MagicMock vs Mock
MagicMock es una subclase de Mock que preconfigura métodos mágicos (__len__, __iter__, __getitem__, etc.). Usa MagicMock cuando el código bajo prueba llama a métodos dunder en el objeto mockeado.
from unittest.mock import MagicMock
mock_list = MagicMock()
mock_list.__len__.return_value = 5
mock_list.__getitem__.return_value = "item"
print(len(mock_list)) # 5
print(mock_list[0]) # itemside_effect para comportamiento complejo
side_effect permite que un mock lance excepciones, devuelva valores distintos en llamadas sucesivas o ejecute una función personalizada.
from unittest.mock import Mock
# Raise an exception
mock_db = Mock()
mock_db.connect.side_effect = ConnectionError("Database unreachable")
# Return different values on successive calls
mock_api = Mock()
mock_api.fetch.side_effect = [{"page": 1}, {"page": 2}, StopIteration]
print(mock_api.fetch()) # {'page': 1}
print(mock_api.fetch()) # {'page': 2}
# Custom function
def validate_input(x):
if x < 0:
raise ValueError("Negative input")
return x * 2
mock_fn = Mock(side_effect=validate_input)
print(mock_fn(5)) # 10Descubrimiento de tests (Test Discovery)
No necesitas listar manualmente cada archivo de tests. El descubrimiento de tests de Python encuentra y ejecuta todos los tests que siguen la convención de nombres.
# Discover and run all tests in the current directory tree
python -m unittest discover
# Specify a start directory and pattern
python -m unittest discover -s tests -p "test_*.py"
# Verbose output
python -m unittest discover -vEl descubrimiento de tests busca archivos que coincidan con test_*.py (patrón por defecto), los importa y ejecuta cualquier clase que herede de unittest.TestCase.
Estructura de proyecto recomendada
my_project/
src/
calculator.py
user_service.py
utils.py
tests/
__init__.py
test_calculator.py
test_user_service.py
test_utils.py
setup.pyEjecuta todos los tests desde la raíz del proyecto:
python -m unittest discover -s tests -vOrganizar tests con test suites
Para un control más fino sobre qué tests se ejecutan, crea suites de tests manualmente.
import unittest
from test_calculator import TestCalculator
from test_user_service import TestUserService
def fast_tests():
"""Suite of tests that run quickly (no I/O, no network)."""
suite = unittest.TestSuite()
suite.addTest(TestCalculator("test_add_positive_numbers"))
suite.addTest(TestCalculator("test_add_negative_numbers"))
suite.addTest(TestCalculator("test_divide_normal"))
return suite
def all_tests():
"""Full test suite."""
loader = unittest.TestLoader()
suite = unittest.TestSuite()
suite.addTests(loader.loadTestsFromTestCase(TestCalculator))
suite.addTests(loader.loadTestsFromTestCase(TestUserService))
return suite
if __name__ == "__main__":
runner = unittest.TextTestRunner(verbosity=2)
runner.run(fast_tests())Omitir tests y fallos esperados
A veces un test solo debería ejecutarse bajo ciertas condiciones: un SO específico, una versión concreta de Python o cuando un servicio externo está disponible.
import unittest
import sys
class TestPlatformSpecific(unittest.TestCase):
@unittest.skip("Temporarily disabled while refactoring")
def test_feature_under_construction(self):
pass
@unittest.skipIf(sys.platform == "win32", "Not supported on Windows")
def test_unix_permissions(self):
import os
self.assertTrue(os.access("/tmp", os.W_OK))
@unittest.skipUnless(sys.platform.startswith("linux"), "Linux only")
def test_proc_filesystem(self):
import os
self.assertTrue(os.path.exists("/proc"))
@unittest.expectedFailure
def test_known_bug(self):
"""This test documents a known bug. It SHOULD fail."""
self.assertEqual(1 + 1, 3)| Decorator | Effect |
|---|---|
@unittest.skip(reason) | Omitir siempre este test |
@unittest.skipIf(condition, reason) | Omitir si la condición es True |
@unittest.skipUnless(condition, reason) | Omitir a menos que la condición sea True |
@unittest.expectedFailure | Marcar como fallo esperado; se reporta como error si pasa |
Tests parametrizados con subTest
Es común probar la misma lógica con distintas entradas. En lugar de escribir métodos de test separados para cada caso, usa subTest para ejecutar aserciones parametrizadas dentro de un solo método.
import unittest
def is_palindrome(s):
cleaned = s.lower().replace(" ", "")
return cleaned == cleaned[::-1]
class TestPalindrome(unittest.TestCase):
def test_palindromes(self):
test_cases = [
("racecar", True),
("hello", False),
("A man a plan a canal Panama", True),
("", True),
("ab", False),
("madam", True),
("Nurses Run", True),
]
for text, expected in test_cases:
with self.subTest(text=text):
self.assertEqual(is_palindrome(text), expected)Con subTest, un fallo en un caso no detiene la ejecución de los demás. La salida identifica exactamente qué subcaso falló:
FAIL: test_palindromes (test_palindrome.TestPalindrome) (text='Nurses Run')
AssertionError: False != TrueSin subTest, el primer fallo abortaría todo el método y no sabrías qué otros casos también fallan.
unittest vs pytest vs doctest
Python tiene tres herramientas de testing integradas o de uso común. Cada una sirve a un propósito distinto.
| Feature | unittest | pytest | doctest |
|---|---|---|---|
| Included in stdlib | Yes | No (pip install) | Yes |
| Test style | Class-based (TestCase) | Function-based (plain def test_) | Embedded in docstrings |
| Assertions | self.assertEqual, self.assertTrue, etc. | Plain assert statement | Expected output matching |
| Fixtures | setUp/tearDown, setUpClass | @pytest.fixture with dependency injection | None |
| Parameterized tests | subTest (limited) | @pytest.mark.parametrize (powerful) | One example per docstring |
| Mocking | unittest.mock (built-in) | unittest.mock + monkeypatch | Not applicable |
| Test discovery | python -m unittest discover | pytest (auto-discovers) | python -m doctest module.py |
| Output on failure | Basic diff | Detailed diff with context | Shows expected vs actual output |
| Plugins | None | 1000+ plugins (coverage, fixtures, etc.) | None |
| Learning curve | Moderate (OOP patterns) | Low (plain functions) | Very low |
| Best for | Standard library projects, no extra dependencies | Most Python projects, complex test setups | Simple examples in documentation |
Cuándo elegir unittest:
- Quieres cero dependencias externas
- Tu organización o proyecto ya usa unittest
- Necesitas organización de tests basada en clases
- Quieres
unittest.mocksin configuración adicional
Cuándo elegir pytest:
- Quieres sintaxis más simple y mejor salida de fallos
- Necesitas parametrización o fixtures potentes
- Dependiendo del ecosistema de plugins de pytest (coverage, async, Django, etc.)
Cuándo elegir doctest:
- Quieres verificar que los ejemplos de código en la documentación siguen funcionando
- Los tests son pares simples de entrada/salida
Ten en cuenta que pytest puede ejecutar tests estilo unittest sin modificaciones. Muchos equipos empiezan con unittest y cambian a pytest como runner manteniendo sus clases de test existentes.
Probar una clase del mundo real: ejemplo completo
Aquí tienes un ejemplo completo probando una implementación de carrito de compras.
# cart.py
class Product:
def __init__(self, name, price):
if price < 0:
raise ValueError("Price cannot be negative")
self.name = name
self.price = price
class ShoppingCart:
def __init__(self):
self.items = []
def add(self, product, quantity=1):
if quantity <= 0:
raise ValueError("Quantity must be positive")
self.items.append({"product": product, "quantity": quantity})
def total(self):
return sum(
item["product"].price * item["quantity"]
for item in self.items
)
def remove(self, product_name):
self.items = [
item for item in self.items
if item["product"].name != product_name
]
def item_count(self):
return sum(item["quantity"] for item in self.items)# test_cart.py
import unittest
from cart import Product, ShoppingCart
class TestProduct(unittest.TestCase):
def test_create_product(self):
p = Product("Widget", 9.99)
self.assertEqual(p.name, "Widget")
self.assertAlmostEqual(p.price, 9.99)
def test_negative_price_raises(self):
with self.assertRaises(ValueError):
Product("Bad", -5.00)
class TestShoppingCart(unittest.TestCase):
def setUp(self):
self.cart = ShoppingCart()
self.apple = Product("Apple", 1.50)
self.bread = Product("Bread", 3.00)
def test_empty_cart_total(self):
self.assertEqual(self.cart.total(), 0)
def test_add_single_item(self):
self.cart.add(self.apple)
self.assertEqual(self.cart.item_count(), 1)
self.assertAlmostEqual(self.cart.total(), 1.50)
def test_add_multiple_items(self):
self.cart.add(self.apple, quantity=3)
self.cart.add(self.bread, quantity=2)
self.assertEqual(self.cart.item_count(), 5)
self.assertAlmostEqual(self.cart.total(), 10.50)
def test_remove_item(self):
self.cart.add(self.apple, quantity=2)
self.cart.add(self.bread)
self.cart.remove("Apple")
self.assertEqual(self.cart.item_count(), 1)
self.assertAlmostEqual(self.cart.total(), 3.00)
def test_remove_nonexistent_item(self):
self.cart.add(self.apple)
self.cart.remove("Nonexistent")
self.assertEqual(self.cart.item_count(), 1)
def test_add_zero_quantity_raises(self):
with self.assertRaises(ValueError):
self.cart.add(self.apple, quantity=0)
def test_add_negative_quantity_raises(self):
with self.assertRaises(ValueError):
self.cart.add(self.apple, quantity=-1)
if __name__ == "__main__":
unittest.main()Buenas prácticas para unittest
1. Una aserción por concepto
Cada método de test debería verificar un concepto lógico. Varias aserciones están bien si todas comprueban aspectos distintos de la misma operación.
# GOOD -- multiple assertions about the same operation
def test_user_creation(self):
user = create_user("alice", "alice@example.com")
self.assertEqual(user.name, "alice")
self.assertEqual(user.email, "alice@example.com")
self.assertIsNotNone(user.id)
# BAD -- testing unrelated things in one method
def test_everything(self):
user = create_user("alice", "alice@example.com")
self.assertEqual(user.name, "alice")
users = list_all_users()
self.assertGreater(len(users), 0) # Unrelated assertion2. Usa nombres de test descriptivos
Los nombres de los tests deben describir el escenario y el resultado esperado. Cuando un test falla en CI, el nombre por sí solo debería decirte qué salió mal.
# GOOD
def test_divide_by_zero_raises_value_error(self):
...
def test_empty_cart_returns_zero_total(self):
...
# BAD
def test_divide(self):
...
def test1(self):
...3. Los tests deben ser independientes
Ningún test debe depender de la salida de otro test ni del orden de ejecución. Cada test debe preparar su propio estado y limpiar después.
4. Mantén los tests rápidos
Las pruebas unitarias deberían ejecutarse en milisegundos. Si un test necesita una base de datos, haz mock. Si necesita una API, haz mock. Reserva las pruebas de integración lentas para una suite aparte.
5. Prueba casos límite
Prueba siempre condiciones de frontera: entradas vacías, valores cero, None, entradas muy grandes y tipos inválidos.
def test_edge_cases(self):
test_cases = [
([], 0), # empty list
([0], 0), # single zero
([-1, -2], -3), # all negative
([999999999], 999999999), # large number
]
for inputs, expected in test_cases:
with self.subTest(inputs=inputs):
self.assertEqual(sum(inputs), expected)6. No pruebes detalles de implementación
Prueba la interfaz pública y el comportamiento, no el estado interno ni métodos privados. Si refactorizas los internos, tus tests deberían seguir pasando.
Ejecutar tests desde la línea de comandos
# Run a specific test file
python -m unittest test_calculator.py
# Run a specific test class
python -m unittest test_calculator.TestCalculator
# Run a specific test method
python -m unittest test_calculator.TestCalculator.test_add_positive_numbers
# Verbose output (shows each test name and result)
python -m unittest -v
# Discover all tests in a directory
python -m unittest discover -s tests -p "test_*.py" -v
# Stop on first failure (fail-fast mode)
python -m unittest -fEscribir y depurar tests con RunCell
Cuando desarrollas en notebooks de Jupyter —común en data science y programación exploratoria— escribir y ejecutar pruebas unitarias se siente incómodo. Los notebooks ejecutan celdas de forma interactiva, pero unittest espera módulos y runners de test. Terminas copiando código entre notebooks y archivos de test, perdiendo el bucle de feedback interactivo.
RunCell (opens in a new tab) es un agente de IA diseñado para Jupyter que cierra esa brecha. Puede generar casos de prueba compatibles con unittest para funciones que defines en celdas del notebook, ejecutarlos dentro del entorno del notebook y explicar fallos con contexto. Si un mock está configurado incorrectamente o falla una aserción, RunCell inspecciona las variables en vivo y te muestra cuáles eran los valores reales, no solo el mensaje de error de la aserción. Para pipelines de datos donde necesitas verificar que transformaciones de DataFrame producen la forma y valores correctos, RunCell puede crear la estructura del test y las aserciones para que te enfoques en la lógica en lugar del boilerplate.
FAQ
¿Cuál es la diferencia entre unittest y pytest?
unittest es el framework de pruebas integrado de Python con una API basada en clases. pytest es un framework de terceros que usa funciones normales y sentencias assert. pytest tiene un ecosistema de plugins más rico y mejor salida de fallos, pero requiere instalación. unittest funciona en cualquier lugar donde se ejecute Python sin dependencias adicionales.
¿Cómo ejecuto un único método de test en unittest?
Usa el comando python -m unittest test_module.TestClass.test_method. Por ejemplo: python -m unittest test_calculator.TestCalculator.test_add_positive_numbers. Esto ejecuta solo el método especificado sin ejecutar otros tests del archivo.
¿Cuál es la diferencia entre setUp y setUpClass?
setUp se ejecuta antes de cada método de test individual, creando un estado nuevo cada vez. setUpClass se ejecuta una vez antes de todos los tests en la clase y es un @classmethod. Usa setUpClass para preparación costosa como conexiones a bases de datos. Usa setUp para estado ligero por test.
¿Cómo hago mock de una API externa en unittest?
Usa unittest.mock.patch para reemplazar la función que llama a la API. Haz patch a la ruta de importación donde se usa la función, no donde está definida. Por ejemplo, si user_service.py importa requests.get, haz patch a user_service.requests.get, no a requests.get.
¿Puede pytest ejecutar tests de unittest?
Sí. pytest es totalmente compatible con clases de test estilo unittest. Puedes ejecutar pytest en un proyecto que use clases unittest.TestCase sin ninguna modificación. Esto hace que la migración sea gradual: puedes escribir tests nuevos en estilo pytest mientras mantienes los tests existentes de unittest.
¿Cómo pruebo que una función lanza una excepción?
Usa self.assertRaises(ExceptionType) como context manager. El test pasa si el código dentro del bloque with lanza la excepción especificada y falla si no se lanza ninguna excepción o se lanza una distinta. Usa assertRaisesRegex para verificar también el mensaje de la excepción.
Conclusión
El framework unittest de Python es un toolkit de testing completo que viene con cada instalación de Python. Proporciona clases de casos de prueba, métodos de aserción ricos, hooks de setup y teardown tanto a nivel de método como de clase, capacidades de mocking vía unittest.mock y descubrimiento de tests integrado. No necesitas instalar nada para empezar a escribir pruebas fiables.
Los fundamentos son sencillos: hereda de TestCase, nombra tus métodos con el prefijo test_, usa métodos de aserción específicos y ejecuta con python -m unittest. A medida que tu proyecto crece, añade setUp/tearDown para aislamiento de tests, @patch para hacer mock de dependencias externas y subTest para pruebas parametrizadas. Organiza los tests en un directorio tests/ y deja que el descubrimiento de tests se encargue del resto.
Escribir tests lleva tiempo al principio. Ahorra mucho más tiempo después. Cada regresión detectada por una prueba unitaria es un incidente de producción que nunca ocurrió, una queja de cliente que nunca llegó y una sesión de depuración que nunca empezó. Tanto si te quedas con unittest como si finalmente te pasas a pytest, los hábitos de testing que construyas alrededor de los patrones de unittest —aislamiento, aserciones claras, dependencias mockeadas y cobertura completa de casos límite— se aplican de forma universal al testing en Python.