Front End

13 fev, 2017

Testes em Javascript: diferença entre Fake, Spy, Stub e Mock

Publicidade

Fala galera, beleza?!

Esse artigo vai fazer parte do livro “construindo APIs testáveis com Node.js”, então todos os feedbacks são muito bem-vindos.

Resolvi escrever sobre a diferença entre fake, spy, stub e mock, pois é comum confundir os termos e também não saber qual testDouble usar para cada cenário. Opa, testeDouble? O que é isso?

testDouble

Testar código com ajax, network, timeouts, banco de dados e outras dependências que produzem efeitos colaterais é sempre complicado. Por exemplo, quando se usa ajax, ou qualquer outro tipo de networking, é necessário comunicar com um servidor que irá responder para a requisição; já com o banco de dados, será necessário inicializar um serviço para tornar possível o teste da aplicação: limpar e criar tabelas para executar os testes etc.

Quando as unidades que estão sendo testadas possuem dependências que produzem efeitos colaterais, como os exemplos acima, não temos garantia de que a unidade está sendo testada isoladamente. Isso abre espaço para que o teste quebre por motivos não vinculados a unidade em si, como, por exemplo, o serviço de banco não estar disponível ou uma API externa retornar uma resposta diferente da esperada no teste.

Há alguns anos, Gerard Meszaros publicou o livro XUnit Test Patterns: Refactoring Test Code e introduziu o termo Test Double (traduzido como “dublê de testes”) que nomeia as diferentes maneiras de substituir dependências. A seguir, vamos conhecer os mais comuns test doubles e quais são suas características, prós e contras.

Na prática

Para facilitar a explicação, será utilizado o mesmo exemplo para os diferentes tipos de test doubles; também será usada uma biblioteca de suporte chamada Sinon.js que possibilita a utilização de stubs, mocks e spies.

A controller abaixo é uma classe que recebe um banco de dados como dependência no construtor. O método que iremos testar unitariamente dessa classe é o método “getAll”. Ele retorna uma consulta do banco de dados com uma lista de usuários.

const Database = {
  findAll() {}
}

class UsersController {
  constructor(Database) {
    this.Database = Database;
  }

  getAll() {
    return this.Database.findAll('users');
  }
}

Fake

Durante o teste, é frequente a necessidade de substituir uma dependência para que ela retorne algo específico, independente de como for chamada, com quais parâmetros, quantas vezes, a resposta sempre deve ser a mesma. Nesse momento, a melhor escolha são os Fakes. Fakes podem ser classes, objetos ou funções que possuem uma resposta fixa independente da maneira que forem chamadas.

O exemplo abaixo mostra como testar a classe UsersController usando um fake:

describe('UsersController getAll()', () => {
  it('should return a list of users', () => {
    const expectedDatabaseResponse = [{
      id: 1,
      name: 'John Doe',
      email: 'john@mail.com'
    }];

    const fakeDatabase = {
      findAll() {
        return expectedDatabaseResponse;
      }
    }
    const usersController = new UsersController(fakeDatabase);
    const response = usersController.getAll();

    expect(response).to.be.eql(expectedDatabaseResponse);
  });
});

Nesse caso de teste não é necessária nenhuma biblioteca de suporte, tudo é feito apenas criando um objeto fake para substituir a dependência do banco de dados. O método “findAll” passa a ter uma resposta fixa, que é uma lista com um usuário.

Para validar o teste, é necessário verificar se a resposta do método “getAll” do controller responde com uma lista igual a declarada no “expectedDatabaseResponse”.

Vantagens:

  • Simples de escrever;
  • Não necessita de bibliotecas de suporte;
  • Desacoplado da dependencia original.

Desvantagens:

  • Não possibilita testar múltiplos casos;
  • Só é possível testar se a saída está como esperado, não é possível validar o comportamento interno da unidade.

Quando usar fakes? Fakes devem ser usados para testar dependências que não possuem muitos comportamentos ou somente para preenchimento de argumentos.

Spy

Como vimos anteriormente, os fakes permitem substituir uma dependência por algo customizado, mas não possibilitam saber, por exemplo, quantas vezes uma função foi chamada, quais parâmetros ela recebeu etc. Para isso existem os spies, como o próprio nome já diz, eles gravam informações sobre o comportamento do que está sendo “espionado”.

No exemplo abaixo é adicionado um spy no método “findAll” do Database para verificar se ele está sendo chamado com os parâmetros corretos:

describe('UsersController getAll()', () => {
  it('should findAll users from database with correct parameters', () => {
    const findAll = sinon.spy(Database, 'findAll');

    const usersController = new UsersController(Database);
    usersController.getAll();

    sinon.assert.calledWith(findAll, 'users');
    findAll.restore();
  });
});

