Pruebas UnitariasLaboratorio final de Programación III
Python UNICAMACHO

FI303205 Programación III, grupo S441E. Actividad final del tercer corte

Tu CRUD ya funciona. Ahora demuéstralo con código.

Hasta hoy has probado tu programa a mano: abres el formulario, escribes datos y miras si salió bien. En este laboratorio vas a escribir pruebas unitarias con unittest: pequeños programas que revisan tu modelo y tu repositorio JSON solos, en segundos, cada vez que cambias algo.

1 sesión3 horas presenciales
10+pruebas mínimas
Tu CRUDno el del ejemplo

¿Qué es una prueba unitaria?

Es un método que ejecuta una sola parte de tu programa (una "unidad": un método, un setter, una operación del CRUD) y comprueba automáticamente que el resultado sea el esperado.

Se repite gratis

Probar a mano 14 casos toma 10 minutos. Con pruebas unitarias toma menos de un segundo, y lo puedes hacer cada vez que cambies el código.

Atrapa lo que se rompe

Si mañana alguien cambia una validación y daña otra cosa, una prueba se pone en rojo y te dice exactamente cuál y en qué línea.

Ya lo conoces de Java

unittest es el equivalente de JUnit. Mismas ideas: clases de prueba, métodos test, aserciones y preparación antes de cada prueba.

La estructura de toda prueba: Preparar, Actuar, Verificar

Preparar (Arrange)Crea los datosUn objeto, un repositorio vacío, una conexión falsa.
Actuar (Act)Ejecuta lo que pruebasLlama al método: repo.crear(e).
Verificar (Assert)Comprueba el resultadoassertEqual(...): si no se cumple, la prueba falla.

De JUnit a unittest

Java (JUnit)Python (unittest)Para qué sirve
class EstudianteTestclass TestEstudiante(unittest.TestCase)Agrupa las pruebas de una clase
@Testmétodo que empieza por test_Marca un método como prueba
@BeforeEachdef setUp(self)Prepara datos antes de cada prueba
@AfterEachdef tearDown(self)Limpia después de cada prueba
assertEquals(a, b)self.assertEqual(a, b)Los dos valores son iguales
assertThrows(...)with self.assertRaises(Error):El bloque debe lanzar esa excepción
Mockito.mock(...)MagicMock()Objeto falso que reemplaza uno real

Las aserciones que vas a usar

AserciónLa prueba pasa si…Ejemplo
assertEqual(a, b)a == bself.assertEqual(e.semestre, 3)
assertTrue(x) / assertFalse(x)x es verdadero / falsoself.assertTrue(repo.eliminar("1001"))
assertIsNone(x) / assertIsNotNone(x)x es / no es Noneself.assertIsNone(repo.buscar("9999"))
assertIsInstance(x, Clase)x es un objeto de esa claseself.assertIsInstance(e, Estudiante)
assertIn(a, b)a está dentro de bself.assertIn("INSERT", sql)
assertRaises(Error)el bloque lanza esa excepciónwith self.assertRaises(DatoInvalidoError):

Haz cada paso sobre tu propio CRUD

El ejemplo usa la clase Estudiante. Tú vas a escribir lo mismo para tu clase (Producto, Mascota, Libro…). Cada paso indica qué debes cambiar.

1

Prepara la estructura del proyecto

  1. Abre la carpeta de tu CRUD en VS Code (Archivo > Abrir carpeta) y verifica que el programa todavía corre.
  2. En el explorador, crea una carpeta nueva llamada exactamente tests al mismo nivel que tu modelo.py.
  3. Dentro de tests crea un archivo vacío llamado __init__.py (dos guiones bajos antes y después). Sin él, Python no encuentra las pruebas.
Estructura esperada
mi_crud/
├── modelo.py               # tu clase con @property y tu excepción
├── repositorio_json.py     # tu CRUD que guarda en .json
├── dao_mysql.py            # tu CRUD con MySQL (DAO)
├── formulario.py           # tu interfaz Tkinter
└── tests/
    ├── __init__.py         # vacío, pero obligatorio
    ├── test_modelo.py
    ├── test_repositorio_json.py
    └── test_dao_mock.py    # reto opcional

