Tabla de Contenidos
Pruebas y fiabilidad
Pruebas unitarias, aislamiento, cobertura e integración continua.
Este módulo forma parte del curso de programación desde cero con Python
Módulo 17. Pruebas y fiabilidad
En el Módulo 10 aprendiste a diseñar casos de prueba y a usar unittest para comprobar que tus funciones hacían lo que debían. Pero las pruebas no son un trámite que se hace una vez y se olvida: son la red de seguridad que te permite cambiar el código sin miedo, detectar regresiones y confiar en que un programa sigue funcionando después de una refactorización (Módulo 16) o de añadir una funcionalidad nueva. Este módulo profundiza en las pruebas automatizadas, en los marcos que Python ofrece (unittest y pytest), en el aislamiento de dependencias con mocks, en la medición de cobertura y en la integración de las pruebas en el ciclo de desarrollo diario.
El objetivo no es que escribas cientos de pruebas, sino que sepas qué probar, cómo aislar lo que pruebas y cómo interpretar los resultados para tomar decisiones informadas sobre la fiabilidad de tu código.
1. Título y alcance
Título: Pruebas y fiabilidad: pruebas unitarias, aislamiento, cobertura e integración continua.
Alcance: Distinguir pruebas manuales de automatizadas; escribir pruebas unitarias con unittest y pytest; parametrizar casos de prueba; usar fixtures para preparar y limpiar el entorno; aislar dependencias externas con unittest.mock; medir la cobertura con coverage.py y pytest-cov; interpretar la cobertura con criterio; y integrar las pruebas en el flujo de desarrollo (ejecución frecuente, regresión, integración continua). Se presupone todo el curso hasta el Módulo 16, en particular funciones (Módulo 9), excepciones (Módulo 11), módulos (Módulo 12) y refactorización (Módulo 16). No se introducen herramientas de CI/CD específicas (se mencionan conceptualmente), ni pruebas de interfaz gráfica o de carga.
2. Objetivos de aprendizaje
Al terminar este módulo serás capaz de:
1. Explicar las limitaciones de las pruebas manuales y las ventajas de las automatizadas.
2. Escribir pruebas unitarias con unittest usando TestCase, assertEqual, assertRaises y assertLogs.
3. Escribir pruebas con pytest usando funciones, assert y el descubrimiento automático.
4. Parametrizar pruebas para cubrir múltiples casos con poco código.
5. Usar fixtures de pytest para preparar datos y recursos, y entender su ciclo de vida.
6. Aislar dependencias externas (APIs, bases de datos, archivos) con unittest.mock y monkeypatch.
7. Medir la cobertura de pruebas con coverage.py o pytest-cov e interpretar el informe.
8. Distinguir entre cobertura de líneas y cobertura de ramas, y evitar la falsa sensación de seguridad.
9. Diseñar pruebas de regresión para errores ya corregidos.
10. Integrar la ejecución de pruebas en el flujo de trabajo diario y en un pipeline de integración continua.
11. Conocer las novedades de Python 3.15 y pytest 9.2 relacionadas con las pruebas.
3. Conocimientos previos
- Del Módulo 9: Funciones, return, docstrings y funciones puras.
- Del Módulo 10: Diseño de casos de prueba (normales, límite, inválidos) y primeras pruebas con unittest.
- Del Módulo 11: Excepciones, try/except y logging (necesario para assertRaises y assertLogs).
- Del Módulo 12: Módulos e importaciones (necesario para organizar pruebas en archivos separados).
- Del Módulo 16: Refactorización y Ruff (contexto para integrar pruebas en el flujo de calidad).
- Ningún conocimiento de pytest, fixtures, mocks ni cobertura.
4. Introducción motivadora: el programa que “funcionaba ayer”
Imagina que has terminado un programa, lo has probado manualmente y todo funciona. Una semana después, cambias una función para añadir una mejora. Vuelves a probar el caso principal: funciona. Pero un caso límite que antes funcionaba ahora falla, y no lo descubres hasta que un usuario se queja. Ese es el problema de las pruebas manuales: no se ejecutan todas cada vez, y olvidan los casos que no se probaron explícitamente.
Las pruebas automatizadas resuelven esto: escribes una vez los casos, y un comando los ejecuta todos en segundos. Si un cambio rompe algo, lo sabes de inmediato, no semanas después. La fiabilidad no consiste en “probar mucho”, sino en probar de forma sistemática y repetible. Este módulo te enseña a construir esa red de seguridad.
5. Explicación paso a paso
5.1. Pruebas manuales y automatizadas
Una prueba manual es la que ejecuta una persona: abre el programa, introduce datos, observa el resultado y decide si es correcto. Es útil para explorar, pero tiene tres problemas: es lenta, no es repetible con fiabilidad (la persona puede olvidar casos) y no escala.
Una prueba automatizada es un programa que ejecuta otro programa (o una función) con datos conocidos y compara el resultado con el esperado. Se ejecuta con un comando, en segundos, y siempre cubre los mismos casos.
[BUENA PRÁCTICA] Automatiza las pruebas de la lógica (funciones puras, cálculos, validaciones). Las pruebas manuales se reservan para la experiencia de usuario (interfaz, flujo de pantallas), que es más difícil de automatizar y cambia con más frecuencia.
5.2. Pruebas unitarias con `unittest`
unittest es el marco de pruebas de la biblioteca estándar. Organiza las pruebas en clases que heredan de unittest.TestCase, y cada método cuyo nombre empiece por test_ es una prueba.
import unittest from calculadora import sumar, dividir class TestCalculadora(unittest.TestCase): def test_sumar_positivos(self): self.assertEqual(sumar(2, 3), 5) def test_dividir_entre_cero(self): with self.assertRaises(ZeroDivisionError): dividir(10, 0) def test_dividir_normal(self): self.assertAlmostEqual(dividir(10, 4), 2.5)
Aserciones más usadas:
| Método | Comprueba |
|---|---|
assertEqual(a, b) | a == b |
assertNotEqual(a, b) | a != b |
assertTrue(x) / assertFalse(x) | Valor booleano |
assertIsNone(x) / assertIsNotNone(x) | Identidad con None |
assertIn(a, b) | Pertenencia |
assertAlmostEqual(a, b) | Igualdad con tolerancia (para float) |
assertRaises(Excepcion) | Que se lance una excepción |
assertLogs(logger, level) | Que se registre un mensaje de log |
[PYTHON] En Python 3.15,unittest.TestCase.assertLogs()acepta un parámetroformatterpara controlar el formato de los mensajes capturados, lo que facilita verificar logs con formato personalizado. Además,assertWarns()yassertWarnsRegex()ya no silencian los warnings que no coinciden con la categoría o expresión regular especificada, y admiten gestores de contexto anidados. Esto puede hacer que tus pruebas muestren warnings que antes se ocultaban; no es un fallo, sino una mayor transparencia.
Ejecutar las pruebas:
- Windows: py -m unittest test_calculadora.py
- Linux: python3 -m unittest test_calculadora.py
5.3. Pruebas con `pytest`
pytest es un marco de pruebas de terceros que se ha convertido en el estándar de facto por su sintaxis más simple. No requiere clases: basta con funciones cuyo nombre empiece por test_, y usa assert directamente.
# archivo: test_calculadora_pytest.py from calculadora import sumar, dividir def test_sumar_positivos(): assert sumar(2, 3) == 5 def test_dividir_entre_cero(): with pytest.raises(ZeroDivisionError): dividir(10, 0)
Instalación: pip install pytest
Ejecución: pytest (descubre automáticamente los archivos test_*.py y *_test.py).
[PYTHON]pytestno forma parte de la biblioteca estándar, pero es la herramienta recomendada por la comunidad para proyectos nuevos. Su salida es más legible, su sistema de fixtures es más potente y su ecosistema de plugins es enorme.unittestsigue siendo útil para código que no puede instalar dependencias o para proyectos heredados.
5.4. Parametrización de pruebas
Si tienes varios casos con la misma lógica, no repitas la función de prueba. pytest ofrece @pytest.mark.parametrize para ejecutar la misma prueba con distintos datos:
import pytest @pytest.mark.parametrize("entrada, esperado", [ (2, 4), (3, 9), (0, 0), (-2, 4), ]) def test_cuadrado(entrada, esperado): assert entrada ** 2 == esperado
[PYTHON] Enpytest9.2.0 (octubre de 2026), los argumentosindirect,idsyscopede@pytest.mark.parametrizeson solo por palabra clave. Pasarlos por posición, que antes producía errores confusos, ahora lanza unTypeErrorclaro. Es un cambio de compatibilidad que conviene conocer si actualizas proyectos antiguos.
5.5. *Fixtures*: preparar y limpiar el entorno
Una fixture es un recurso que se prepara antes de una prueba y se limpia después: una conexión a una base de datos de prueba, un archivo temporal, un objeto configurado. pytest las implementa con funciones decoradas con @pytest.fixture:
import pytest @pytest.fixture def lista_vacia(): return [] @pytest.fixture def lista_con_datos(): return [1, 2, 3] def test_agregar(lista_vacia): lista_vacia.append(4) assert len(lista_vacia) == 1 def test_sumar(lista_con_datos): assert sum(lista_con_datos) == 6
Las fixtures se inyectan como parámetros en las funciones de prueba. Su ámbito (scope) puede ser function (por defecto, se ejecuta para cada prueba), module, class o session.
[BUENA PRÁCTICA] Usa fixtures para recursos que deben crearse y destruirse de forma controlada. Si una prueba modifica un recurso compartido, usascope="function"para evitar que los cambios se filtren a otras pruebas.
5.6. Aislamiento de dependencias con *mocks*
Una prueba unitaria debe probar una unidad de código de forma aislada. Si tu función llama a una API externa, lee un archivo o consulta una base de datos, la prueba se vuelve lenta, frágil y dependiente del entorno. La solución es sustituir esas dependencias por dobles de prueba (mocks).
unittest.mock ofrece Mock y patch para reemplazar objetos durante una prueba:
from unittest.mock import patch from mi_modulo import obtener_precio def test_obtener_precio_sin_red(): with patch("mi_modulo.consultar_api") as mock_api: mock_api.return_value = 100 assert obtener_precio("producto") == 100 mock_api.assert_called_once_with("producto")
patch reemplaza temporalmente consultar_api por un mock. La prueba no hace ninguna llamada real; verifica que la función se comportó como se esperaba y que se llamó con los argumentos correctos.
[PYTHON]pytestofrecemonkeypatchcomo alternativa más ligera para modificar atributos, diccionarios o variables de entorno durante una prueba. Para dependencias externas complejas,unittest.mockes más potente y permite verificar llamadas, argumentos y valores de retorno.
[BUENA PRÁCTICA] No mockees todo. Si tu función es pura (Módulo 9), no necesita mocks. Los mocks son para aislar fronteras del sistema: APIs, bases de datos, archivos, relojes, aleatoriedad. Abusar de ellos hace que las pruebas verifiquen la implementación en lugar del comportamiento.
5.7. Cobertura: medir qué se prueba
La cobertura (coverage) mide qué porcentaje de tu código se ejecuta durante las pruebas. No garantiza que las pruebas sean buenas, pero revela zonas no probadas que podrían contener errores.
Con coverage.py:
pip install coverage coverage run -m pytest coverage report -m
El informe muestra líneas cubiertas, no cubiertas y el porcentaje total. -m añade las líneas exactas que no se ejecutaron.
Con pytest-cov (plugin):
pip install pytest-cov pytest --cov=mi_paquete --cov-report=term-missing
[PYTHON]coverage.py7.15.4 (agosto de 2026) soporta Python 3.10 hasta 3.15, incluida la versión free-threading. Se puede configurar enpyproject.tomlbajo[tool.coverage]. El pluginpytest-covintegra la medición directamente en la ejecución depytesty es la forma más cómoda de obtener cobertura en proyectos que ya usan este marco.
Cobertura de líneas vs. cobertura de ramas: la cobertura de líneas cuenta si una línea se ejecutó alguna vez. La cobertura de ramas comprueba si ambos caminos de una condición (if/else) se han probado. Un 100% de líneas no implica un 100% de ramas: una línea dentro de un if puede ejecutarse, pero la rama else puede no haberse probado nunca. Activa --cov-branch para medir ramas.
[BUENA PRÁCTICA] No persigas el 100% de cobertura como un fin. Un 80-90% bien elegido suele ser más valioso que un 100% obsesivo que prueba líneas triviales. Prioriza cubrir la lógica de negocio, los casos límite y los manejadores de errores. La cobertura es una brújula, no una medalla.
5.8. Pruebas de regresión
Una prueba de regresión es una prueba que se añade después de corregir un error, para asegurar que ese error no vuelva a aparecer. Es una de las prácticas más rentables: cada bug corregido deja una prueba que lo vigila para siempre.
def test_regresion_edad_negativa(): """Regresión: la validación de edad aceptaba valores negativos.""" with pytest.raises(ValueError): validar_edad(-5)
[BUENA PRÁCTICA] Cuando corrijas un error, escribe primero la prueba que lo reproduce (y que falla), y luego la corrección. Así garantizas que la prueba realmente detecta el problema y que la corrección lo resuelve.
5.9. Integración de las pruebas en el desarrollo
Las pruebas no son una fase final: se ejecutan constantemente.
- Durante el desarrollo: ejecuta las pruebas de la función que estás modificando después de cada cambio pequeño. - Antes de un commit: ejecuta toda la suite para asegurarte de que no has roto nada. - En integración continua (CI): un servidor ejecuta las pruebas automáticamente en cada push. Si fallan, el cambio no se integra.
[BUENA PRÁCTICA] Integra las pruebas en tu editor (VS Code, PyCharm) para ejecutarlas con un atajo. Cuanto menor sea la fricción, más probable es que las ejecutes. Y configura Ruff (Módulo 16) junto con las pruebas: el linter detecta problemas de estilo y las pruebas detectan problemas de comportamiento.
6. Ejemplo completo comentado: pruebas de una función con dependencia externa
Código bajo prueba (clima.py):
import requests def obtener_temperatura(ciudad): """Devuelve la temperatura actual de una ciudad usando una API externa.""" respuesta = requests.get(f"https://api.clima.example/{ciudad}") respuesta.raise_for_status() return respuesta.json()["temperatura"]
Prueba con mock (test_clima.py):
from unittest.mock import patch import pytest from clima import obtener_temperatura def test_obtener_temperatura_sin_red(): with patch("clima.requests.get") as mock_get: mock_get.return_value.json.return_value = {"temperatura": 22} mock_get.return_value.raise_for_status.return_value = None assert obtener_temperatura("Sevilla") == 22 mock_get.assert_called_once_with("https://api.clima.example/Sevilla") def test_obtener_temperatura_error_http(): with patch("clima.requests.get") as mock_get: mock_get.return_value.raise_for_status.side_effect = requests.HTTPError with pytest.raises(requests.HTTPError): obtener_temperatura("Sevilla")
Seguimiento: patch sustituye requests.get por un mock. En la primera prueba, el mock devuelve un JSON con la temperatura y se verifica que se llamó con la URL correcta. En la segunda, raise_for_status lanza una excepción, y la prueba comprueba que se propaga. No se hace ninguna llamada real a la red.
7. Seguimiento de la ejecución de una prueba
Cuando ejecutas pytest:
1. Descubre los archivos test_*.py o *_test.py en el directorio actual.
2. Recoge las funciones cuyo nombre empieza por test_.
3. Para cada prueba, prepara las fixtures necesarias.
4. Ejecuta la función. Si un assert falla, la prueba se marca como fallida y se muestra el valor esperado y el obtenido.
5. Limpia las fixtures (si tienen código de limpieza).
6. Al final, muestra un resumen: pruebas pasadas, fallidas, errores y omitidas.
Con unittest, el proceso es similar, pero las pruebas se organizan en clases y se ejecutan con unittest.main() o el descubrimiento automático.
8. Errores habituales
| Error | Causa | Cómo evitarlo |
|---|---|---|
| Probar solo el caso feliz | Olvidar límites y errores. | Diseña casos normales, límite e inválidos (Módulo 10). |
| Pruebas que dependen del orden | Estado compartido entre pruebas. | Usa fixtures con scope="function" y limpia el estado. |
| Pruebas lentas por llamadas reales | No aislar APIs o bases de datos. | Usa mock o monkeypatch para las fronteras del sistema. |
| Perseguir el 100% de cobertura | Métrica como fin, no como medio. | Prioriza lógica de negocio y casos límite. |
| No ejecutar las pruebas con frecuencia | Fricción o pereza. | Intégralas en el editor y en el flujo de commit. |
| Pruebas frágiles que fallan por detalles irrelevantes | Verificar implementación en lugar de comportamiento. | Prueba entradas y salidas, no pasos internos. |
| No añadir pruebas de regresión | Olvidar el bug corregido. | Cada corrección genera una prueba. |
[ERROR HABITUAL] Una prueba que pasa siempre, incluso cuando el código está mal, es peor que no tener prueba: da una falsa sensación de seguridad. Asegúrate de que las pruebas pueden fallar: introduce un error deliberado y comprueba que la prueba lo detecta.
9. Particularidades de Python frente a otros lenguajes
- unittest en la biblioteca estándar: no necesitas instalar nada para empezar. En Java, JUnit es externo; en C, no hay marco estándar.
- pytest como estándar de facto: su sintaxis con assert y funciones es más ligera que JUnit o NUnit. La comunidad lo ha adoptado mayoritariamente para proyectos nuevos.
- unittest.mock: el módulo de mocks está en la biblioteca estándar desde Python 3.3. En otros lenguajes, se requieren bibliotecas externas (Mockito en Java, Moq en C#).
- Fixtures con inyección por parámetro: el sistema de fixtures de pytest es más expresivo que los setUp/tearDown de unittest o JUnit, porque permite composición y ámbitos flexibles.
- Cobertura con coverage.py: es la herramienta de referencia, integrada con pytest mediante pytest-cov. En otros ecosistemas, las herramientas de cobertura suelen estar ligadas al marco de pruebas.
10. Buenas prácticas
Incorrecto (prueba dependiente de la red y del orden):
def test_obtener_datos(): datos = obtener_datos_de_api() # Llamada real assert len(datos) > 0
Mejor (aislada y determinista):
def test_obtener_datos(mocker): mocker.patch("mi_modulo.obtener_datos_de_api", return_value=[1, 2, 3]) datos = obtener_datos() assert datos == [1, 2, 3]
[BUENA PRÁCTICA] Escribe pruebas rápidas, aisladas y deterministas. Una prueba que depende de la red, del tiempo o del orden de ejecución es una prueba frágil que acabará ignorándose. Y recuerda: la cobertura es una herramienta de diagnóstico, no un objetivo en sí mismo.
11. Windows y Linux
- Ejecutar unittest: py -m unittest discover (Windows) o python3 -m unittest discover (Linux).
- Ejecutar pytest: pytest funciona igual en ambos sistemas. Si el comando no se encuentra, usa py -m pytest o python3 -m pytest.
- Cobertura: coverage run -m pytest y coverage report -m son idénticos en ambos sistemas.
- Rutas en pruebas: usa pathlib y tmp_path (fixture de pytest) para crear archivos temporales de forma portable. Evita rutas absolutas codificadas.
- CI: las plataformas de integración continua (GitHub Actions, GitLab CI) ejecutan las pruebas en contenedores Linux por defecto; si necesitas Windows, configura una matriz de sistemas operativos.
[WINDOWS / LINUX] Si una prueba pasa en Linux pero falla en Windows, revisa las diferencias de rutas, finales de línea (\nvs.\r\n) y mayúsculas en nombres de archivo.pathliby las fixtures depytestayudan a evitarlo.
12. Ejercicios guiados
Ejercicio guiado 1: Primera prueba con unittest.
Escribe una función es_mayor_de_edad(edad) y una prueba con unittest que compruebe los casos 17, 18 y 25. Ejecuta la prueba y comprueba que pasa.
Ejercicio guiado 2: Parametrización con pytest.
Escribe una función clasificar_nota(nota) que devuelva "Suspenso", "Aprobado", "Notable" o "Sobresaliente". Escribe una prueba parametrizada que cubra al menos seis casos, incluidos los límites (4.9, 5.0, 6.9, 7.0, 8.9, 9.0).
Ejercicio guiado 3: Mock de una dependencia.
Escribe una función obtener_usuario(id) que llame a consultar_api(id). Escribe una prueba que sustituya consultar_api con un mock y verifique que obtener_usuario devuelve el diccionario esperado y que la API se llamó con el id correcto.
13. Ejercicios autónomos
1. Predicción: ¿Qué muestra pytest si una prueba tiene assert 2 + 2 == 5? ¿Y si tiene assert 2 + 2 == 4? Ejecútalo y observa la salida.
2. Cobertura: Toma una función con varias ramas (if/elif/else). Escribe pruebas que cubran solo una rama y mide la cobertura. Luego añade pruebas para las demás y compara.
3. Fixtures: Crea una fixture que proporcione una lista con tres elementos y úsala en dos pruebas distintas. Comprueba que cada prueba recibe una lista independiente.
4. Regresión: Introduce un error deliberado en una función (por ejemplo, edad > 18 en lugar de edad >= 18). Escribe una prueba que lo detecte. Corrige el error y comprueba que la prueba pasa.
5. Reto opcional: Investiga el parámetro formatter de assertLogs() en Python 3.15 y escribe una prueba que verifique que una función registra un mensaje con un formato concreto.
14. Soluciones razonadas
Solución al ejercicio guiado 1:
import unittest def es_mayor_de_edad(edad): return edad >= 18 class TestMayoriaEdad(unittest.TestCase): def test_menor(self): self.assertFalse(es_mayor_de_edad(17)) def test_limite(self): self.assertTrue(es_mayor_de_edad(18)) def test_mayor(self): self.assertTrue(es_mayor_de_edad(25))
Solución al ejercicio guiado 2:
import pytest def clasificar_nota(nota): if nota < 5: return "Suspenso" elif nota < 7: return "Aprobado" elif nota < 9: return "Notable" return "Sobresaliente" @pytest.mark.parametrize("nota, esperado", [ (4.9, "Suspenso"), (5.0, "Aprobado"), (6.9, "Aprobado"), (7.0, "Notable"), (8.9, "Notable"), (9.0, "Sobresaliente"), ]) def test_clasificar(nota, esperado): assert clasificar_nota(nota) == esperado
Solución al ejercicio guiado 3:
from unittest.mock import patch from usuarios import obtener_usuario def test_obtener_usuario(): with patch("usuarios.consultar_api") as mock_api: mock_api.return_value = {"id": 1, "nombre": "Ana"} assert obtener_usuario(1) == {"id": 1, "nombre": "Ana"} mock_api.assert_called_once_with(1)
Solución al ejercicio 1 (predicción): La primera prueba falla y pytest muestra assert 4 == 5 con los valores esperado y obtenido. La segunda pasa y se marca como PASSED.
15. Resumen
- Las pruebas automatizadas son repetibles, rápidas y no dependen de la memoria del programador.
- unittest es el marco estándar; pytest es el estándar de facto por su sintaxis simple y su sistema de fixtures.
- La parametrización evita duplicar pruebas para casos similares.
- Las fixtures preparan y limpian recursos; los mocks aíslan dependencias externas.
- La cobertura mide qué código se ejecuta; es una brújula, no una medalla. Distingue líneas de ramas.
- Las pruebas de regresión protegen contra la reaparición de errores ya corregidos.
- Las pruebas se integran en el desarrollo diario y en la integración continua.
- Python 3.15 mejora assertLogs (formatter) y assertWarns (no silencia warnings no coincidentes); pytest 9.2 hace que parametrize sea keyword-only en sus argumentos avanzados.
16. Autoevaluación
1. ¿Por qué una prueba que llama a una API real es frágil? ¿Cómo la aislarías?
2. ¿Qué diferencia hay entre cobertura de líneas y cobertura de ramas? ¿Cuál es más informativa?
3. ¿Cuándo usarías unittest en lugar de pytest?
4. ¿Qué es una prueba de regresión y por qué es rentable?
5. Reto: Sin ejecutar, predice qué ocurre si una prueba tiene dos assert y el primero falla. ¿Se ejecuta el segundo? Justifica la respuesta.
17. Fuentes oficiales
- Documentación oficial de unittest: https://docs.python.org/3.15/library/unittest.html
- Documentación oficial de unittest.mock: https://docs.python.org/3.15/library/unittest.mock.html
- Novedades de Python 3.15 (unittest): https://docs.python.org/3/whatsnew/3.15.html
- Documentación de pytest: https://docs.pytest.org/
- Changelog de pytest (9.2.0, octubre 2026): https://docs.pytest.org/en/stable/changelog.html
- Documentación de coverage.py: https://coverage.readthedocs.io/
- Documentación de pytest-cov: https://pytest-cov.readthedocs.io/
- Guía de configuración de cobertura en pyproject.toml: https://coverage.readthedocs.io/en/7.15.4/config.html
