"Es dif铆cil encontrar un error cuando lo est谩s buscando; es a煤n m谩s dif铆cil cuando supones que el c贸digo est谩 libre de errores."

-- 鉁嶏笍 Steve McConnell

Las pruebas de c贸digo deben ser f谩ciles de preparar y c贸modas de ejecutar. Estas son las cualidades que aporta Jest al mundo del testing. Es sencillo y c贸modo. Vamos a verlo por pasos.

Jest

Jest es un framework para pruebas unitarias. Centrado en determinar que todo tu c贸digo es correcto. Para ello vamos a empezar por el principio.

Instalar Jest

Instalarlo es cosa de ni帽os. Partimos de una aplicaci贸n Node y agregamos la dependencia. Puedes hacerlo con npm o con yarn y guardarlo como dependencia principal o para desarrollo.

yarn add jest
npm i --save jest

Y ya est谩. Aunque yo recomiendo agregar unas dependencias extra para facilitar a煤n mas el uso. Incluyo el analizador de c贸digo eslint , el embellecedor prettier y el empaquetador babel. En VSCode tambi茅n activo la extensi贸n orta.vscode-jest

{"devDependencies": {"@babel/core": "^7.11.5","@babel/preset-env": "^7.11.5","@types/jest": "^26.0.13","babel-jest": "^26.3.0","eslint": "^7.8.1","eslint-config-prettier": "^6.11.0","eslint-plugin-jest": "^23.20.0","eslint-plugin-prettier": "^3.1.4","jest": "^26.4.2","jest-fetch-mock": "^3.0.3","prettier": "^2.1.1"}}

Configuraci贸n

Nada. Como lo oyes, no hay nada que hacer. Simplemente ejecutarlo y listo. Por supuesto que lo puedes adaptar o manipular, pero s贸lo lo haremos cuando lo necesitemos.

Ya, pero quieres saber c贸mo... Pues hay varias opciones; a mi me gusta configurarlo en su propio fichero jest.config.js.

// For a detailed explanation regarding each configuration property, visit:// https://jestjs.io/docs/en/configuration.html
module.exports = {
  verbose: true};

En este caso le he dicho que quiero explicaciones detalladas de lo que va ocurriendo.

Ejecuci贸n

Las pruebas se pueden lanzar desde l铆nea de comandos o mejor escript谩ndolo en el package.json. Algo as铆 es suficiente para empezar.

{"scripts": {"test": "jest --watch -o"}}

Aqu铆 le decimos que se lancen todas las pruebas, que vigile los cambios para relanzarlas, pero s贸lo aquellas afectadas desde el 煤ltimo commit. Tienes m谩s informaci贸n en la p谩gina dedicada al CLI de Jest

Desarrollo de pruebas con Jest

驴Ejecutar? 驴El qu茅? Pues una especificaci贸n, o en el argot de Jest un fichero tu-prueba.spec.js.

Vamos a realizar un conjunto de pruebas muy sencillo. Dado un c贸digo en JavaScript como este:

export const basic = {
  balance: 0,
  deposit(amount) {
    this.balance += amount;
  },
  withdraw(amount) {
    this.balance -= amount;
  }
};

Ya lo s茅, es muy b谩sico; pero esto es solo un Hola Mundo. Lo puedes ver en el repositorio del laboratorio

Hola Mundo

import { basic } from './basic';

test('basic exists', () => {
  expect(basic).toBeDefined();
});

test('basic balance is 0', () => {
  expect(basic.balance).toEqual(0);
});

Evidente, 驴no?. Si somos novatos en las pruebas s贸lo tenemos que familiarizarnos con un concepto com煤n a otros frameworks.

Las pruebas se definen como funciones dentro de otras funciones que las ejecutan.

En este caso la funci贸n test(), tambi茅n usable bajo el alias it() es la funci贸n clave de todas tus pruebas Jest.

Es una funci贸n que recibe dos argumentos. El primero es una cadena que describe la prueba y el segundo es otra funci贸n con la prueba en s铆.

Dentro de esa funci贸n interna encontraremos llamadas a m谩s funciones del framework; que casi siempre seguir谩n una sint谩is similar a esta expect(recibido).toEqual(esperado);

Comportamiento unitario

Pon un poco de orden en tus pruebas. Ya hemos visto en Cypress que las pruebas deben ser limpias. Es decir, que debe haber un orden y una estructura similar de una prueba a otra.

A mi me gusta tomar prestado el Given When Then de las pruebas de comportamiento, pero tambi茅n es muy usado el simple it should para decir en lenguaje humano lo que deber铆a ocurrir. Y eso es lo importante, que quede muy claro el objetivo de la prueba. Y para eso no ahorres texto en las descripciones de tus test. Recuerda el acr贸nimo DAMP (Descriptive And Meaningful Phrases).

Estos son ejemplos un poco m谩s elaborados, pero igualmente sencillos. Lo importante es que el resultado sea significativo. Y que el c贸digo que lo acompa帽e responda a las expectativas.

describe('GIVEN: a basic object', () => {
  test('WHEN: read the balance THEN returns 0', () => {
    expect(basic.balance).toEqual(0);
  });
  test('WHEN: make a deposit of 6 THEN should have a balance of 6', () => {
    const input = 6;
    basic.deposit(input);
    const actual = basic.balance;
    const expected = 6;
    expect(actual).toEqual(expected);
  });
});

F铆jate tambi茅n en la nomenclatura propuesta para las variables. Adem谩s de aclarar mucho la intenci贸n a qui茅n lo lea, tambi茅n puede servirte de gu铆a cuando est谩s empezando.

  • input : valores de entrada
  • actual : valores reales obtenidos
  • expected : resultado esperado