Tus archivos tienen otro nombre? No hay problema. Si tu modelo está en paso6_crud_poo.py, en las pruebas escribe from paso6_crud_poo import Producto. Lo importante es que la carpeta tests quede junto a tus archivos.

2

Escribe tu primera prueba

Crea tests/test_modelo.py con este contenido mínimo. Fíjate en las tres partes: preparar (setUp), actuar (crear el objeto) y verificar (assertEqual).

tests/test_modelo.py
import unittest
from modelo import Estudiante, DatoInvalidoError


class TestEstudiante(unittest.TestCase):

    def setUp(self):
        self.ana = Estudiante("1001", "Ana Gómez", "ana@correo.com", 3)

    def test_crear_estudiante_valido(self):
        self.assertEqual(self.ana.nombre, "Ana Gómez")
        self.assertEqual(self.ana.semestre, 3)

Adáptalo: cambia Estudiante por tu clase, usa los atributos de tu clase y valores reales de tu negocio. Por ejemplo: Producto("P01", "Teclado", 85000, 10) y assertEqual(self.teclado.precio, 85000).

La explicación de cada línea está en Código explicado.

3

Prueba que tu encapsulamiento sí protege el objeto

Estas son las pruebas más valiosas. Ya hiciste setters con validaciones; ahora demuestras que funcionan. Recuerda cómo se ve una propiedad validada:

modelo.py (fragmento que ya tienes)
@semestre.setter
def semestre(self, valor):
    valor = int(valor)
    if not 1 <= valor <= 10:
        raise DatoInvalidoError("El semestre debe estar entre 1 y 10")
    self._semestre = valor

Para probar que se lanza la excepción usas assertRaises como bloque with. Agrega estos métodos dentro de la misma clase TestEstudiante:

tests/test_modelo.py (continuación)
    def test_semestre_fuera_de_rango(self):
        with self.assertRaises(DatoInvalidoError):
            Estudiante("1003", "Camila", "camila@correo.com", 50)

    def test_correo_sin_arroba(self):
        with self.assertRaises(DatoInvalidoError):
            Estudiante("1004", "Pedro", "pedrocorreo.com", 2)

    def test_setter_protege_al_modificar(self):
        with self.assertRaises(DatoInvalidoError):
            self.ana.nombre = ""
        self.assertEqual(self.ana.nombre, "Ana Gómez")

Adáptalo: escribe una prueba por cada regla de tus setters. Si tu Producto no acepta precio negativo, prueba Producto("P02", "Mouse", -500, 3). Si tu excepción tiene otro nombre, usa ese nombre.

4

Ejecuta las pruebas y lee el resultado

  1. Abre la terminal de VS Code: menú Terminal > Nueva terminal (o Ctrl + ñ).
  2. Verifica que estás en la carpeta del proyecto (la que contiene tests), no dentro de tests.
  3. Escribe el comando y presiona Enter. La opción -v muestra el nombre de cada prueba.
Terminal
PS C:\Users\TuUsuario\mi_crud> python -m unittest discover -v
test_correo_sin_arroba (tests.test_modelo.TestEstudiante.test_correo_sin_arroba) ... ok
test_crear_estudiante_valido (tests.test_modelo.TestEstudiante.test_crear_estudiante_valido) ... ok
test_documento_con_letras (tests.test_modelo.TestEstudiante.test_documento_con_letras) ... ok
test_semestre_fuera_de_rango (tests.test_modelo.TestEstudiante.test_semestre_fuera_de_rango) ... ok
test_semestre_texto_se_convierte_a_numero (tests.test_modelo.TestEstudiante.test_semestre_texto_se_convierte_a_numero) ... ok
test_setter_protege_al_modificar (tests.test_modelo.TestEstudiante.test_setter_protege_al_modificar) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.002s

