Una sola razón para fallar
Hace unos días hablando con un compañero surgió la conversación sobre si las pruebas unitarias pueden o deben tener más de un assert por test. Si no quieres seguir leyendo o no te interesa más el tema puedes pasar directamente a la respuesta: Sí. Pueden y deben.
¿Existe realmente la discusión sobre si las pruebas pueden o deben tener más de un assert por método? Resulta que sí, aunque no sea la polémica del momento, fíjate tú. Me he encontrado ya a varias personas que mantienen la tesis de que un test unitario no debe tener más de un assert. ¿De dónde viene esta idea sobre la cantidad de asserts que deben o no aparecer en un test unitario? ¿Es realmente una regla que alguien ha enunciado o solo una idea extendida?
Lo que sabemos sobre los test en cuanto a cuestiones de características que se hayan enunciado claramente y se acepten más o menos dentro del sentido común y la práctica de la industria del software es que deben seguir las reglas FIRST: Fast, Isolated, Repeteable, Self-Validating y Timely. Y es en la característica Isolated donde yo creo que surge la discrepancia entre lo que debe ser y lo está siendo debido a una interpretación demasiado literal de lo que allí se dice: un test debe ser independiente (no debe tener dependencias con otros test o código externo) y que solo debe tener una razón para fallar lo que pretende es que las condiciones de ese test sean controladas. Enunciado de otro modo: un test debe tener una única afirmación lógica y esta puede estar formada por varias afirmaciones físicas (varios asserts). Esto es lo que llamamos el SRP (Single Reponsability) de los test unitarios. Al igual que una clase solo debe responde a una necesidad de usuario, y por tanto un solo motivo para cambiar, un test solo debe comprobar una proposición y por tanto tener un solo motivo para fallar.
En definitiva: si tomásemos el método que estamos probando como una función matemática podríamos decir que dicha función es de variable múltiple y por tanto podemos fijar valores para algunos de esos parámetros mientras pasamos diferentes valores a al parámetro libre para determinar su comportamiento. Esa es la única razón para fallar, la variabilidad en el parámetro libre de la función.
Pudiera ser que la regla de un solo assert por test (poco escrita, por cierto, me está costando encontrar referencias directas) provenga de tiempos más difíciles en la que los frameworks de testing no nos permitían hacer tantas cosas como hoy en día. Y creedme que muchas de estas reglas han llegado tras varias décadas hasta nosotros sin que nadie se preguntase su fundamento provocando con ello algunos destrozos importantes. Nunca me olvido del tema de las nomenclaturas de los nombres test, a cada cual más rocambolesca y compleja de usar y leer mas cuando hoy en día se puede usar un lenguaje natural en los datos asociados en el test y no dependemos únicamente solo del nombre de la función del test.
De vuelta en el año 2020, resulta que disponemos de frameworks de testing con fantásticas características como las categorizaciones y el paso de parámetros a la función del test. Gracias a esto podemos fijar las condiciones del resto de variables de las que dependen la funcionalidad que estamos probando y centrarnos en elegir valores para nuestro parámetro libre junto con el resultado esperado. De ese modo el único motivo para que dicho test falle son los valores que esperamos que nos den un resultado de fallo. Por supuesto para comprobarlo podemos hacer diferentes assert siempre con el objetivo de comprobar una única afirmación. Para mí por tanto la afirmación de que solo debe un assert por test no solo es errónea, sino que además nos genera situaciones que no favorecen ni ayudan al desarrollo de software, justamente lo contrario que debía ocurrir.
Hay por supuesto algunas cuestiones prácticas sobre el motivo de no seguir la regla de un assert por test:
1. Mantenibilidad: Dependiendo del negocio y sobre todo de como de buenos seamos creando código que siga los principios SOLID vamos a obtener un conjunto útil de test unitarios o bien un conjunto insufrible de test unitarios al que dedicar muchas horas cada vez que introducimos un cambio.
Precisamente la necesidad de tener test unitarios es poder introducir cambios y obtener información de su impacto, o lo que es lo mismo: fijar el comportamiento de nuestro sistema. Con una estrategia de un assert por test lo que obtenemos es grandes cantidades de test difíciles de mantener e interpretar ya que hemos delegado la responsabilidad de fijar las condiciones de la prueba en el nombre del método de test y no en el framework de testing.
Cuando se introduce o de elimina una dependencia, o se modifica el negocio, en una clase no solo hay que modificar los test que hayan cambiado su comportamiento, sino que hay que añadir o eliminar nuevos test. La diferencia de haber usado una solución u otra puede cambiar radicalmente el numero de test que debemos modificar (mantener) de unos pocos a decenas.
2. Tiempo de ejecución: Si nuestros test unitarios deben ser rápidos de ejecutar, en parte esto es una característica propia ya que se trata de unidades de código pequeñas y sin dependencias, debemos asegurar que no creamos conjuntos de test repetitivos. Puede no parecer una cuestión importante pero estos conjuntos de test se ejecutan continuamente en nuestros sistemas de integración y despliegue varios miles de veces al año y esto tiene un impacto tanto en la factura a nuestros proveedores como en tiempo que pierden los desarrolladores. No hace falta hacer los cálculos para saber que un minuto de diferencia en un conjunto de test ejecutándose mil veces o más tiene un coste.
Si hemos dividido un a afirmación lógica en múltiples afirmaciones físicas tendremos exactamente la misma parametrización repetida en cada uno de esos métodos de test que deberá ejecutarse de manera aislada (esto no es elección nuestra, es una funcionalidad de los frameworks de testing) para cada una de las comprobaciones que hacemos. ¿Es realmente necesaria esta sobrecarga?
Por supuesto que esto no es una regla inflexible y en definitiva depende de la importancia que tenga ese assert (esa proposición lógica que estamos probando) darle más o menos protagonismo. Si nos ciñésemos estrictamente a la regla de un solo assert rápidamente encontraríamos casos imposibles como las pruebas para un mapeo de propiedades entre dos entidades de datos o las necesarias para probar un artefacto como un orquestados con una docena de colaboradores.
¿Y qué pasa con el TDD?
Pues a mi me parece perfectamente factible empezar con único assert, o muy pocos, por método y en los ciclos de refactor consolidarlos según se materializa el diseño.

















