A la hora de realizar una prueba unitaria en Javascript, la mayoría de los programadores nos preguntamos: ¿Qué debería cubrir mi prueba? ¿Qué framework debería usar?¿Estoy realizando el test correctamente?

En este post intentaré responder a cada una de esas preguntas para facilitaros el trabajo a la hora de escribir una prueba unitaria.

¿Qué es un framework de pruebas unitarias?

Un framework de pruebas unitarias es una herramienta que nos permite escribir pruebas sobre un bloque de código, ejecutándolo bajo un entorno de javascript sin necesidad de interferir en el IDE ni en la propia aplicación.

¿Para qué sirven los test unitarios?

Los test unitarios sirven para asegurarnos de que un bloque de nuestro código, función o clase, funcionan correctamente y abarca la mayoría de casos de uso que se puedan dar. Además, proporciona robustez y calidad a nuestro código y confirma que funciona correctamente.

¿Qué debería cubrir mi prueba unitaria?

Nuestra prueba unitaria deberá cubrir todas las posibles funcionalidades dentro de un bloque de código. Por ejemplo, dado el siguiente código, vamos a analizar qué casos de uso nos hacen falta:

const objectMapping = {
    ACTIVE: 'Activo',
    INACTIVE: 'Inactivo',
    OBSOLETE: 'Obsoleto',
};

function getObjectDescription(type) {
    if (!type) {
        return "El argumento 'type' no existe";
    }
    return objectMapping[type];
}

export default getObjectDescription;

Tenemos un módulo que nos exporta el método “getObjectDescription” al que pasándole como parámetro el “type” nos devuelve un “string”. Ahora bien, identifiquemos los diferentes casos que se nos pueden dar con dicho módulo:

  1. Qué “getObjectDescription” exista.
  2. Llamar al método “getObjectDescription” con un parámetro que exista en el “objectMapping”.
  3. Llamar al método “getObjectDescription” con un parámetro que no exista en el “objectMapping”.
  4. Llamar al método “getObjectDescription” con un parámetro vacío.
  5. Llamar al método “getObjectDescription” sin parámetro.

Estos son, a primera vista, los casos de uso que se pueden presentar en dicho bloque de código.

¿Qué framework de pruebas unitarias debería usar?

Actualmente, disponemos de muchos frameworks para realizar las pruebas de nuestro código en Javascript, estos son los más populares del año pasado:

Jest

  • Es el framework de test de Facebook, con el que se hacen los test de React, pero también se puede usar con otros frameworks como Vue o Angular, incluso con VanillaJs.
  • Es muy rápido y muy fácil de aprender si empiezas con los test.
  • Nos permite hacer test por snapshot (test para asegurarnos de que nuestra interfaz de usuario no cambia).
  • Nos da la posibilidad de crear Spies para métodos y mocks de manera muy sencilla.

Mocha

  • Es uno de los frameworks más usados desde hace bastantes años.
  • Se puede usar junto con otros frameworks de test, como Chai o Enzyme.
  • Dispone de una gran cantidad de documentación.
  • Es una herramienta muy flexible debido a la gran comunidad que lo respalda.

Jasmine

  • Es el framework recomendado por Angular para los test unitarios.
  • Es fácil empezar con este framework, con una curva de aprendizaje suave.
  • Jest está basado en Jasmine.

Aunque podríamos analizar uno a uno dichos frameworks, como el objetivo del post es aprender a escribir bien nuestros test, optaremos por Jest y veremos cómo escribir correctamente un test en dicho framework.

¿Estoy realizando el test correctamente?

Antes de empezar a escribir nuestro test hay ciertas cosas que deberíamos conocer o, por lo menos, que nos resulten familiares:

describe(‘[group description], () => …);

  • Nos permite agrupar nuestros bloques de test en un mismo fichero de manera separada y así tener más organizado nuestro bloque de test. Por ejemplo, si queremos probar diferentes partes de un mismo método que contenga “if” o “switch”.

beforeEach(() => …);

  • Se escribe dentro de nuestra agrupación (describe). Ejecuta un código antes de cada uno de los test, por ejemplo, para reinicializar una clase, componente, método…

afterEach(() => …);

  • Se escribe dentro de nuestra agrupación (describe) y ejecuta un código después de cada uno de los test. Nos sirve para reinicializar una clase, componente, método…

beforeAll(() => …);

  • Se escribe dentro de nuestra agrupación (describe) y ejecuta un código antes de acabar los test, como reinicializar una clase, componente, método…

afterAll(() => …);

  • Se escribe dentro de nuestra agrupación (describe). Ejecuta un código después de acabar los test. Lo utilizamos para reinicializar una clase, componente, método...

it(‘[test description]’, () => …);

  • Dentro de cada uno se escriben nuestros test y los agrupamos mediante el describe.

