Como construir uma biblioteca de testes em JavaScript do zero

Provavelmente você usa alguma biblioteca de testes no seu dia-a-dia - e se não usa, por favor comece. Talvez seja Mocha, Jasmine, ou o Jest. Seja lá qual for a sua biblioteca de testes favoritas, a API provavelmente será próxima disso:

describe("booleans", () => {
  it("false === false", () => {
    expect(false).toBe(false);
  });
  it("true === true", () => {
    expect(true).toBe(true);
  });
});

E é exatamente essa API que vamos implementar nesse post.

describe

Começando pelo mais fácil, essa é a API do describe:

describe("booleans", () => {
  // código aqui
});

Antes de tudo, reconheça que o describe é apenas uma função. Mais nada.

Dado que isso foi feito, olhe por uns instantes, antentamente, para os parâmetros dessa função.

Faça esse exercício por alguns minutos e tente identificar os parâmetros da função describe. Se você não conseguir, tudo bem. A resposta virá em seguida.

O primeiro parâmetro provavelmente foi fácil de identificar: é uma string. Mas e o segundo?

Bem, o segundo também não é tão difícil assim: é uma função. Pra ser mais específico: uma função de callback. Toda função que é passada a outra função como parâmetro, que é então invocada dentro da função externa leva esse nome.

Então se formos implementar a função describe, podemos começar assim:

function describe(description, callback) {}

Segundo passo, vamos logar pro usuário a descrição que foi passada:

function describe(description, callback) {
  console.log(description);
}

E pra terminar, vamos invocar a função de callback que foi enviada:

function describe(description, callback) {
  console.log(description);
  callback();
}

Com isso já podemos utilizar a função describe, dessa forma:

describe("booleans", () => {
  // código aqui
});

E ter como output isso:

$ booleans

it

Observe agora o uso da função describe juntamente com a função it:

describe("booleans", () => {
  it("false === false", () => {
    // código aqui
  });
  it("true === true", () => {
    // código aqui
  });
});

Com exceção do nome, elas são iguais. Tem a mesma funcionalidade e propósito. Portanto, a implementação ficará:

function it(description, callback) {
  console.log(description);
  callback();
}

Com isso já podemos utilizar a função describe e it, dessa forma:

describe("booleans", () => {
  it("false === false", () => {
    // código aqui
  });
  it("true === true", () => {
    // código aqui
  });
});

E ter como output isso:

$ booleans
$ false === false
$ true === true

Hmm, não ficou muito legal. Poderíamos acrescentar um pouco de indentação no log da função it para termos uma noção de hierarquia:

function it(description, callback) {
  console.log(`  ${description}`);
  callback();
}

E com essa melhoria, nosso output fica assim:

$ booleans
$   false === false
$   true === true

expect

Deixei intencionalmente a parte mais difícil por último. Tentarei explicar cada etapa, separadamente, para que não fique tão complicado assim.

Observemos o expect em uso:

expect(false).toBe(false);

Uma das principais diferenças entre os excelentes desenvolvedores (ou desenvolvedoras) e os que são apenas bons, é que os primeiros conseguem quebrar um problema complexo em sub-problemas menores. Então, façamos isso.

Vejamos a primeira parte da função expect:

expect(false);

Agora ficou mais fácil. Podemos implementar essa primeira parte da seguinte forma:

function expect(left) {}

Só que essa parte não vai nos levar muito longe, precisamos da segunda. Então vamos observar novamente a função expect, completa, em uso:

expect(false).toBe(false);

A função .toBe parece ser encadeada da função expect. Podemos tratá-la como um método. Mas pra isso, expect deverá retornar um objeto:

function expect(left) {
  return {
    toBe(right) {},
  };
}

Ótimo. Nesse ponto a sintaxe já funciona, basta agora implementarmos a função toBe. Bem, o que queremos é que se o valor da esquerda (left) for diferente do valor da direita (right), então devemos logar pro usuário o erro:

function expect(left) {
  return {
    toBe(right) {
      if (left !== right) {
        console.log(`    expected "${left}" to be "${right}"`);
      }
    },
  };
}

Com isso podemos utilizar as funções describe, it e expect juntas, dessa forma:

describe("booleans", () => {
  it("false === false", () => {
    expect(false).toBe(true);
  });
});

E ter como output:

$ booleans
$   false === false
$     expected "false" to be "true"

O código completo da nossa versão simplificada de uma biblioteca de testes em JavaScript está aqui.