驴Te recuerda a algo?. S铆 a la triple AAA: Arrange, Act Assert . No importa tanto qu茅 convenio sigas. Pero aseg煤rate de seguir uno.

Antes de nada

Vas a tener ocasiones en las que la preparaci贸n de la pruebas sea compleja. En otros casos ser谩 la propia prueba la que sea compleja. Vamos a ver estas dos situaciones, aunque por supuesto sigamos en un escenario muy b谩sico.

describe('GIVEN: a basic object with a previous balance of 6', () => {
  beforeEach(() => {
    basic.balance = 0;
    const input = 6;
    basic.deposit(input);
  });
  test('WHEN: make a withdraw of 4 THEN should have a balance of 2', () => {
    const input = 4;
    basic.withdraw(input);
    const actual = basic.balance;
    const expected = 2;
    expect(actual).toEqual(expected);
  });
  test('WHEN: make a withdraw of 8 THEN should have a balance of -2', () => {
    const input = 8;
    basic.withdraw(input);
    const actual = basic.balance;
    const expected = -2;
    expect(actual).toEqual(expected);
  });
});

Aqu铆 hacemos uso de la funci贸n beforeEach() de Jest que te permite ejecutar un c贸digo com煤n a diversas pruebas. Vale, eso est谩 bien, pero tambi茅n es cierto que las funciones empiezan a crecer y a tener mucho c贸digo similar, cuando no redundante. Necesitamos una soluci贸n com煤n.

Vuelve la triple AAA

Lo s茅 me encantan los acr贸nimos. Tengo poca memoria y es una manera de acordarme y empezar con una estructura. Aqu铆 lo voy a hacer creando unas funciones de ayuda que ir茅 invocando o asignando seg煤n necesite.

describe('GIVEN: a basic object with a previous balance of 10 WHEN: i ask for a borrow of 4', () => {
  beforeAll(() => {
    arrangeBalance();
    const input = 4;
    actBorrow(input);
  });
  test('THEN should have a balance of 14', assertBalance);
  test('AND THEN should have disposed of 4', assertDisposed);
});
function arrangeBalance() {
  const input = 10;
  basic.balance = input;
}
function actBorrow(input) {
  basic.borrow(input);
}
function assertBalance() {
  const actual = basic.balance;
  const expected = 14;
  expect(actual).toEqual(expected);
}
function assertDisposed() {
  const actual = basic.disposed;
  const expected = 4;
  expect(actual).toEqual(expected);
}

Estas micro funciones cumplen varios cometidos. Al tener nombre dejan un rastro claro para casos de fallos. Por supuesto que el nombre tambi茅n sirve para aclarar el prop贸sito a otro programador. Y ah铆 es d贸nde entra Arrange, Act, Assert asignando a cada funci贸n un cometido 煤nico: preparar, ejercitar el c贸digo y comprobar el resultado. Adem谩s, algunas veces son reutilizables; pero eso no es lo fundamental.

Sin fallos

Todo c贸digo puede fallar. Pero si la la excepci贸n ya es esperada, entonces la prueba debe tambi茅n comprobar que lo excepcional funciona como se espera.

  borrow(amount) {
    if (amount + this.disposed > this.balance) {
      throw "you can't request so much credit";
    }
    this.disposed += amount;
    this.balance += amount;
  },

Esta funci贸n comprueba una condici贸n l贸gica y lanza un error si no se cumple. Debemos capturar ese error y asegurar que se lanza cuando es preciso.

describe('GIVEN: a basic object with a previous balance of 10 WHEN: i ask for a borrow of 40', () => {
  beforeAll(() => {
    arrangeBalance();
  });
  test('THEN should receive an error', assertDisposedThrowsError);
});
function arrangeBalance() {
  const input = 10;
  basic.balance = input;
}
function assertDisposedThrowsError() {
  const input = 14;
  expect(() => actBorrow(input)).toThrow();
}

Aqu铆 la sintaxis es un poquit铆n rara, pero necesaria para que Jest haga internamente el trabajo sucio de envolver en try{}catch(e){} tu sujeto de pruebas.

C贸digo cubierto

Al lanzar las pruebas se ejecuta tambi茅n el sujeto de pruebas, el SUT. Pero 驴Cu谩ntas l铆neas se ejercitan? Pues depende de lo cuidadoso que seas al especificar las condiciones y las expectativas. Al porcentaje de l铆neas que se ejecutan sobre el total se le llama cobertura.

En principio una mayor cobertura es un signo de mayor confianza en la prueba. Pero no es determinante. De todas formas se considera que 80% es un buen indicador y Jest te ofrece ese dato y otros muchos.

Como siempre, te sugiero que escriptes todos tus comando de consola.

  {"coverage": "jest src/unit/basic/basic.spec.js --collect-coverage",}

Solicitar el informe de cobertura de c贸digo es as铆 de simple; pero necesita que en la configuraci贸n le especifique qu茅 umbrales consideras aptos. Yo suelo utilizar algo as铆 en el jest.config.js:

module.exports = {
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80
    },
  },
}

En el se incluyen el famoso 80% para l铆neas, ramas condicionales y funciones. Si est谩s empezando con las pruebas no te obsesiones con esta m茅trica. Cada test que hagas estar谩s m谩s cerca del objetivo: tener confianza en tu c贸digo y dormir tranquilamente.

Tienes el ejemplo completo en el laboratorio del curso de Testing con Jest.