Lab guiado: Tests del repositorio SQLite y documentación de la versión final¶
- Unidad: UT3 — Persistencia con bases de datos
- Prerrequisitos: Lab UT3-A1 (BD creada) + Lab UT3-A2 completo (
RepositorioProductosSQLite,errores.pyy adaptación demaquina.py— Paso 8 incluido) - Agrupamiento: Parejas (conductor/navegante)
- Recursos:
- Ficheros de trabajo:
tests/test_repositorio_sqlite.py,CHANGELOG.md,docs/CONTRATO_REPOSITORIO.md,docs/ARQUITECTURA_POR_CAPAS.md,docs/TESTS_Y_PASOS.md,README.md
Cómo trabajar en parejas¶
Este lab se realiza en parejas conductor/navegante. Leed las instrucciones antes de empezar.
Roles¶
| Rol | Qué hace |
|---|---|
| Conductor | Escribe el código/configuración y ejecuta los pasos. Solo el conductor toca el teclado. |
| Navegante | Lee el enunciado en voz alta, propone soluciones, detecta errores y formula las preguntas. |
Rotación¶
- Cambiad de rol al final de cada paso.
- Al rotar, el navegante ocupa el teclado y el conductor se convierte en navegante.
- Ambos debéis poder explicar cualquier parte del trabajo al terminar el lab.
Entrega¶
Carpeta comprimida en formato zip que contenga todos los ficheros modificados o creados durante el lab: tests/test_repositorio_sqlite.py y los cuatro ficheros de documentación actualizados.
Antes de empezar
Este lab parte del proyecto con el Lab A2 completamente terminado: RepositorioProductosSQLite, errores.py y la adaptación de maquina.py (Paso 8 del Lab A2, que añade actualizar() y corrige comprar, reponer y agregar_item). Todos los comandos se ejecutan desde la carpeta padre de expendedora/ — la misma desde la que arrancas la aplicación con python -m expendedora.presentation.menu.
Verifica que los tests existentes siguen pasando:
Si alguno falla, resuélvelo antes de continuar.
Paso 1 — Estructura del fichero de test tests/test_repositorio_sqlite.py¶
Conceptos
Los tests de repositorio leen y escriben en un fichero .db real. Para que no se contaminen entre sí, cada test trabaja sobre una BD nueva y vacía — distinta de la de producción. BD_TEST es un atributo de clase: todas las instancias de la clase de test comparten la misma ruta.
Objetivo¶
Crear el esqueleto del fichero de test con los imports necesarios y el atributo BD_TEST.
Tarea¶
Crea el fichero tests/test_repositorio_sqlite.py:
Verifica que el fichero se puede importar sin errores:
Pregunta:¶
¿Por qué usamos un nombre diferente al de la BD de producción?
Respuesta
Para que los tests nunca toquen expendedora.db. Si un test guarda datos de prueba en la BD de producción, contamina los datos reales de la aplicación. Con un nombre distinto, setUp y tearDown pueden crear y borrar libremente el fichero de test sin riesgo.
Comprobación¶
Sin tests aún, pero tampoco errores de importación. Si ves ModuleNotFoundError, comprueba que repositorio_sqlite.py y errores.py existen en infrastructure/.
Paso 2 — setUp y tearDown: aislar la BD de cada test tests/test_repositorio_sqlite.py¶
Conceptos
setUp se ejecuta automáticamente antes de cada test: crea el esquema y el repositorio. tearDown se ejecuta después de cada test, tanto si pasa como si falla: elimina el fichero. Forman un ciclo simétrico — si setUp crea, tearDown destruye. El unlink() inicial en setUp garantiza un estado limpio aunque el proceso anterior se hubiera interrumpido.
Objetivo¶
Implementar el ciclo setUp/tearDown para que cada test parta de una BD limpia.
Tarea¶
Añade los dos métodos a la clase:
Pregunta:¶
¿Qué pasaría si solo tuviéramos
setUpsintearDown?
Respuesta
Los ficheros test_expendedora.db se acumularían entre ejecuciones. Peor aún: al ejecutar por segunda vez, setUp intentaría CREATE TABLE productos sobre una BD que ya tiene esa tabla y lanzaría OperationalError: table already exists. Los tests dejarían de funcionar de forma impredecible.
Comprobación¶
Ejecuta los tests y verifica que el fichero test_expendedora.db no existe al terminar:
Solo debe aparecer expendedora.db.
Paso 3 — Verificar que guardar() persiste los datos tests/test_repositorio_sqlite.py¶
Conceptos
El test comprueba el resultado observable: tras guardar(), los datos se recuperan con obtener(). No comparamos objetos enteros porque Item no tiene __eq__ definido — comparar item1 == item2 sería siempre False. En su lugar, verificamos cada atributo por separado. Para float usamos assertAlmostEqual para evitar problemas de redondeo.
Objetivo¶
Escribir los tests que verifican que guardar() persiste tanto un Item como un ItemConDescuento.
Tarea¶
Añade los dos tests a la clase:
Comprobación¶
Paso 4 — Verificar el tipo devuelto por obtener() tests/test_repositorio_sqlite.py¶
Conceptos
El repositorio debe reconstruir el tipo correcto: ItemConDescuento si el producto tiene entrada en descuentos, Item base si no la tiene. assertIsInstance(obj, Clase) verifica el tipo real del objeto — es el assert adecuado cuando la lógica depende de la clase concreta, no solo de los atributos.
Objetivo¶
Escribir los tests que verifican que obtener() devuelve el tipo exacto correcto.
Tarea¶
Añade los dos tests:
Pregunta:¶
¿Por qué no basta con verificar
item.nombre == "Agua"para saber siobtener()devuelve el tipo correcto?
Respuesta
Porque nombre es un atributo de Item, presente en ambas clases. Un repositorio que siempre devolviera Item pasaría ese test aunque fuera incorrecto: no estaría diferenciando entre productos con y sin descuento. assertIsInstance detecta exactamente ese fallo.
Comprobación¶
Los 4 tests escritos hasta ahora deben pasar.
Paso 5 — Testar las excepciones de dominio tests/test_repositorio_sqlite.py¶
Conceptos
El repositorio traduce sqlite3.IntegrityError a ProductoYaExisteError y devuelve ProductoNoEncontradoError cuando no existe el código. Los tests verifican excepciones de dominio, no excepciones de SQLite: si cambiamos el motor de BD, los tests siguen siendo válidos.
Con el gestor de contexto se puede inspeccionar el mensaje:
Objetivo¶
Verificar que el repositorio lanza las excepciones de dominio correctas y que el mensaje incluye el código problemático.
Tarea¶
Los dos primeros tests están completos. Completa el tercero con una sola línea de assertIn:
Pista
ctx.exception es el objeto excepción. str() lo convierte en texto. assertIn("X9", ...) comprueba que el código aparece en ese texto.
Mala práctica
Si cambias de motor de BD, este test se rompe aunque el repositorio funcione bien. Testea siempre excepciones de dominio.Comprobación¶
Ejecuta la suite completa:
Todos los tests de test_item.py, test_maquina.py y test_repositorio_sqlite.py deben estar en verde.
Tests completados
La suite de tests del repositorio SQLite está lista. Lo siguiente es documentar todos los cambios introducidos desde la versión 0.3.0.
Paso 6 — Registrar los cambios en el CHANGELOG CHANGELOG.md¶
Conceptos
El CHANGELOG.md registra qué cambió en cada versión (formato Keep a Changelog). La versión más reciente va al inicio del fichero. Las secciones Added y Changed describen qué se añadió y qué se modificó respecto a la versión anterior.
Objetivo¶
Añadir la entrada [0.4.0] al inicio de CHANGELOG.md.
Tarea¶
Añade el siguiente bloque antes de la entrada [0.3.0]. Sustituye XX por el día de hoy:
Comprobación¶
El fichero debe empezar con ## [0.4.0] seguido de ## [0.3.0].
Paso 7 — Actualizar el contrato del repositorio docs/CONTRATO_REPOSITORIO.md¶
Conceptos
El contrato ha cambiado: guardar() ya no lanza ValueError genérico sino ProductoYaExisteError, y obtener() ya no devuelve None sino que lanza ProductoNoEncontradoError. Cualquiera que implemente un repositorio alternativo debe conocer este contrato actualizado.
Objetivo¶
Reemplazar el contenido de docs/CONTRATO_REPOSITORIO.md para reflejar el nuevo comportamiento.
Tarea¶
Reemplaza el contenido completo del fichero:
Comprobación¶
El fichero tiene 5 secciones: contrato, implementaciones, excepciones, tests. Sin referencias a None ni ValueError.
Paso 8 — Actualizar la arquitectura y los pasos de test docs/ARQUITECTURA_POR_CAPAS.md + docs/TESTS_Y_PASOS.md¶
Objetivo¶
Añadir los nuevos ficheros al mapa de la arquitectura y al documento de ejecución de tests.
Tarea¶
A) docs/ARQUITECTURA_POR_CAPAS.md — Reemplaza la sección Mapa de archivos:
B) docs/TESTS_Y_PASOS.md — Añade al final:
Comprobación¶
ARQUITECTURA_POR_CAPAS.mdlistarepositorio_sqlite.py,errores.py,test_repositorio_sqlite.pyycrear_bd.py.TESTS_Y_PASOS.mdtiene la nueva sección al final con el comando de ejecución.
Paso 9 — Actualizar el README README.md¶
Objetivo¶
Actualizar la estructura del proyecto en README.md para incluir los nuevos ficheros.
Tarea¶
Reemplaza el bloque text de la sección ## Estructura del proyecto:
Actualiza también la sección ## Tests para incluir el nuevo fichero:
Comprobación¶
El README muestra repositorio_sqlite.py, errores.py, test_repositorio_sqlite.py y crear_bd.py en la estructura, y el comando python -m unittest en la sección de tests.
Katas de refactor¶
Kata 1 — unlink(missing_ok=True)
En setUp y tearDown hay una comprobación if self.BD_TEST.exists() antes de unlink(). En Python 3.8+ se puede eliminar usando unlink(missing_ok=True). Refactoriza ambos métodos para usar esta forma más concisa.
Kata 2 — Test de listar()
El repositorio tiene un método listar(). Escribe un test que guarde tres productos (dos Item y un ItemConDescuento), llame a listar() y verifique que devuelve exactamente tres elementos. ¿Qué devuelve listar() cuando el repositorio está vacío? Escribe también ese test.
Kata 3 — setUpClass (avanzado)
setUp crea el esquema antes de cada test aunque el esquema sea siempre el mismo. Investiga setUpClass y tearDownClass (se ejecutan una sola vez para toda la clase) y rediseña la suite para que el esquema se cree una vez y cada test solo inserte y limpie datos. ¿Qué ventajas e inconvenientes tiene frente al diseño original?
Checklist de entrega¶
- He creado
tests/test_repositorio_sqlite.pyconBD_TESTcomo atributo de clase. - He implementado
setUpque crea el esquema completo antes de cada test. - He implementado
tearDownque elimina el fichero de BD al terminar. - He escrito tests para
guardar()verificando atributos individuales. - He escrito tests de tipo con
assertIsInstanceyassertNotIsInstance. - He escrito tests de excepciones con
assertRaisesusando excepciones de dominio. - He verificado el mensaje de la excepción con
ctx.exception. - He añadido la entrada
[0.4.0]alCHANGELOG.md. - He actualizado
docs/CONTRATO_REPOSITORIO.mdcon el nuevo contrato. - He actualizado
docs/ARQUITECTURA_POR_CAPAS.mdydocs/TESTS_Y_PASOS.md. - He actualizado
README.mdcon la estructura actual del proyecto.
Actividades de ampliación¶
A — Test de listar() vacío (Paso 5) tests/test_repositorio_sqlite.py
¿Qué devuelve listar() cuando el repositorio está vacío? Escribe el test que lo comprueba.
B — Test de stock a cero (Paso 3) tests/test_repositorio_sqlite.py
Guarda un Item con cantidad=0 y verifica que se puede guardar y recuperar correctamente. Esto garantiza que el repositorio no confunde "stock vacío" con "producto inexistente".
C — Cobertura del repositorio SQLite (Paso 5)
Genera el reporte de cobertura HTML y analiza infrastructure/repositorio_sqlite.py. ¿Qué líneas no están cubiertas? Escribe un test adicional para cubrir al menos una:
D — docs/DATOS_INICIALES.md (Paso 6)
La versión 0.4.0 cambia cómo se cargan los datos iniciales: ahora es crear_bd.py quien los inserta. Actualiza docs/DATOS_INICIALES.md para reflejar que los datos se precargan vía ese script y describe qué productos contiene la BD inicial.
E — Reto final: test de integración
Sin plantilla ni pistas. Diseña y escribe un test de integración que:
1. Cree una BD de test con el esquema completo.
2. Use RepositorioProductosSQLite como repositorio.
3. Instancie MaquinaExpendedora con ese repositorio.
4. Simule un flujo completo: seleccionar producto, insertar dinero, comprar y verificar que el stock disminuye en la BD.
Define tú mismo qué assertions son necesarios para que el test sea significativo.