leanmind logo leanmind text logo

Blog

Código Sostenible

Cómo escribir código fácil de mantener mediante valores, principios y técnicas.

Agrupar test por contexto

Por Carlos Blé Jurado

Una de las principales ventajas de los frameworks de testing de la familia RSpec, como son Jasmine y Jest, es la capacidad de anidar los test por su contexto. Tanto el contexto de ejecución, como el documental, porque las cadenas de texto que reciben las funciones describe, e it, se concatenan cuando un test falla, de manera que podemos construir frases con sentido de negocio. Este tipo de framewoks, nos permite contar historias con los test, son una de las herramientas técnicas más expresivas para practicar storytelling.

Agrupación de test por su contexto

Te habrás dado cuenta de que estoy escribiendo “test” para referirme al plural, y es que la RAE lo recoge así, porque dice que es más fácil de pronunciar. Así que ya sabes otra curiosidad, según la RAE, el plural del anglicismo test, queda igual que el singular; no son tests, ni teses.

Una pregunta frecuente en el testing es, ¿hace falta una clase o suite de test para cada clase o módulo de producción? En realidad, por cada clase o módulo de test, puede haber N clases o suites de test. Un mismo artefacto responde de manera diferente ante contextos diferentes, y esto lo podemos expresar muy bien con la anidación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
describe("the shopping cart", () => {
    describe("when it's Black Friday", () => {
        it("offers the 3x2 discount", () => {
           /*...*/
        });
        it("...", () => {
           /*...*/
        });
    });

    describe("when it's a new customer", () => {
        it("offers a 10% discount", () => {
           /*...*/
        });
        it("...", () => {
           /*...*/
        });
    }):
});

Si el primer test falla, el mensaje de error contendrá lo siguiente: “the shopping cart when it’s Black Friday offers the 3x2 disccount”.

Hay personas que gustan de utilizar un alias de la función “describe”, para llamarle “context”. Veamos otro ejemplo de agrupación por contexto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
context = describe;

describe("the shopping cart", () => {
    context("when there is a discount", () => {
        it("applies the 3x2 to expensive items", () => {
           /*...*/
        });
        it("...", () => {
          /*...*/
        });
    });
    context("when no discount is applicable", () => {
        it("sums the total cost of all the items upon checkout", () => {
           /*...*/
        });
        it("...", () => {
          /*...*/
        });
    }):
});

En estos ejemplos, los contextos están dentro de la misma suite de test (describe), mientras que en otras ocasiones, los bloques describe podrían ser totalmente independientes, incluso aunque ataquen a la misma clase, módulo o función de producción ¿Cómo determinamos cuál es el contexto, para saber agrupar los test?, ¿debemos tenerlo claro a priori? Yo lo consigo a base de hacer refactoring de los test, ya que antes de escribirlos, no tengo todavía claro cuál es el contexto que comparten. Conforme voy extrayendo duplicidad de los test, muevo el código común a las funciones especiales, beforeEach y/o afterEach:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
describe("the shopping cart", () => {
    let repository;
    beforeEach(() => {
        repository = initializeMockRepository();
    });
    context("when there is a discount", () => {
        beforeEach(() => {
            enableDiscounts(repository);
        });
        it("applies the 3x2 to expensive items", () => {
           /*...*/
        });
        it("...", () => {
          /*...*/
        });
    });
    context("when no discount is applicable", () => {
        beforeEach(() => {
            disableDiscounts(repository);
        });
        it("sums the total cost of all the items upon checkout", () => {
           /*...*/
        });
        it("...", () => {
          /*...*/
        });
    }):
});