Note que é adicionado um spy na função “findAll” do Database. Dessa maneira, o Sinon devolve uma referência a essa função e também adiciona alguns comportamentos a ela que possibilitam realizar checagens como “sinon.assert.calledWith(findAll, ‘users’)” onde é verificado se a função foi chamada com o parâmetro esperado.

Vantagens:

  • Permite melhor assertividade no teste;
  • Permite verificar comportamentos internos;
  • Permite integração com dependências reais.

Desvantagens:

  • Não permitem alterar o comportamento de uma dependência;
  • Não é possível verificar múltiplos comportamentos ao mesmo tempo.

Quando usar spies? Spies podem ser usados sempre que for necessário ter assertividade de uma dependência real ou, como em nosso caso, em um fake. Para casos onde é necessário ter muitos comportamos, é provável que stubs e mocks venham melhor a calhar.

Stubc

Fakes e spies são simples e substituem uma dependência real com facilidade, como visto anteriormente, porém, quando é necessário representar mais de um cenário para a mesma dependência eles podem não dar conta. Para esse cenário, entram na jogada os Stubs, que são spies que conseguem mudar o comportamento dependendo da maneira em que forem chamados, veja o exemplo abaixo:

describe('UsersController getAll()', () => {
  it('should return a list of users', () => {
    const expectedDatabaseResponse = [{
      id: 1,
      name: 'John Doe',
      email: 'john@mail.com'
    }];

    const findAll = sinon.stub(Database, 'findAll');
    findAll.withArgs('users').returns(expectedDatabaseResponse);

    const usersController = new UsersController(Database);
    const response = usersController.getAll();

    sinon.assert.calledWith(findAll, 'users');
    expect(response).to.be.eql(expectedDatabaseResponse);
    findAll.restore();
  });
});

Quando usamos stubs, podemos descrever o comportamento esperado, como nessa parte do código:

findAll.withArgs('users').returns(expectedDatabaseResponse)

Quando a função “findAll” for chamada com o parâmetro “users”, retornara a resposta padrão.

Com stubs é possível ter vários comportamentos para a mesma função com base nos parâmetros que são passados, essa é uma das maiores diferenças entre stubs e spies.

Como dito anteriormente, stubs são spies que conseguem alterar o comportamento. É possível notar isso na asserção “sinon.assert.calledWith(findAll, ‘users’)”; ela é a mesma asserção do spy anterior. Nesse teste, são feitas duas asserções apenas para mostrar a semelhança com spies, pois múltiplas asserções em um mesmo caso de teste é considerado uma má prática.

Vantagens:

  • Comportamento isolado;
  • Diversos comportamentos para uma mesma função;
  • Bom para testar código assíncrono.

Desvantagens:

  • Assim como spies, não é possível fazer múltiplas verificações de comportamento.

Quando usar stubs? Stubs são perfeitos para utilizar quando a unidade tem uma dependência complexa, que possui múltiplos comportamentos. Além de serem totalmente isolados, eles também têm o comportamento de spies, o que permite verificar os mais diferentes tipos de comportamento.

Mock

Mocks e stubs são comumente confundidos, pois ambos conseguem alterar comportamento e também armazenar informações. Mocks também podem ofuscar a necessidade de usar stubs, pois eles podem fazer tudo que stubs fazem. O ponto de grande diferença entre mocks e stubs é sua responsabilidade: stubs tem a responsabilidade de se comportar de uma maneira que possibilite testar diversos caminhos do código, como, por exemplo, uma resposta de uma requisição http ou uma exceção. Já os mocks substituem uma dependência permitindo a verificação de múltiplos comportamentos ao mesmo tempo.

O exemplo a seguir mostra a classe UsersController sendo testada utilizando Mock:

describe('UsersController getAll()', () => {
  it('should call database with correct arguments', () => {
    const databaseMock = sinon.mock(Database);
    databaseMock.expects('findAll').once().withArgs('users');

    const usersController = new UsersController(Database);
    usersController.getAll();

    databaseMock.verify();
    databaseMock.restore();
  });
});

A primeira coisa a se notar no código é a maneira de fazer asserções com Mocks. Elas são descritas nessa parte:

"databaseMock.expects('findAll').once().withArgs('users')"

Nela são feitas duas asserções, a primeira para verificar se o método “findAll” foi chamado uma vez e na segunda se ele foi chamado com o argumento “users”, após isso o código é executado e é chamada a função “verify()” do Mock que irá verificar se as expectativas foram atingidas.

Vantagens:

  • Verificação interna de comportamento;
  • Diversos asserções ao mesmo tempo.

Desvantagens:

  • Diversas asserções ao mesmo tempo podem tornar o teste difícil de entender.

Quando usar mocks? Mocks são úteis quando é necessário verificar múltiplos comportamentos de uma dependência. Isso também pode ser sinal de um design de código mal pensado, onde a unidade tem muita responsabilidade. É necessário ter muito cuidado ao usar Mocks, já que eles podem tornar os testes pouco legíveis.

Espero que seja útil pessoal!

Referencias: