Con la v0.20 el proyecto ya tiene historial: puedes volver a cualquier punto anterior y sabes quién cambió qué y por qué. Pero Git no responde a la pregunta que de verdad importa cada vez que tocas una línea: ¿sigue funcionando todo lo demás? Hasta ahora la respuesta la dabas a mano, arrancando python -m tareafacil, creando una tarea, marcándola, filtrando, guardando y saliendo, opción por opción. Eso cuesta cinco minutos, se hace mal a la tercera vez y se acaba omitiendo justo el día en que hacía falta. Esta lección construye la red de seguridad que falta: pruebas automatizadas que comprueban solas, en menos de un segundo, que el programa hace lo que debe. Verás el patrón que siguen todas las pruebas, las dos herramientas del ecosistema Python —unittest, que viene incluida, y pytest, la que usa todo el mundo—, qué merece la pena probar y qué no, y por qué la separación entre entrada/salida y lógica que hicimos en 04-04 era, en realidad, la preparación para este momento.
Contenido
- Por qué probar
- Tipos de prueba
- Qué hace probable a un código
- El patrón AAA y el nombre del test
assert: la primera pruebaunittest, la biblioteca estándarpytest, la herramienta del ecosistemaunittestfrente apytest- Qué probar y qué no
- Cobertura
- TDD: rojo, verde, refactorizar
- TareaFácil v0.21: la carpeta
tests/ - Errores comunes y consejos
- Ejercicios
- Conclusión
- Por qué probar
Una prueba automatizada es simplemente código que ejecuta tu código y comprueba que el resultado es el esperado. Suena a trabajo extra, y lo es la primera vez; deja de serlo en cuanto sumas lo que cuesta lo contrario:
- Repasar el menú a mano cuesta minutos en cada cambio, y se hace peor con el tiempo: al principio compruebas los diez casos, a la décima vez solo el que has tocado, y el fallo aparece justo en otro.
- El miedo paraliza: sin pruebas, mejorar código que ya funciona parece una temeridad, así que nadie lo mejora y el proyecto se pudre.
- Una prueba es documentación que no miente:
test_completar_iguala_hechos_a_diasexplica el comportamiento esperado mejor que cualquier párrafo, y si el comportamiento cambia, la prueba falla.
La palabra clave es regresión: un fallo que aparece en algo que antes funcionaba. Las pruebas automatizadas son la única defensa práctica contra ellas, y son lo que permite refactorizar sin miedo, que es exactamente lo que haremos en Estilo y refactorización.
- Tipos de prueba
| Tipo | Qué comprueba | Tamaño y velocidad | Ejemplo en TareaFácil |
|---|---|---|---|
| Unitaria | Una pieza aislada: una función o un método | Muy rápida, milisegundos | Tarea.progreso() devuelve 50.0 con 2 de 4 días |
| De integración | Que varias piezas encajan entre sí | Más lenta | Guardar la agenda en JSON y volver a cargarla |
| De sistema | El programa entero, como lo usa el usuario | Lenta y frágil | Recorrer el menú simulando pulsaciones |
Este curso se centra en las unitarias, con alguna de integración, y por una razón práctica: son rápidas, señalan con precisión qué está roto y no dependen de la interfaz. Una prueba de sistema que recorre el menú se rompe en cuanto cambias un texto, y cuando falla no dice dónde está el problema. La proporción sana en cualquier proyecto es la misma: muchas unitarias, algunas de integración, muy pocas de sistema.
- Qué hace probable a un código
Aquí se cobra la deuda de Descomponer un programa en funciones, donde separamos la entrada y la salida de la lógica y dijimos que eso permitiría probar el código. Compara estas dos versiones:
# NO SE PUEDE PROBAR BIEN: pide por teclado e imprime
def calcular_progreso():
hechos = int(input("Dias hechos: "))
dias = int(input("Dias totales: "))
print(f"Progreso: {hechos / dias * 100:.1f}%")
# SI SE PUEDE PROBAR: recibe datos y devuelve un valor
def progreso(hechos: int, dias: int) -> float:
"""Devuelve el porcentaje de avance, entre 0.0 y 100.0."""
return min(100.0, hechos / dias * 100)La primera es imposible de probar de forma razonable: para ejecutarla hay que simular el teclado, y para comprobar el resultado hay que capturar lo que imprime. La segunda se prueba en una línea: progreso(2, 4) == 50.0. La regla que se deduce es la que ya conoces con otro nombre: las funciones que reciben datos y devuelven resultados son probables; las que hablan con el mundo, no. Por eso modelo.py y agenda.py no tienen un solo print, y por eso todo lo que se pueda comprobar vive ahí y no en interfaz.py.
- El patrón AAA y el nombre del test
Todas las pruebas del mundo tienen la misma estructura de tres pasos, conocida como AAA:
- Arrange (preparar): crear los datos y los objetos que necesita la prueba.
- Act (actuar): ejecutar exactamente una cosa, la que se está probando.
- Assert (comprobar): verificar que el resultado es el esperado.
def test_completar_marca_la_tarea_y_iguala_los_dias():
tarea = Tarea("Cartel feria del libro", "luis", "alta", 4) # Arrange
tarea.completar() # Act
assert tarea.completada is True # Assert
assert tarea.hechos == 4Y el nombre importa tanto como el contenido, porque es lo único que verás cuando la prueba falle. Un buen nombre dice qué se prueba y qué se espera: test_completar_marca_la_tarea_y_iguala_los_dias te ahorra abrir el fichero; test_1 o test_tarea no dicen nada. La convención habitual es test_<que>_<condicion>_<resultado esperado>, y no importa que quede largo: los nombres de las pruebas se leen, no se escriben a mano.
assert: la primera prueba
assert: la primera pruebaAntes de usar herramienta alguna, assert ya te permite comprobar cosas. Su forma es assert condicion, "mensaje si falla": si la condición es verdadera no ocurre nada, y si es falsa lanza un AssertionError con tu mensaje.
from tareafacil.modelo import Tarea
tarea = Tarea("Logotipo Sole", "nuria", "alta", 4)
assert tarea.prioridad == "alta", "la prioridad deberia normalizarse"
assert tarea.progreso() == 0.0, "una tarea nueva no tiene avance"
tarea.hechos = 2
assert tarea.progreso() == 50.0, f"esperaba 50.0 y salio {tarea.progreso()}"
print("Todas las comprobaciones han pasado.")Guardado en un fichero y ejecutado, o pasa en silencio o revienta señalando la línea. Es un primer paso perfectamente válido, y también la razón de que en 08-02 dijéramos que el sitio natural del assert son las pruebas. Sus límites aparecen enseguida: se para en el primer fallo sin comprobar el resto, no agrupa nada, no da un informe y no hay forma de ejecutar solo una parte. Para eso están las herramientas.
unittest, la biblioteca estándar
unittest, la biblioteca estándarunittest viene con Python, así que no hay nada que instalar. Sus pruebas son métodos que empiezan por test_ dentro de una clase que hereda de unittest.TestCase:
# tests/test_modelo.py
import unittest
from tareafacil.modelo import Tarea, TareaInvalida
class TestTarea(unittest.TestCase):
def setUp(self):
"""Se ejecuta ANTES de cada test: cada uno parte de lo mismo."""
self.tarea = Tarea("Cartel feria del libro", "luis", "alta", 4)
def test_normaliza_el_responsable_a_minusculas(self):
tarea = Tarea("Presupuesto Vidal", " MARTA ", "media", 2)
self.assertEqual(tarea.responsable, "marta")
def test_completar_iguala_los_dias_hechos(self):
self.tarea.completar()
self.assertTrue(self.tarea.completada)
self.assertEqual(self.tarea.hechos, 4)
def test_prioridad_invalida_lanza_excepcion(self):
with self.assertRaises(TareaInvalida):
Tarea("Cartel", "luis", "urgente", 3)Cuatro piezas que conviene entender. setUp se ejecuta antes de cada método de prueba, de modo que ninguna prueba hereda el estado de otra —hay también tearDown, que se ejecuta después y sirve para limpiar—. Los métodos assertEqual, assertTrue, assertIn comprueban igualdad, verdad y pertenencia. Y assertRaises, usado con with, comprueba lo contrario de lo habitual: que el bloque sí lance esa excepción; si no la lanza, la prueba falla. Se ejecuta desde la carpeta del proyecto:
$ python -m unittest discover -s tests -v test_completar_iguala_los_dias_hechos (test_modelo.TestTarea) ... ok test_normaliza_el_responsable_a_minusculas (test_modelo.TestTarea) ... ok test_prioridad_invalida_lanza_excepcion (test_modelo.TestTarea) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK
pytest, la herramienta del ecosistema
pytest, la herramienta del ecosistemapytest no viene con Python, pero es lo que usa prácticamente todo el mundo, porque quita la ceremonia: no hacen falta clases y se comprueba con el assert normal.
# tests/test_agenda.py
import pytest
from tareafacil.modelo import Tarea, TareaInvalida
from tareafacil.agenda import Agenda
def test_ordenadas_pone_primero_la_prioridad_alta():
agenda = Agenda()
agenda.agregar(Tarea("Presupuesto Vidal", "marta", "baja", 2))
agenda.agregar(Tarea("Cartel feria", "luis", "alta", 4))
assert [t.prioridad for t in agenda.ordenadas()] == ["alta", "baja"]
def test_prioridad_invalida_lanza_tarea_invalida():
with pytest.raises(TareaInvalida):
Tarea("Cartel", "luis", "urgente", 3)
@pytest.mark.parametrize("hechos, dias, esperado", [
(0, 4, 0.0), (2, 4, 50.0), (4, 4, 100.0), (8, 4, 100.0),
])
def test_progreso_calcula_el_porcentaje(hechos, dias, esperado):
tarea = Tarea("Cartel", "luis", "alta", dias)
tarea.hechos = hechos
assert tarea.progreso() == esperadoTres herramientas nuevas ahí. pytest.raises hace lo mismo que assertRaises. @pytest.mark.parametrize es la más rentable de todas: escribe una prueba y ejecútala con cuatro juegos de datos, que aparecen como cuatro pruebas independientes en el informe; sin ella habrías escrito cuatro funciones casi idénticas. Y fíjate en el último caso, (8, 4, 100.0): es un caso límite, más días hechos que previstos, exactamente el tipo de cosa que la parametrización invita a añadir.
Las fixtures son la forma que tiene pytest de preparar datos comunes, el equivalente del setUp. Se declaran con un decorador y se piden por nombre como parámetro:
@pytest.fixture
def agenda_con_tres():
"""Agenda de ejemplo del Estudio Alba."""
agenda = Agenda()
for t in (Tarea("Cartel feria", "luis", "alta", 4),
Tarea("Logotipo Sole", "nuria", "media", 5),
Tarea("Presupuesto Vidal", "marta", "baja", 2)):
agenda.agregar(t)
return agenda
def test_filtrar_por_responsable_devuelve_solo_las_suyas(agenda_con_tres):
resultado = agenda_con_tres.filtrar_por("responsable", "luis")
assert len(resultado) == 1
assert resultado[0].titulo == "Cartel feria"Y hay una fixture incorporada que resuelve un problema muy real: tmp_path, una carpeta temporal distinta para cada prueba, que Python borra al terminar. Es lo que permite probar el guardado en fichero sin tocar el tareas.json de verdad:
def test_guardar_y_cargar_conserva_las_tareas(agenda_con_tres, tmp_path):
ruta = tmp_path / "tareas.json" # carpeta temporal, no la real
almacen.guardar(agenda_con_tres, ruta)
recuperada = almacen.cargar_tareas(ruta)
assert len(recuperada) == 3
assert recuperada.buscar("Logotipo Sole").responsable == "nuria"Eso es una prueba de integración: comprueba que Agenda, to_dict, from_dict y el módulo almacen encajan entre sí. Y comprueba lo único que importa de la persistencia: que lo que entra vuelve a salir igual (un ciclo guardar → cargar, o round-trip).
unittest frente a pytest
unittest frente a pytest| Aspecto | unittest |
pytest |
|---|---|---|
| Instalación | Incluido en Python | pip install pytest |
| Estructura | Clases que heredan de TestCase |
Funciones sueltas |
| Comprobación | self.assertEqual(a, b) |
assert a == b |
| Excepciones | with self.assertRaises(E): |
with pytest.raises(E): |
| Datos comunes | setUp / tearDown |
Fixtures |
| Varios casos | Un método por caso | @parametrize |
| Informe de fallo | Escueto | Muy detallado: muestra los valores reales |
| Ejecución | python -m unittest discover |
pytest |
Ambas son correctas y conviven: pytest ejecuta también las pruebas escritas con unittest. La recomendación práctica: aprende unittest porque lo encontrarás en proyectos antiguos y porque no requiere instalar nada, y escribe con pytest porque es más corto, más legible y su informe de errores te dice qué valor salió y cuál esperabas.
- Qué probar y qué no
No se prueba todo: se prueba lo que puede romperse y duele. Para cada función merece la pena cubrir tres familias de casos:
- Casos normales: lo que ocurre el 90 % de las veces. Una tarea con 2 de 4 días da 50 %.
- Casos límite: los bordes, donde vive la mayoría de los fallos. Cero días hechos, agenda vacía, la lista con un solo elemento, más días hechos que previstos, textos con espacios o en mayúsculas.
- Casos de error: lo que debe fallar y cómo debe fallar. Una prioridad inventada tiene que lanzar
TareaInvalida, no devolverNoneen silencio.
Y hay una trampa clásica: probar la implementación en lugar del comportamiento. Una prueba que comprueba que Agenda guarda internamente una lista llamada _tareas se romperá el día que cambies esa lista por un diccionario, aunque el programa siga funcionando perfectamente. Prueba siempre lo que promete la función —lo que devuelve, lo que lanza, cómo queda el objeto—, nunca sus tripas. La señal de alarma es fácil de reconocer: si al refactorizar sin cambiar el comportamiento se rompen las pruebas, las pruebas estaban mal escritas.
- Cobertura
La cobertura mide qué porcentaje de tus líneas ejecutan las pruebas. Se obtiene con una herramienta externa:
El informe lista cada fichero con su porcentaje y las líneas que no se han ejecutado nunca, y ahí está su verdadera utilidad: descubrir ramas enteras que nadie prueba —típicamente los except—. Pero conviene entender su límite: la cobertura mide qué se ejecuta, no qué se comprueba. Un test que llame a todas las funciones sin un solo assert da el 100 % y no garantiza absolutamente nada. Un 90 % con pruebas que comprueban los casos límite vale infinitamente más que un 100 % vacío. Úsala para encontrar huecos, nunca como objetivo.
- TDD: rojo, verde, refactorizar
TDD (Test-Driven Development, desarrollo dirigido por pruebas) le da la vuelta al orden habitual: primero se escribe la prueba, y solo después el código que la hace pasar. El ciclo tiene tres pasos:
flowchart LR
A[ROJO: escribe una prueba que falla] --> B[VERDE: el codigo minimo para que pase]
B --> C[REFACTORIZAR: limpia sin romper nada]
C --> A
Escribir la prueba primero obliga a decidir qué debe hacer la función antes de pensar en cómo, y garantiza que la prueba de verdad comprueba algo: si nunca la has visto fallar, no sabes si funciona. No hace falta aplicar TDD siempre —en este curso no lo haremos—, pero hay un caso en el que es claramente la mejor opción: cuando arreglas un fallo. Escribe primero una prueba que lo reproduzca (rojo), arréglalo (verde) y esa prueba quedará para siempre impidiendo que el fallo vuelva.
- TareaFácil v0.21: la carpeta
tests/
tests/Las pruebas viven en su propia carpeta, junto al paquete:
tareafacil/ <- el proyecto: tareafacil/ (el paquete) y README.md
└── tests/
├── test_modelo.py
└── test_agenda.pyLos ficheros de tests/ son los que has visto en los apartados 6 y 7: test_modelo.py comprueba la validación de Tarea, la normalización del responsable y completar(); test_agenda.py comprueba el orden de ordenadas(), el filtro por responsable y el ciclo guardar/cargar con tmp_path. Y esta es una ejecución real, con un fallo de verdad:
$ pytest -q
....F...
=================================== FAILURES ===================================
_________________ test_progreso_calcula_el_porcentaje[8-4-100.0] _______________
def test_progreso_calcula_el_porcentaje(hechos, dias, esperado):
tarea.hechos = hechos
> assert tarea.progreso() == esperado
E assert 200.0 == 100.0
E + where 200.0 = <Tarea 'Cartel'>.progreso()
tests/test_modelo.py:34: AssertionError
1 failed, 7 passed in 0.06sLa prueba en rojo es el caso límite (8, 4, 100.0): con más días hechos que previstos, progreso() devolvía 200 %. El informe de pytest lo dice todo: qué caso concreto falló, qué valor salió (200.0) y cuál se esperaba (100.0). El arreglo es una línea en modelo.py:
def progreso(self) -> float:
"""Devuelve el porcentaje de avance, entre 0.0 y 100.0."""
return min(100.0, self.hechos / self.dias * 100) # antes, sin min()TareaFácil v0.21: ocho pruebas que se ejecutan en cinco centésimas de segundo y verifican lo que antes exigía recorrer el menú a mano. A partir de aquí, cada cambio se valida con un solo comando, y el proyecto acaba de ganar la red de seguridad que hacía falta para poder mejorarlo sin miedo. Existen además herramientas de integración continua que ejecutan estas mismas pruebas automáticamente en cada push al remoto; queda muy lejos de este curso, pero es bueno saber que el paso siguiente existe.
Errores Comunes y Consejos
- Probar funciones que hacen
input()yprint(). No se puede. Extrae la lógica a una función que reciba datos y devuelva un valor, y prueba esa. - Pruebas que dependen unas de otras. Cada prueba debe poder ejecutarse sola y en cualquier orden. Para eso están
setUpy las fixtures. - Tocar los datos reales. Una prueba que escribe en
tareas.jsonte borrará el trabajo algún día. Usatmp_path. - Probar la implementación, o poner nombres como
test_1ytest_tarea. Si al refactorizar sin cambiar el comportamiento se rompen las pruebas, estaban mal escritas; y el nombre es lo único que ves cuando algo falla. - Perseguir el 100 % de cobertura. Es fácil de falsear y no garantiza nada. Cubre los casos límite y los de error, que es donde están los fallos.
- Consejo: cuando encuentres un fallo, escribe primero la prueba que lo reproduce. Te confirma que lo has entendido y evita que vuelva nunca más.
Ejercicios
Ejercicio 1: Hacer probable una función
Esta función no se puede probar. Sepárala en dos —una con la lógica y otra con la conversación— y escribe dos pruebas con pytest para la parte lógica, incluyendo un caso límite:
def registrar_horas():
horas = float(input("Horas trabajadas: "))
tarifa = float(input("Tarifa por hora: "))
total = horas * tarifa
if horas > 8:
total += (horas - 8) * tarifa * 0.25 # recargo por hora extra
print(f"Total: {total:.2f} EUR")Ejercicio 2: Casos normales, límite y de error
Escribe las pruebas de Agenda.buscar(titulo), que devuelve la tarea con ese título (sin distinguir mayúsculas) o None si no existe. Cubre un caso normal, dos casos límite y comprueba también qué pasa con una agenda vacía. Usa una fixture para la agenda de ejemplo.
Ejercicio 3: Parametrizar y probar ficheros
Convierte estas tres pruebas casi idénticas en una sola parametrizada, y escribe además una prueba del ciclo guardar/cargar usando tmp_path:
def test_prioridad_alta_es_valida():
assert Tarea("A", "luis", "alta", 1).prioridad == "alta"
def test_prioridad_media_es_valida():
assert Tarea("A", "luis", "MEDIA", 1).prioridad == "media"
def test_prioridad_baja_es_valida():
assert Tarea("A", "luis", " baja ", 1).prioridad == "baja"Soluciones
Solución 1.
# logica.py: recibe datos, devuelve un valor. Probable.
JORNADA = 8
RECARGO_EXTRA = 0.25
def coste_horas(horas: float, tarifa: float) -> float:
"""Devuelve el coste total, con recargo del 25% a partir de 8 horas."""
total = horas * tarifa
if horas > JORNADA:
total += (horas - JORNADA) * tarifa * RECARGO_EXTRA
return round(total, 2)
def registrar_horas() -> None: # la conversacion, aparte
"""Pide los datos por teclado y muestra el coste."""
horas = float(input("Horas trabajadas: "))
tarifa = float(input("Tarifa por hora: "))
print(f"Total: {coste_horas(horas, tarifa):.2f} EUR")
# tests/test_logica.py
def test_ocho_horas_justas_no_llevan_recargo(): # caso limite
assert coste_horas(8, 30) == 240.0
def test_horas_extra_aplican_el_recargo():
assert coste_horas(10, 30) == 315.0 # 240 + 2*30*1.25El caso límite de las ocho horas exactas es el más valioso, porque es donde vive el error clásico: un >= en vez de un > cobraría recargo por una jornada normal, y solo esa prueba lo detectaría. Fíjate además en que la función de conversación quedó tan simple que ya no necesita prueba: lo único que hace es llamar a la que sí está probada.
Solución 2.
@pytest.fixture
def agenda():
a = Agenda()
a.agregar(Tarea("Cartel feria", "luis", "alta", 4))
a.agregar(Tarea("Logotipo Sole", "nuria", "media", 5))
return a
def test_buscar_encuentra_una_tarea_existente(agenda):
assert agenda.buscar("Cartel feria").responsable == "luis"
def test_buscar_no_distingue_mayusculas(agenda): # caso limite
assert agenda.buscar("CARTEL FERIA") is not None
def test_buscar_devuelve_none_si_no_existe(agenda): # caso limite
assert agenda.buscar("Tarea inventada") is None
def test_buscar_en_agenda_vacia_devuelve_none():
assert Agenda().buscar("Cartel feria") is NoneLas cuatro pruebas describen el contrato completo de buscar: encuentra, ignora mayúsculas, devuelve None cuando no hay coincidencia y no revienta con una agenda vacía. Ninguna mira cómo está guardada la colección por dentro, así que si mañana Agenda cambia su lista por un diccionario, estas pruebas seguirán pasando: comprueban el comportamiento, no la implementación.
Solución 3.
@pytest.mark.parametrize("entrada, esperada", [
("alta", "alta"), ("MEDIA", "media"), (" baja ", "baja"),
])
def test_la_prioridad_se_normaliza(entrada, esperada):
assert Tarea("A", "luis", entrada, 1).prioridad == esperada
def test_guardar_y_cargar_conserva_los_datos(tmp_path):
ruta = tmp_path / "tareas.json"
agenda = Agenda()
agenda.agregar(Tarea("Cartel feria", "luis", "alta", 4))
almacen.guardar(agenda, ruta)
recuperada = almacen.cargar_tareas(ruta)
assert len(recuperada) == 1
assert recuperada.buscar("Cartel feria").prioridad == "alta"Tres pruebas se han convertido en una que sigue ejecutándose tres veces, y añadir un cuarto caso ahora cuesta una línea. La segunda prueba usa tmp_path para escribir en una carpeta temporal: jamás toca el tareas.json real, y la carpeta desaparece al terminar. Es una prueba de integración en toda regla, porque para pasar necesitan funcionar Agenda, Tarea.to_dict, from_dict y las dos funciones del almacén.
Conclusión
Una prueba automatizada es código que ejecuta tu código y comprueba el resultado, y su valor real es defenderte de las regresiones: fallos que aparecen en algo que antes funcionaba. Hay pruebas unitarias, de integración y de sistema, y la proporción sana es muchas de las primeras y muy pocas de las últimas. Lo que hace probable a una función es exactamente lo que separamos en 04-04: recibir datos y devolver valores, sin input() ni print() de por medio. Toda prueba sigue el patrón AAA —preparar, actuar, comprobar— y lleva un nombre descriptivo, que es lo único que verás cuando falle. Con assert ya se puede comprobar cualquier cosa, y sobre esa base se levantan las dos herramientas: unittest, incluida en Python, con clases TestCase, métodos test_*, assertEqual/assertTrue/assertIn/assertRaises y setUp/tearDown, ejecutada con python -m unittest discover; y pytest, que se instala con pip y prescinde de clases, comprueba con el assert normal, informa con detalle de qué valor salió y cuál se esperaba, y aporta pytest.raises, @pytest.mark.parametrize para ejecutar una prueba con muchos juegos de datos, fixtures para preparar lo común y tmp_path para probar ficheros sin tocar los datos reales. Se prueban los casos normales, los casos límite —donde están casi todos los fallos— y los casos de error, siempre contra el comportamiento y nunca contra la implementación. La cobertura sirve para encontrar huecos, no como objetivo: mide qué se ejecuta, no qué se comprueba. Y TDD —rojo, verde, refactorizar— es especialmente útil al arreglar un fallo: primero la prueba que lo reproduce, después el arreglo.
TareaFácil es ahora la v0.21: una carpeta tests/ con test_modelo.py y test_agenda.py, ocho pruebas que se ejecutan en centésimas de segundo y que ya han encontrado un fallo real —el progreso que superaba el 100 %— y lo han dejado arreglado y protegido para siempre. Con esto tienes las cuatro piezas que faltaban al terminar el módulo 7: documentación, gestión de errores, historial y pruebas. Queda una última cosa, y es la que separa un programa que funciona de un programa del que uno se enorgullece: cómo está escrito. Hay funciones en interfaz.py que han crecido hasta las cuarenta líneas, números sueltos repetidos por varios ficheros, nombres heredados de cuando el programa era un ejercicio y trozos de código duplicados. En Estilo, legibilidad y refactorización se arregla todo eso con PEP 8, con herramientas automáticas y con un catálogo de refactorizaciones —apoyándote, precisamente, en las pruebas que acabas de escribir.
Fundamentos de la Programación
Módulo 1: Introducción a la Programación
- ¿Qué es la programación?
- Historia de la programación
- Lenguajes de programación
- Entornos de desarrollo
- Del problema al algoritmo
Módulo 2: Conceptos Básicos
- Variables y tipos de datos
- Operadores y expresiones
- Entrada y salida de datos
- Conversión de tipos y validación de datos
Módulo 3: Estructuras de Control
Módulo 4: Funciones y Procedimientos
- Definición y uso de funciones
- Parámetros y retorno de valores
- Ámbito de variables
- Descomponer un programa en funciones
- Funciones como valores: lambda y orden superior
Módulo 5: Estructuras de Datos
- Listas y arreglos
- Cadenas de caracteres
- Diccionarios y conjuntos
- Tuplas y estructuras anidadas
- Guardar datos en archivos: texto, CSV y JSON
Módulo 6: Algoritmos Básicos
Módulo 7: Objetos y Organización del Código
- De los datos a los objetos: clases e instancias
- Atributos, métodos y constructor
- Colecciones de objetos
- Módulos, paquetes e importaciones
Módulo 8: Buenas Prácticas y Herramientas
- Documentación y comentarios
- Depuración y manejo de errores
- Control de versiones
- Pruebas automatizadas
- Estilo, legibilidad y refactorización