OK
ResultadoSímboloQué significa
ok.La prueba pasó: el código hace lo que esperabas.
FAILFUn assert no se cumplió: tu código da un resultado distinto al esperado.
ERRORELa prueba se cayó antes de verificar (un import mal escrito, un atributo que no existe). Revisa el mensaje.

Si dice Ran 0 tests: falta tests/__init__.py, el archivo no empieza por test_ o el método no empieza por test_. Si dice ModuleNotFoundError: No module named 'modelo': estás ejecutando desde la carpeta equivocada. En Windows, si python no funciona, usa py -m unittest discover -v.

También puedes usar el panel de pruebas de VS Code: clic en el ícono del matraz (Testing) en la barra lateral > Configure Python Tests > unittest > carpeta tests > patrón test_*.py. Verás cada prueba con un círculo verde o rojo y podrás ejecutarlas con un clic.

5

Prueba tu CRUD en JSON sin dañar tus datos

Una regla de oro: las pruebas nunca deben tocar tus datos reales. Por eso cada prueba trabaja con un archivo .json temporal que se crea en setUp y se borra en tearDown.

tests/test_repositorio_json.py
import os
import tempfile
import unittest

from modelo import Estudiante
from repositorio_json import RepositorioJSON


class TestRepositorioJSON(unittest.TestCase):

    def setUp(self):
        self.carpeta = tempfile.TemporaryDirectory()
        ruta = os.path.join(self.carpeta.name, "prueba.json")
        self.repo = RepositorioJSON(ruta)

    def tearDown(self):
        self.carpeta.cleanup()

    def test_crear_y_buscar(self):
        self.repo.crear(Estudiante("1001", "Ana Gómez", "ana@correo.com", 3))
        encontrado = self.repo.buscar("1001")
        self.assertIsNotNone(encontrado)
        self.assertEqual(encontrado.nombre, "Ana Gómez")

Completa con una prueba para cada operación: listar vacío, no permitir duplicados, actualizar y eliminar. El archivo completo, con la explicación de cada línea, está en Código explicado.

Si tu repositorio tiene la ruta fija (por ejemplo open("productos.json") dentro de los métodos), primero cámbialo para que reciba la ruta en el constructor: def __init__(self, ruta="productos.json"): self.ruta = ruta. Esto se llama inyección de dependencias y es justamente lo que hace que tu código se pueda probar.

6

Reto: prueba tu DAO de MySQL sin MySQL Bonificación

Una prueba unitaria no debe depender de que el servidor MySQL esté encendido. Como tu DAO recibe la conexión (EstudianteDAO(Conexion())), puedes entregarle una conexión falsa hecha con MagicMock y controlar lo que "responde" la base de datos.

tests/test_dao_mock.py (fragmento)
    def setUp(self):
        self.conexion_falsa = MagicMock()
        cnx = self.conexion_falsa.conectar.return_value.__enter__.return_value
        self.cursor = cnx.cursor.return_value.__enter__.return_value
        self.dao = EstudianteDAO(self.conexion_falsa)

    def test_buscar_convierte_fila_en_objeto(self):
        self.cursor.fetchone.return_value = (1, "1001", "Ana Gómez", "ana@correo.com", 3)
        e = self.dao.buscar("1001")
        self.assertIsInstance(e, Estudiante)

La explicación línea por línea está en Código explicado. Si tu DAO usa cnx = conectar() sin with, las líneas 12 y 13 cambian: pregúntale al docente.

7

Haz la prueba de ruptura

Una prueba que nunca falla no prueba nada. En la sección Prueba de ruptura vas a dañar a propósito una regla de tu modelo para confirmar que tus pruebas lo detectan. Lo que observes lo describes en la entrega.

8

Verifica el mínimo y entrega

  • Carpeta tests con __init__.py.
  • Mínimo 10 pruebas en total, todas en verde (OK).
  • Mínimo 3 pruebas con assertRaises, una por cada regla distinta de tus setters.
  • Pruebas del repositorio JSON con setUp y tearDown: crear, buscar, actualizar y eliminar.
  • Nombres de pruebas que digan qué comprueban (test_precio_negativo_lanza_error, no test_1).
  • Prueba de ruptura hecha y descrita.

