Ao escrever testes unitários, as vezes perdemos muito tempo testando algumas repetições ou chamadas que não são de nossas responsabilidade testar.

Gosto de usar algumas abordagens para escrever menos testes e que eles tenham mais consistencia cobrindo o que realmente importa. 1

Cobrir o que realmente importa é ir além dos 100% do coverage, para exemplificar usarei uma implementação recente que fiz em uma API.

Um pouco sobre o trecho à ser testado e também os casos do teste, o objetivo é encapsular a biblioteca Joi e retornar uma promise. Aqui temos alguns casos interessantes para exemplificar testes.

Geralmente testes de códigos assincronos é uma zona cinza para boa parte dos desenvolvedores de software e por isso escolhi ele como exemplo, mas antes quero explicar como penso ao escrever testes.

  1. Testes são referencias e explicações para o futuro, de um trecho de código.

  2. O teste deve ser claro e simples.

  3. Os testes devem ser rápidos e fáceis de serem escritos e executados, quero entregar software funcionando, mais do que documentação extensa.

Vamos ao código:

function Validating(data, schema) {
  return new Promise((resolve, reject) => {
    if (!data) return reject('No data');
    if (!schema) return reject('No schema');

    Joi.validate(data, schema, (err, result) => {
      err ? reject(err) : resolve(result);
    });
  });
}

(Para facilitar a leitura, removi alguns colchetes e quebras de linha do código original)

Foco no que importa

Não precisamos testar se a função existe ou foi definida, afinal esse seria um dos primeiros erros que encontrariamos ao rodar os testes, então deveriamos testar o retorno da chamada da função? Também não!

Apesar de um teste unitário checar entradas e saídas de funções, o comportamento das entradas e saídas são o que realmente importam, testar se uma promise foi retornada, náo ajuda muito à saber os resultados reais dessa chamada e o teste quebra ao usarmos alguns items em que não há uma promise.

No caso do código acima, a primeira checagem que podemos fazer é se a promise é rejeitada ao chamar uma função sem os argumentos necessários.

Para isso escrevemos o seguinte caso de teste:

it('expect a promise rejection on call without parameters', (done) => {
  Validating().catch((result) => {
    expect(result).to.have.string('No data');
    done();
  });
});

Por que escrever o expect no catch dessa chamada?

Neste primeiro caso de teste a definição é ter uma promise rejeitada após a chamada sem os argumentos data e schema. Testar o resultado no catch facilitar por pegarmos o resultado esperado na ordem da função.

Não preciso criar stubs, spies, testar, validar se uma promise foi retornada, ao chamar o Validating isso já está implicito. Qualquer alteração no código que remova a promise, vai estourar excessão por não possuir o catch da promise.

Vamos observar o segundo ponto que envolve dois casos de testes parecidos, mas que tem responsabilidades de checagem diferentes.

it('expect a promise rejection on call with no data', (done) => {
  Validating(null, fakeSchema).catch((result) => {
    expect(result).to.have.string('No data');
    done();
  });
});

it('expect a promise rejection on call with no schema', (done) => {
  Validating(fakeData, null).catch((result) => {
    expect(result).to.have.string('No schema');
    done();
  });
});

Nestes casos de testes, primeiro checamos a chamada sem o parâmetro data e o segundo caso sem o parâmetro schema, com isso ajudamos a cobrir dois cenários possíveis de erro e que evita uma chamada mais complexas que também iria ser um erro, esse tipo de código poupa recursos e ajuda em uma resposta rápida de um resultado inválido por falta dos argumentos corretos.

Apesar dos 3 casos de testes mostrados até aqui serem muito semelhantes eles checam comportamentos diferentes. Isso garante maior resiliencia do código que escrevemos.

Validando chamadas assíncronas

Em nosso terceiro caso, vamos testar as chamadas e o resultado do joi.validate, precisamos testar se essas chamada ocorre e também se não existe modificações ao longo da execução.

it('expect first argument on call validate be object', () => {
  expect(joiValidatetub.callsArg(0)).to.be.a('object');
  Validating({}, fakeSchema).catch();
}); 

it('expect second argument on call validate be a function', () => {
  expect(joiValidatetub.callsArg(1)).to.be.a('function');
  Validating({}, fakeSchema).catch();
});

it('expect third argument on call validate be a function', () => {
  expect(joiValidatetub.callsArg(2)).to.be.a('function');
  Validating({}, fakeSchema).catch();
});

Testando dessa forma evitamos gastar tempo, por exemplo, se o joi.validate existe e ao invés disso verificamos se as chamadas ocorrem com os parâmetros corretos, já que está implícito que essa chamada ocorre.

Já disse foco? Então vamos de novo

Agora temos os dois últimos cenários são bem simples de testar, o retorno de um promise com uma falha, após a chamada da lib externa e o caso de sucesso.

  it('expect promise rejection after error on call joi validate', (done) => {
    joiValidatetub.callsFake((data, schema, cb) => {
      cb({message: 'err'});
    });

    Validating({}, fakeSchema).catch((err) => {
      expect(err).to.have.property('message');
      expect(err.message).to.be.equal('err');
      done();
    });
  });

  it('expect promise resolves with sucess after call joi validate', () => {
    joiValidatetub.callsFake((data, schema, cb) => {
      cb(null, { message: 'sucess' });
    });

    Validating({}, fakeSchema).then((sucess) => {
      expect(sucess).to.have.property('message');
      expect(sucess.message).to.be.equal('sucess');
      done();
    }).catch((err) => {
    })
  });

Após validar se o Joi foi chamado corretamente, mas sem me importar com a implementação do mesmo, podemos testar qual é o comportamento da função e se não houve modificações.

Para testar isso, criamos um stub do joi e respondemos a chamada de acordo com o que queremos testar e com isso chamamos o callback que nos foi passado, para que sejam ativados a rejeição da promise ou o sucesso da mesma.


Se você gostou desse passo a passo de como crio os testes unitários para serem simples e rápidos, me fala no twitter, também se tiver qualquer dúvida, só me falar por lá.