Atlasingeniería

Testing y calidadFundamentos — JuniorTema 2Junior

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

  1. 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.
  2. 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.
  3. 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 pruebaQué hacerQué se gana
La función lee la hora con now()Que reciba el momento como parámetroSe puede probar el 29 de febrero sin esperar cuatro años
Adentro crea la conexión a la baseQue reciba el repositorio ya construidoLa prueba pasa uno falso y no necesita base
Lee una variable de entornoQue la lea quien la llama y la paseLa configuración se prueba una vez, no en cada test
Devuelve void y escribe en un logQue devuelva el resultado y escriba afueraSe verifica lo que decidió, no lo que imprimió
Usa un singleton globalQue reciba la dependenciaDos pruebas dejan de pisarse entre ellas
Las cinco filas son la misma jugada: sacar la decisión de dónde vienen las cosas fuera de la función que hace el trabajo. Eso es lo que se llama inyectar dependencias, y no necesita ninguna biblioteca.

Antes de seguir, predecí

Una función calcula el precio con descuento y, adentro, pregunta la fecha para saber si el cupón venció. ¿Cómo la probás con un cupón vencido?

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 / 100

Metele 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))

Meté cada cambio y mirá qué prueba lo agarra. La que no se pone en rojo con ninguno no está probando nada: su nombre promete algo que nadie verifica.

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.

NombreQué haceCuándo conviene
StubDevuelve lo que se le pidió que devuelvaCuando la unidad necesita un dato para seguir
FalsoUna implementación de verdad pero simple, como un repositorio en memoriaCuando hay varias operaciones seguidas y el estado importa
EspíaRegistra con qué lo llamaronCuando lo que hay que verificar es que se mandó el mail
Mock estrictoExige una secuencia exacta de llamadasCasi nunca: fija la implementación y se rompe en cada refactor
La última fila es la que produce suites que hay que reescribir enteras cuando se cambia algo por dentro, sin que ningún comportamiento haya cambiado.

Cómo se pregunta esto en una entrevista

Autoevaluación

¿Lo entendiste?

¿Qué hace unitaria a una prueba?
Cuesta mucho escribir la prueba de una función. ¿Qué es lo más probable?
¿Por qué el valor esperado se escribe a mano y no se calcula en el test?
¿Cuál de estos dobles conviene evitar como regla?