No todos los describe/context anidados están obligados a tener su propio beforeEach, a veces anidamos simplemente para documentar mejor. Lo cierto es que los métodos de preparación de contexto como beforeEach, nos dan las pistas perfectas para saber agrupar. Por ejemplo, si tengo 5 test en un mismo describe, y resulta que tengo un beforeEach, del cual algunas líneas aplican a 2 test y otras aplican a los 3 restantes, directamente puedo separar los conjuntos de test. La regla es cuidar de que todos los test que estén bajo los efectos de un beforeEach, utilicen todas las líneas que este contiene, es decir, que no falten ni sobren líneas en el beforeEach. Si encontrarnos la función beforeEach al principio, resulta molesto para la lectura, puede ponerse debajo del último test del conjunto, aunque puede resultar sorprendente para algunas personas, pues lo más habitual es verla al principio (es parte de la preparación de los escenarios).

Táctica para legacy

Cuando trabajamos en el mantenimiento de baterías de test que no han seguido esta pauta de refactoring, es común que nos encontremos con suites que contienen decenas de elementos. Llegados a este punto, no es práctico el uso de la función beforeEach para inicializar, porque se van a romper muchos test. Como alternativa, podemos hacer uso de un método de inicialización al que invocamos desde los test de manera deliberada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
describe("the shopping cart", () => {
    it("applies the 3x2 to expensive items", () => {
       { repository } = setup();
    });
    it("...", () => {
      /*...*/
    });
    /*...*/
    function setup(){
       /*...*/
    }
});

Así, vamos eliminando duplicidad y ruido de los test sin que se rompan. Una vez realizadas las extracciones, ya empezaría a ser más intuitivo agruparlos por contexto, como vimos en los ejemplos anteriores. Por tanto, esta idea es muy interesante para “legacy test”.

Legacy vs green field

Ahora bien, si estás empezando a añadir test (green field) y usas esta técnica de invocar de manera explícita a un método de inicialización en los test (como el setup del ejemplo), corres el riesgo de no darte cuenta de agrupar los test, puesto que no ves emerger el contexto, no ves el patrón.

Mi consejo es que utilices los métodos beforeEach y afterEach para tus baterías de test nuevas, jugando a contar historias con la agrupación de contextos. Verás que así los test ayudan mucho a documentar la solución.

¿Qué demonios es SUT?

Hay una práctica que me he encontrado en varios proyectos, que es llamar sut a la variable que representa la instancia de la clase que queremos probar (o al módulo). SUT viene de System Under Test, aunque también lo he visto como Subject Under Test. Es una idea que puede ser interesante para legacy test, cuando los test son enormes y tienen tantas líneas que no es fácil distinguir qué se está queriendo probar entre toda la maraña.

Para código green field, mi recomendación es llamar a las instancias por su nombre de negocio, porque así nos hacemos una mejor idea de si la API es intuitiva:

1
2
3
4
5
6
7
it("applies the 3x2 to cheap items", () => {
   shoppingCart.addItems(aMouse(), aMouse(), aMouse());

   checkoutOrder = shoppingCart.checkout();

   expect(checkoutOrder.billableItems()).toEqual(aMouse(), aMouse());
});

Al utilizar el nombre de la clase (o módulo) seguida del nombre del método (o función), estamos construyendo frases con sentido de negocio, con mayor expresividad. Es más claro así, que como “sut.checkout”. Otro beneficio cuando rechazamos el uso de “sut”, es que de nuevo obtenemos más pistas sobre cómo agrupar nuestros test. Si estamos probando “shoppingCart”, va a cantar mucho que los test de “billing” estén en el mismo saco, mientras que si a todo le llamamos “sut”, quizá no sea tan evidente.

¿Quieres saber más?

Las tácticas que usamos cuando el código es legado, a menudo distan de las que están a nuestra disposición cuando tenemos la suerte de trabajar en un green field. Todo esto y mucho más sobre cómo escribir test más fáciles de entender y mantener, lo tienes en mi libro, Diseño Ágil con TDD. Si te he gustado esta entrada, te gustará el libro.

Publicado el 02/08/2021 por

¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.

Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?

Impulsamos el crecimiento profesional de tu equipo de developers