"Los probadores de software siempre van al cielo; Ya han tenido su parte de infierno."

-- 鉁嶏笍 Tester an贸nimo

La mayor铆a del software empresarial se ha escrito sin pruebas. Y esto dificulta mucho su mantenimiento. Hacer pruebas sobre c贸digo heredado es costoso y poco atractivo. Pero hay que hacerlo.

Lo contrario implica hacer las m铆nimas modificaciones y siempre con la inquietud de haber roto algo. Desde luego ya no hablamos de refactoring. Si funciona... no lo toques. Y as铆 nos va, con software mal dise帽ado en el cual es dif铆cil arreglar defectos o a帽adir funcionalidad.

Y la soluci贸n pasa por hacer pruebas. Las pruebas autom谩ticas son una inversi贸n rentable que te permite asegurar el funcionamiento de un programa mientras lo modificas para corregir, mantener o mejorar.

C贸digo heredado

Con Jest es relativamente sencillo probar este c贸digo heredado, que quiz谩 sea reciente, quiz谩s incluso sea tuyo. No importa, vamos a ver qu茅 situaciones y problemas nos encontramos.

El ejemplo propuesto es un sistema bancario rid铆culamente simple en tres ficheros.

Se trata de una clase Account con m茅todos de negocio para ingresos y gastos y otra Clerk para ejecutar operaciones y otra Tranasactions almacenar dichas operaciones.

Happy Path

Con lo que sabemos de Jest es f谩cil entender esta prueba; y entendiendo esta prueba es f谩cil adivinar la funcionalidad del programa. Adem谩s seguimos usando el patr贸n given-when-then y los nombrados de variables tambi茅n por convenio sut, input, actual, expected.

import { Account } from './bank/account';

describe('GIVEN a new account with a deposit', () => {
  const sut = new Account();
  const input = 20;
  sut.deposit(input);
  test('SHOULD have the correct balance', () => {
    const actual = sut.getBalance();
    const expected = 20;
    expect(actual).toBe(expected);
  });
});

describe('GIVEN a new account with two deposits', () => {
  const sut = new Account();
  const inputA = 20;
  sut.deposit(inputA);
  const inputB = 10;
  sut.deposit(inputB);
  test('SHOULD accumulate the amounts in the balance', () => {
    const actual = sut.getBalance();
    const expected = 30;
    expect(actual).toBe(expected);
  });
});

Excepciones

驴Todo bien? M谩s o menos. El caso es que este c贸digo puede demostrar que el programa funciona, eso est谩 bien; y se puede extender para realizar pruebas m谩s complejas (manejo de descubiertos, aportaciones m谩s o menos generosas...). Por ejemplo en otro fichero podr铆amos probar las retiradas de dinero y sus l铆mites aceptables.

describe('GIVEN a new account with slightly more withdraw than deposit', () => {
  const sut = new Account();
  const inputDeposit = 15;
  sut.deposit(inputDeposit);
  const inputWithdraw = 20;
  sut.withdraw(inputWithdraw);
  test('SHOULD have a negative balance', () => {
    const actual = sut.getBalance();
    expect(actual).toBeLessThan(0);
  });
});

describe('GIVEN a new account with a lot more withdraw than deposit', () => {
  const sut = new Account();
  const inputDeposit = 15;
  sut.deposit(inputDeposit);
  const inputWithdraw = 200;
  test('SHOULD throw an exception', () => {
    expect(() => sut.withdraw(inputWithdraw)).toThrow();
  });
});

F铆jate por ejemplo c贸mo se prueban las excepciones. Es muy importante comprobar que el c贸digo se comporta de la manera esperada justo en los peores momentos.

Asincronismo

En JavaScript m谩s temprano que tarde te vas a encontrar con c贸digo as铆ncrono. En programaci贸n front-end es el d铆a d铆a; y en el back... tambi茅n.

Pero actualmente tenemos t茅cnicas de desarrollo as铆ncrono muy sencillas como los comandos async y await y que se implementan perfectamente en Jest.

Suponiendo que ahora nuestro sistema almacenase y recuperase las transacciones en un almac茅n remoto, todo el proceso pasar铆a a ser as铆ncrono. Y eso en Jest es casi transparente. Solamente tendremos que anotar las funci贸n de pruebas con los comandos async y await

import { Account } from './bank_async/account';

describe('a new async account with a deposit', () => {
  const sut = new Account();
  const input = 20;
  test('should have the correct balance', async () => {
    await sut.deposit(input);
    const actual = sut.getBalance();
    const expected = 20;
    expect(actual).toBe(expected);
  });
});

A pesar de que este c贸digo es correcto, es posible que te encuentres con que no puedes ejecutar estas pruebas. 驴C贸mo? Pues porque al ser Jest un framework originalmente basado en Node, pues se lleva mal con algunas librer铆as t铆picas del front. Yo lo he forzado en el laboratorio usando fetch para las llamadas remotas. Forc茅 el problema para mostrarte una soluci贸n.

Mocks

La teor铆a del testing nos dice que podemos, casi debemos, usar dobles en lugar de dependencias reales. Sobre todo si estas nos impiden ejecutar las pruebas, como en este caso. As铆 que recurro a un mock de las funciones fetch.

Afortunadamente no hay que programar nada porque los problemas comunes tienen soluciones comunes y p煤blicas. En este caso con el paquete est-fetch-mock que se instala y luego se invoca en una sola instrucci贸n.

require('jest-fetch-mock').enableMocks();
import { Account } from './bank _async/account';

驴Entonces? Ya est谩 todo bien, 驴no?. Mas o menos, pues la trampa est谩 en que es todo esto es una prueba de integraci贸n.

En esta prueba estamos ejercitando a Acount y sin querer a a todas sus dependencias. Es decir damos por bueno que Clerk y Transactions tambi茅n funcionan. 驴Y eso es malo? No necesariamente; si la prueba pasa es una buena dosis de confianza. Pero si no la pasa... entonces no sabremos gran cosa sobe el motivo del fallo.

Pero tras refrescar el concepto de mock la mejora de esta situaci贸n est谩 cerca.