expect()...

  • Se escribe dentro de nuestro test (it) para decir qué esperamos que ocurra con lo que hemos escrito.

Empezamos con nuestra prueba (recordad que debemos cubrir los puntos descritos anteriormente). Pero, antes, deberemos preparar nuestro fichero de test. Por lo general, los ficheros de test se suelen ubicar en la misma carpeta donde se encuentra el fichero que vamos a usar para las pruebas, poniéndole el mismo nombre seguido de .spec.js o .test.js. Personalmente, prefiero tener separado los ficheros de pruebas de los de la aplicación. Por ello, suelo crearme una carpeta con los test a la misma altura de donde se encuentran las fuentes, siguiendo una estructura similar a la contenida en src.

Volvamos a la generación de nuestro fichero de test. Todo fichero de test tiene la siguiente estructura base:

import getObjectDescription from [ruta de nuestro fichero];
describe('Test unitarios de nuestro módulo "getObjectDescription"', () => {
    // En el caso de querer reiniciar o volver a instanciar nuestro módulo usaremos los métodos “beforeEach”, “afterEach”, “beforeAll” o “afterAll” según nos convenga.
});

Una vez que tengamos nuestra estructura de fichero, vamos con los test:

  1. Que “getObjectDescription” exista.
it('getObjectDescription to be truthy, () => {
// Con toBeTruthy estamos diciendole a jest que esperamos que exista nuestro método
    expect(getObjectDescription).toBeTruthy();
});
  1. Llamar al método “getObjectDescription” con un parámetro que exista en el “objectMapping”. Dicho método nos devuelve un string que nos sirve para comprobar que lo que nos devuelve, es algo que existe: Deberemos almacenarlo en una variable y comprobar dicha variable.
it("getObjectDescription('ACTIVE') to be 'Activo'", () => {
    const result = getObjectDescription('ACTIVE');
expect(result).toBe('Activo'); // Ponemos 'Activo' porque es el valor de nuestro objeto
}); 
  1. Llamar al método “getObjectDescription” con un parámetro que no exista en el “objectMapping”.
it("getObjectDescription('DRAFT') to be false", () => {
    const result = getObjectDescription('DRAFT');
expect(result).toBeFalsy(); // toBeFalsy comprueba si el valor es nulo/undefined/false
});
  1. Llamar al método “getObjectDescription” con un parámetro vacío. Exactamente igual al test anterior.
it("getObjectDescription('') to be false", () => {
    const result = getObjectDescription(');
expect(result).toBeFalsy(); // toBeFalsy comprueba si el valor es nulo/undefined/false
}); 
  1. Llamar al método “getObjectDescription” sin parámetro.
it("getObjectDescription() to be 'El argumento 'type' no existe'", () => {
    const result = getObjectDescription(');
expect(result).toBe('El argumento 'type' no existe');
}); 

Y nuestro test debería quedar algo parecido a lo siguiente:

import getObjectDescription from [ruta de nuestro fichero];

describe('Test unitarios de nuestro módulo "getObjectDescription"', () => {
// En el caso de querer reiniciar o volver a instanciar nuestro módulo usaremos los métodos "beforeEach", "afterEach", "beforeAll" o "afterAll" según nos convenga.

    it('getObjectDescription to be truthy, () => {
// Con toBeTruthy estamos diciendole a jest que esperamos que exista nuestro método
    expect(getObjectDescription).toBeTruthy();
});

it("getObjectDescription('ACTIVE') to be 'Activo'", () => {
    const result = getObjectDescription('ACTIVE');
expect(result).toBe('Activo'); // Ponemos 'Activo' porque es el valor de nuestro objeto
});

it("getObjectDescription('DRAFT') to be false", () => {
    const result = getObjectDescription('DRAFT');
expect(result).toBeFalsy(); // toBeFalsy comprueba si el valor es nulo/undefined/false
});

it("getObjectDescription('') to be false", () => {
    const result = getObjectDescription(');
expect(result).toBeFalsy(); // toBeFalsy comprueba si el valor es nulo/undefined/false
});

it("getObjectDescription() to be 'El argumento 'type' no existe'", () => {
    const result = getObjectDescription(');
expect(result).toBe('El argumento 'type' no existe');
}); 

});

Conclusiones

Escribir un test unitario no es algo complejo, pero sí que lleva su tiempo. Normalmente, si en realizar una funcionalidad se tarda 1 hora, para realizar los test de dicha funcionalidad se suele calcular multiplicando por tres el tiempo de desarrollo, ya que tenemos que abarcar, a ser posible, un mínimo del 90% de los posibles casos que se puedan dar: por ejemplo, cuando el código se ejecuta correctamente o cuando queremos controlar una excepción.
Todo ello conlleva unas acciones que, para tener un test correcto, hay que cubrir en la mayoría de posibilidades o casos que se puedan dar en nuestro código.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.