"Haz lo m谩s simple que pueda funcionar."

-- 鉁嶏笍 Kent Beck

La idea de TDD, Test Driven Development, es hacer antes las pruebas que el c贸digo. 驴Por qu茅?

  • Si haces las pruebas antes... bien, porque al menos tienes las pruebas hechas.
  • Solo tienes que hacer el m铆nimo c贸digo que pase la prueba. Nada m谩s.
  • Para poder probar f谩cilmente, har谩s un c贸digo f谩cil de manejar; mejor dise帽ado.

Todo empieza con los requerimientos

Antes de hacer nada, conviene saber qu茅 vamos a hacer. Esto es, conocer los requerimientos funcionales del software. Te los pueden dar de manera m谩s o menos formal. Pero en cualquier caso t煤 puedes adaptarlos al est谩ndar que mejor te convenga.

A lo largo de este curso he empleado una versi贸n sencilla de las historias de usuario. Creo que cualquiera las puede entender, y hay pocos est谩ndares que exijan menos. Para el caso de las pruebas unitarias lo adaptar茅 tanto que casi deber铆a llamarle historias de programador. Son mucho m谩s granulares y definen el detalle de un proceso, conociendo las tripas del sistema. Son pruebas de caja blanca absoluta, y su documentaci贸n es una por tanto mucho m谩s t茅cnica y precisa.

FEATURE: a BankClient account
As_a: high level service
I_want_to: have a class where deposit money
In_order_to: accumulate several amounts of money for later

Esto a煤n es muy gen茅rico, pero podemos mejorarlo con la especificaci贸n de lo casos de comportamiento esperado. Usando como plantilla el GWT, Given When Then, podemos ir poco a poco haci茅ndolos cada vez m谩s detallados.

Given: a new BankClient objec
When: i make a deposit of 10
Then: returns a balance of 10

De aqu铆 sacaremos directamente las cadenas de texto que acompa帽an a las pruebas, tanto en ejecuci贸n como en desarrollo.

Las pruebas

Las pruebas TDD son pruebas para programadores. Las hacemos por nuestro propio bien. Sin que nos las pidan, sin esperar que las valoren.

Hacemos las pruebas para estar seguros de hacer lo que se pide, nada m谩s, pero bien hecho.

La estructura, los textos y el c贸mo se hacen debe ser a nuestro gusto. Yo te propongo seguir con la estructura AAA y el nombrado GWT. Pero repito, estas pruebas son para ti, es posible que no las vea nadie que no vea el c贸digo. Est谩n al mismo nivel.

Empezamos.

describe('GIVEN: a new BankClient object', () => {
  const sut = new BankClient();
  test('WHEN: i make a deposit of 10 THEN returns a balance of 10', () => {
    const input = 10;
    const actual = sut.deposit(input);
    const expected = 10;
    expect(actual).toEqual(expected);
  });
});

Y la ejecutamos... y falla. 馃敶

La implementaci贸n

Ahora que hemos visto fallar a nuestra prueba, vamos a hacer que la pase. 驴C贸mo? Escribiendo el m铆nimo c贸digo que satisfaga la especificaci贸n funcional descrita.

export class BankClient {
  constructor() {}
  deposit(amount) {
    return 10;
  }
}

Listo 馃煩, v谩monos a casa que se est谩 haciendo de noche.

La mejora

驴Sigues ahi? Ya, te crees que te estoy tomando el pelo. Pero no. Normalmente el m铆nimo c贸digo que pasa una prueba se resuelve con una constante y, la verdad, es poco pr谩ctico. Poco variable mejor dicho.

Es momento de hacer dos cosas. Lo primero enriquecer las pruebas, lo segundo refactorizar el c贸digo.

import { BankClient } from './bank-client';
describe('GIVEN: a new BankClient object', () => {
  let sut;
  beforeEach(() => {
    sut = new BankClient();
  });
  test('WHEN: i make a deposit of 10 THEN returns a balance of 10', () => {
    const input = 10;
    const actual = sut.deposit(input);
    const expected = 10;
    expect(actual).toEqual(expected);
  });
  test('WHEN: i make a deposit of 15 THEN returns a balance of 15', () => {
    const input = 15;
    const actual = sut.deposit(input);
    const expected = 15;
    expect(actual).toEqual(expected);
  });
});

Ok, ya veo d贸nde falla 馃敶. Realmente quieres que se te devuelva lo mismo que ingresas. No puede ser m谩s f谩cil.

export class BankClient {
  constructor() {}
  deposit(amount) {
    return amount;
  }
}

Ahora s铆 que est谩 bien 馃煩.

No tan r谩pido, vamos a seguir enriqueciendo la prueba. Agrega este caso

test('WHEN: i make a deposit of 10 and then a new one of 15 THEN the last one returns the accumulate of 25', () => {
  let input = 10;
  sut.deposit(input);
  input = 15;
  const actual = sut.deposit(input);
  const expected = 25;
  expect(actual).toEqual(expected);
});

Vaya 馃敶, parece que necesitar茅 alg煤n tipo de acumulador... Hag谩moslo

export class BankClient {
  constructor() {
    this.acumlator = 0;
  }
  deposit(amount) {
    this.acumlator += amount;
    return this.acumlator;
  }
}

Correcto de nuevo 馃煩. Imagino que vas pillando el sistema. Pasito a pasito. Escribiendo el c贸digo que pase la prueba. Refin谩ndola para cubrir m谩s casos. Escribiendo c贸digo para pasar la nueva prueba.

El ciclo virtuoso

Este ciclo descrito se completa con un proceso de refactoring, o mejora en el dise帽o. Este trabajo se realiza sobre el c贸digo correcto; lo recalco, es una mejora. Pero al hacerlo sobre c贸digo respaldado por las pruebas nos permite realizar los cambios con plena tranquilidad.

export class BankClient {
  constructor() {
    this.balance = 0;
  }
  deposit(amount) {
    this.balance += amount;
    return this.balance;
  }
}

Peque帽as mejoras constantes 馃挋. Caminando despacio sobre suelo seguro. Es el ciclo virtuoso completo:

馃敶 RED : definir la prueba y comprobar que falla.

馃煩 GREEN : Escribir el m铆nimo c贸digo posible que satisfaga la prueba.

馃挋 REFACTOR : Mejorar dicho c贸digo manteniendo el respaldo de la prueba.

Repetir este ciclo refinando y creando nuevas pruebas hasta completar el requerimiento funcional completo.