¿Sabías que let y let! no son lo mismo? A simple vista pueden parecerlo, pero no lo son y en este artículo te voy a explicar las principales diferencias.
La respuesta corta es que let carga el dato o variable recién cuando es requerido, y let! no. De todas formas, si querés entender un poco más sobre el asunto te invito a seguir leyendo, porque te voy a contar en detalle cómo llegué a comprenderlo yo, en mi proceso de aprendizaje sobre testing.
En Unagi, tenemos un Campus para capacitarnos internamente, con unidades sobre muchos temas. La primera vez que leí ambos términos fue cuando estaba haciendo la unidad de Tests, en el libro “Everyday testing with Rspec”. La verdad es que inicialmente no le di demasiada importancia a ninguno de los dos, hasta que noté que se usaban mucho en los ejemplos y me cayó la ficha de que debían ser importantes.
Venía usando before para definir data que seguro iba a necesitar en distintos bloques describe o context, pero no necesariamente todos los tests la usaban: podían haber excepciones. Si escalamos a una aplicación grande o compleja, llenar los tests de datos que no siempre utilizamos puede ralentizarlos significativamente, o hacerlos menos legibles (imaginate un bloque before enorme al comienzo de un test: te da la sensación de que el asunto va a ser largo, pesado y la verdad es que asusta 😅).
Para prevenir esto, Rspec provee el método let, el cual hace un “lazy-load” de data, es decir que la misma se “carga” o define recién cuando es requerida (o sea, cuando es invocada en un bloque it, describe o context).
Por ejemplo, supongamos que tenemos una aplicación en la cual un usuario puede tener proyectos, cada uno con distintas tareas, y queremos hacer un test para el modelo Tarea (para simplificarme la vida a mi, ya que esta fue justamente la ejercitación que tuve en el Campus). Establezcamos además que no va a ser posible crear una tarea que no pertenezca a ningún proyecto o sin un nombre, obviamente, por lo que definimos el test así:
De paso te cuento, por si no lo sabías, que FactoryBot es una herramienta muy útil ya que nos permite tener “moldes” pre-armados de nuestros datos, los cuales vienen muy bien para automatizar la creación de objetos a la hora de testear. Si te interesa el tema, podés encontrar una introducción en este artículo.
El primer bloque it verifica que, si pertenece a un Proyecto y tiene un nombre, entonces la Tarea debería ser válida y poder guardarse. Los dos siguientes chequean que, sin estas cualidades, no sea posible crearla.
Podemos ver que project es llamado en la línea 8, al crear una nueva Tarea
por lo que let crea el nuevo Proyecto recién ahí (y se deshace de él una vez que finalizó dicho bloque, en la línea 12). Los otros dos tests no lo necesitan, por lo que al ejecutarlos, ningún Proyecto es creado.
Sin embargo, no siempre nos sirve utilizar let. Por ejemplo, imaginemos que tenemos un buscador de Tareas, y queremos testear la situación en la que no hay ninguna que contenga el String buscado. Para ello crearemos varias tareas con distintos nombres, ya que seguramente también querríamos chequear el caso en el que una o varias de ellas coincidan con la búsqueda, intentando reutilizar data. Sin embargo, no podríamos hacer uso de las tareas que creamos a no ser que las llamáramos explícitamente (de lo contrario, ni siquiera serían creadas), lo cual no queda muy legible:
Como la legilibilidad es algo a lo que siempre apuntamos al momento de testear, si bien este código no fallaría, no es el más adecuado.
Para corregirlo o mejorarlo podemos usar let!, que a diferencia de let no hace “lazy-load”, sino que carga la data directamente antes de los bloques context o describe, sin necesidad de ser requerida.
Es decir, si definimos:
Y testeamos el caso donde la búsqueda sí tiene resultados:
Notaremos que corre correctamente dado que task1 y task3 fueron definidas con let!, es decir, creadas incluso sin ser llamadas. De todas formas, a pesar de que es correcto y legible, debemos reconocer que nos encontramos en la misma situación que con el before: creamos data que puede no ser utilizada, y además debemos tener cuidado de no causar errores inesperados en tests que tengan esta data extra como resultado.
De hecho, en este momento cualquiera podría estar preguntándose cuál es la diferencia entre let! y before. Si bien se parecen mucho en cuanto a funcionalidad, la principal diferencia es que let! nos da una referencia explícita al dato que estamos creando, mientras que con before debemos usar variables de instancia (por eso nos referimos a ellas con @ al principio).
También hay que notar que let y let! son fácilmente confundibles entre sí y seguimos queriendo un test legible, por lo que cada programador/a deberá evaluar si es preferible utilizarlo o volcar los datos necesarios a un bloque before. Además, vale la pena mencionar que esa no es la única desventaja de let: si bien hace parecer más concisos y cortos nuestros tests, esto tiene un costo.
Debemos recordar que cada it o describe es un test individual, y si bien pueden haber tests de sólo 3 líneas, hay que tener en cuenta cómo funciona por detrás.
Volviendo a nuestro ejemplo, el siguiente test parece razonable
Tenemos un proyecto que pertenece a un usuario y tiene un nombre. ¿Por qué no sería válido? De hecho sí lo es, pero hay que tener en cuenta que el user al que pertenece implica invocar a otro let:
Y que quizás cada uno de los objetos otorgado a los atributos de User (nickname_1, date_of_birth_1, city1 y url1) implique llamar a otro let, que puede tener más anidados… Creo que se entiende el punto. Asumiendo la peor situación, lo que parecía un test súper simple implicaría como mínimo 5 llamados a la base de datos (uno por cada let) y esos mismos podrían tener incluso más.
Por lo tanto, creo que en muchos casos vale la pena preguntarnos si no es mejor definir toda o parte de la data a utilizar dentro del bloque del test. Pensando en un programador que se suma al proyecto, si corriera los tests de la aplicación debería seguramente, en el primer caso, leer todo el archivo para entender qué variables están siendo definidas, dónde están siendo utilizadas y el por qué de la demora al ejecutarse (recordemos el como mínimo 5 llamadas a la base de datos). En cambio, definiendo la data dentro del test:
Es sumamente fácil identificar qué datos están siendo usados y cómo se definen. Además, no accedemos nunca a la base de datos gracias al método build_stubbed de FactoryBot (puede leerse más sobre él aquí), que a diferencia de build o create no persiste los datos definidos, creando así un test más rápido.
Primero, quiero agradecerte por leer este artículo 😊 Y segundo, con las diferencias, ventajas y desventajas de let establecidas, me gustaría preguntarte ¿Qué elegís vos al momento de testear? Yo todavía me estoy formando, así que me sería muy útil leerte en comentarios contándome qué opción preferís y por qué 👀🙌