Pruebas unitarias y diseño testeable
Una prueba unitaria no es una prueba chica: es una que corre sin nada prendido y falla por un solo motivo. Lo que hace difícil escribirlas casi nunca es la herramienta, es el diseño del código que se quiere probar.
Para este tema conviene tener claro:Pruebas automatizadas y depuración
«Esto no se puede testear» casi nunca es cierto. Lo que suele pasar es que la función que se quiere probar lee la hora, abre una conexión y escribe un archivo, todo adentro, y no hay forma de preguntarle nada sin levantar medio sistema.
La dificultad para escribir una prueba es información sobre el código, no sobre el test. Es la señal más barata de que algo está demasiado enredado, y llega antes que cualquier revisión.
Qué hace unitaria a una prueba
No es el tamaño: es de qué depende. Una prueba unitaria ejercita una unidad de comportamiento sin base de datos, sin red, sin reloj y sin archivos. Eso trae tres cosas que las otras no tienen.
Lo que se gana cuando no depende de nada
- Corre en milisegundos. Una suite de mil pruebas unitarias tarda menos que abrir el proyecto, así que se corre a cada rato y no una vez por día.
- Falla por un solo motivo. Si la prueba se pone en rojo, el problema está en la unidad, no en la red ni en los datos de prueba de otro.
- Se puede correr en cualquier lado. En la máquina de quien recién entró, sin credenciales y sin un entorno preparado.
El diseño es lo que decide si se puede probar
Una función que recibe lo que necesita y devuelve un valor se prueba sola. Una que lo va a buscar adentro, no.
| Lo que traba la prueba | Qué hacer | Qué se gana |
|---|---|---|
| La función lee la hora con now() | Que reciba el momento como parámetro | Se puede probar el 29 de febrero sin esperar cuatro años |
| Adentro crea la conexión a la base | Que reciba el repositorio ya construido | La prueba pasa uno falso y no necesita base |
| Lee una variable de entorno | Que la lea quien la llama y la pase | La configuración se prueba una vez, no en cada test |
| Devuelve void y escribe en un log | Que devuelva el resultado y escriba afuera | Se verifica lo que decidió, no lo que imprimió |
| Usa un singleton global | Que reciba la dependencia | Dos pruebas dejan de pisarse entre ellas |
Antes de seguir, predecí
Una prueba, un motivo para fallar
La prueba que verifica cinco cosas se rompe por cualquiera de las cinco y no dice cuál. Y la que repite la implementación adentro del test no verifica nada: si el cálculo está mal, los dos están mal igual.
Código bajo prueba
def coupon_discount(price, percent, days_left):
if days_left < 0:
return 0
if percent > 100:
percent = 100
return price * percent / 100Metele un bug
El código está como lo escribió quien lo programó: toda la suite pasa.
La suite
- pasaun cupón vencido no descuenta nadacoupon_discount(100, 20, -1) == 0
- pasael 20 por ciento de 100 son 20coupon_discount(100, 20, 3) == 20
- pasaun cupón de 150 por ciento descuenta como máximo el preciocoupon_discount(100, 150, 3) == 100
- pasadevuelve un númeroisinstance(resultado, (int, float))
La cuarta prueba —«devuelve un número»— no se pone en rojo con ninguno de los cuatro cambios. Está probando que el código hace lo que hace, que es la forma más común de sumar pruebas sin sumar confianza.
Los dobles, y cuánto conviene usarlos
Cuando la unidad necesita algo de afuera, se le pasa un reemplazo. Los nombres se mezclan seguido y la diferencia importa poco comparada con una regla: reemplazá lo que está en el borde, no lo que estás probando.
| Nombre | Qué hace | Cuándo conviene |
|---|---|---|
| Stub | Devuelve lo que se le pidió que devuelva | Cuando la unidad necesita un dato para seguir |
| Falso | Una implementación de verdad pero simple, como un repositorio en memoria | Cuando hay varias operaciones seguidas y el estado importa |
| Espía | Registra con qué lo llamaron | Cuando lo que hay que verificar es que se mandó el mail |
| Mock estricto | Exige una secuencia exacta de llamadas | Casi nunca: fija la implementación y se rompe en cada refactor |
Cómo se pregunta esto en una entrevista
Autoevaluación