Línea por línea

Cada fila muestra una línea del código y lo que hace. Este código está probado: las 14 pruebas pasan. Elige el archivo:

El proyecto de ejemplo sirve para comparar, no para entregar. Si entregas las pruebas de Estudiante en vez de las de tu propia clase, la actividad no se califica.

Rompe tu código a propósito

Así compruebas que tus pruebas sirven: si dañas una regla y todo sigue en verde, tus pruebas no están revisando esa regla.

Daña una regla

En tu modelo.py cambia una validación. Por ejemplo, el límite del semestre de 10 a 100:

if not 1 <= valor <= 100:     # antes decía 10

Ejecuta las pruebas

Corre python -m unittest discover y toma una captura del resultado.

Restaura el código

Vuelve a dejar la regla como estaba y comprueba que todo regresa a OK.

Resultado real al dañar la regla
......F.......
======================================================================
FAIL: test_semestre_fuera_de_rango (tests.test_modelo.TestEstudiante.test_semestre_fuera_de_rango)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\Users\TuUsuario\mi_crud\tests\test_modelo.py", line 21, in test_semestre_fuera_de_rango
    with self.assertRaises(DatoInvalidoError):
AssertionError: DatoInvalidoError not raised

----------------------------------------------------------------------
Ran 14 tests in 0.006s

FAILED (failures=1)

Cómo leerlo

  • Qué prueba falló: test_semestre_fuera_de_rango.
  • Dónde: línea 21 de test_modelo.py.
  • Por qué: se esperaba la excepción y no se lanzó, porque ahora el semestre 50 es "válido".

Esto es lo que vas a escribir en la entrega, con tu regla y tu prueba.

Cómo se califica (sobre 5.0)

La nota del laboratorio es parte del tercer corte. En la sustentación el docente elige una de tus pruebas y te pide explicarla línea por línea.

CriterioQué se revisaValor
Estructura y ejecuciónCarpeta tests con __init__.py; python -m unittest discover corre sin errores; mínimo 10 pruebas en OK.0.5
Casos válidos del modeloAl menos 3 pruebas que verifican que el objeto se crea y se modifica bien, con valores propios de tu negocio.0.75
Pruebas del encapsulamientoAl menos 3 pruebas con assertRaises, una por regla distinta, usando tu excepción propia.1.0
Pruebas del repositorio JSONCrear, buscar, actualizar y eliminar; archivo temporal con setUp y tearDown; no tocan tus datos reales.1.25
Prueba de rupturaDescribe qué regla dañaste, qué prueba falló, en qué línea y por qué.0.75
SustentaciónExplica línea por línea la prueba que el docente elija y responde por qué se usa un archivo temporal.0.75
Bonificación: reto MagicMockAl menos 2 pruebas del DAO de MySQL con una conexión falsa (la nota máxima sigue siendo 5.0).+0.3

Cómo subir tu laboratorio

Puedes volver a enviar las veces que necesites antes del cierre; cada envío reemplaza al anterior.

1. Toma la captura

Ejecuta python -m unittest discover -v. Presiona Windows + Shift + S y recorta la terminal de forma que se vean los nombres de tus pruebas, la línea Ran N tests y el OK. Guárdala como PNG.

2. Comprime tu proyecto

Borra las carpetas __pycache__ y .venv si existen. Luego clic derecho sobre la carpeta del proyecto > Comprimir en archivo ZIP (en Windows 10: Enviar a > Carpeta comprimida). Debe ser .zip, no .rar.

3. Sube aquí

Ingresa con tu número de documento; aparecerá tu nombre. Confirma que eres tú, llena el formulario, adjunta el ZIP y la captura y presiona Enviar entrega. La entrega es individual.

Ingresar

El mismo con el que estás matriculado en el grupo S441E.