Conceitos de Mocking - Testing Series #2

O que é Mocking

LinkO que é mocking?

Bem-vindo ao segundo artigo da nossa série de testes de software! O último artigo explorou testes estáticos, unitários, de integração e de ponta a ponta.

Neste artigo, exploraremos:

  1. O que é mocking?
  2. Quais são os principais conceitos por trás disso?
  3. Quando usar mocking e quando não usar mocking?
  4. Quais são as melhores práticas para o mocking?

Não nos aprofundaremos em dados falsos, canhotos, motoristas, espiões ou nada disso. Vamos nos concentrar apenas no modelo mental em torno do mocking. Todo o resto pode vir depois.

Então, o que é mocking?

Mocking é o processo de simulação das dependências externas do código que está sendo testado. Assim, isolando seu código da infinidade de coisas das quais ele depende.

Essa "simulação" pode vir de duas formas:

  1. Dados falsos
  2. Interações falsas

Interações Falsas

Por exemplo, digamos que você queira testar uma função que requer uma conexão com um banco de dados. A função pode depender de um banco de dados, mas o banco de dados não é o foco aqui. O foco é a própria função. Não estamos testando o banco de dados ou a conexão do banco de dados. Estamos apenas testando uma função que depende dessas coisas.

TypeScript
export interface User {
  id: string;
  name: string;
}

export interface DatabaseConnection<T extends { id: string }> {
  findOneByID: (id: T['id']) => Promise<T | null>;
}

/**
 * Grabs the user by its ID and returns its name. Returns "Unknown" if we can't
 * find a user with that ID.
 */
export const getUserName = async (
  db: DatabaseConnection<User>,
  userID: string
): Promise<string> => {
  const maybeUser: User | null = await db.findOneByID(userID);
  return maybeUser?.name ?? 'Unknown';
};

Claro, ao fornecer uma conexão de banco de dados real, garantimos que nosso teste seja mais "real". Mas isso vem à custa da complexidade e capacidade de manutenção. Em vez disso, poderíamos apenas fornecer um banco de dados falso para nossa função.

Esse banco de dados falso é considerado uma interação simulada, porque simula algo com o qual nossa função interage.

TypeScript
export const createMockedDatabaseConnection = <T extends { id: string }>(
  data: Array<T>
): DatabaseConnection<T> => ({
  findOneByID: async (id) => data.find((el) => el.id === id) ?? null
});

Dados falsos

Também temos dados simulados, dados falsos usados em testes.

Por exemplo, o banco de dados falso que fornecemos foi apenas para que pudéssemos começar a testar nossa função. Agora que temos todas as interações das quais nossa função depende, precisamos realmente testá-la. E como fazemos isso? Passando entradas e esperando certas saídas.

Para passar entradas, precisamos de dados. E isso não precisa ser dados reais do seu banco de dados. Podemos usar dados simulados (falsos).

TypeScript
describe('getUserName()', () => {
  it('Returns the correct user name', async () => {
    // Fake data
    const fakeUser: User = { id: '1', name: 'Joe' };

    // Fake interactions
    const fakeDatabase: DatabaseConnection<User> =
      createMockedDatabaseConnection([fakeUser]);

    // Assertions
    const actualUserName = await getUserName(fakeDatabase, fakeUser.id);
    expect(actualUserName).toBe(fakeUser.name);
  });
});

Neste exemplo, criamos um usuário falso chamado Joe com ID 1.

LinkQuando usar mocking

Uma preocupação comum é quando usar. Afinal, poderíamos ter usado um banco de dados real em nossos testes acima. Então, como podemos decidir quando devemos usar mocking?

Primeiro, daremos uma olhada nos prós e contras:

Prós

  • Menos configuração porque não vem com todas as dependências de um banco de dados real;
  • Sem limpeza porque não estamos salvando nada em um banco de dados real, portanto, se não houver estado persistente, não há nada para limpar;

Limpando o estado do banco de dados

  • Mais rápido porque (novamente) ele não precisa criar todas as dependências de um banco de dados real;
  • Isolado porque nossos testes ainda passarão mesmo se o banco de dados real quebrar.

Banco de dados quebrado

Com relação a esse último ponto, pode parecer ruim que nosso teste passe mesmo que não passe com um banco de dados real. Eu estava cético sobre isso também, mas aqui está o raciocínio: se o banco de dados estiver quebrado, apenas os testes do banco de dados devem falhar, não os testes que dependem do banco de dados. A vantagem desse isolamento é a depuração.

Sem usar mocking

  • ❌ Testes de banco de dados
  • ❌ Testes de pagamento
  • ❌ Testes de criação de usuários
  • ❌ Testes de blog

Usando o mocking

  • ❌ Testes de banco de dados
  • ✅ Testes de pagamento
  • ✅ Testes de criação de usuários
  • ✅ Testes de blog

Imagine que você tenha mil testes que dependem do banco de dados. Se eles estiverem usando o banco de dados real em vez de uma simulação, todos esses testes falharão, deixando você com a tarefa de descobrir por que tudo está travando. Mas se os testes forem isolados, apenas os testes de banco de dados falharão, então você sabe exatamente onde procurar.

Contras

  • Menos confiança porque você não está usando interações reais (ou dados).

Confiança é a palavra-chave aqui. Você tem que considerar que toda vez que você mocka (simula) algo, você está criando um ambiente falso, então isso levanta a questão: isso funcionará no ambiente real? Isso realmente depende de quão bem o teste é escrito, mas ainda assim, não é o ambiente real, então um teste que usa simulação não pode fornecer a mesma confiança que um teste que não usa simulação.

Testes mockados versus testes reais

Não há bala de prata aqui. Nenhuma resposta mágica. Você tem que ponderar cada caso individualmente. De quanta confiança você está disposto a abdicar?

Para mim, a resposta é nenhuma. Mas também não quero deixar de aproveitar todos os benefícios de usar mocking. Então, ao invés de escolher entre:

  1. Testes rápidos e simulados;
  2. Ou testes lentos, reais, de ponta a ponta.

Escolho os dois.

Conforme mencionado no artigo anterior, executamos todos os tipos de testes aqui.

Para feedback imediato, executamos testes de unidade e integração em cada alteração de arquivo. Esses testes devem ser executados muito rápido, então, para eles, usamos dados simulados e interações simuladas.

Então, quando abrimos uma solicitação de merge, nosso pipeline de integração contínua cria um ambiente de teste onde executamos nossos testes de ponta a ponta com dados falsos, mas sem interações falsas.

Essa combinação me deixa confiante de que meu código funciona quando todo o teste passa.

LinkPráticas recomendadas de testes simulados

Agora, supondo que você já decidiu usar (mocking) simulações, aqui estão algumas práticas recomendadas:

  1. Apenas simule tipos que você possui.

Tipos externos podem mudar a qualquer momento, então não simule eles. Observe que estamos falando de tipos, não de valores reais. Você pode simular totalmente uma biblioteca de terceiros. Só não simule a sua assinatura do tipo.

TypeScript
// ✅ Do that

import FooType from 'FooLib';

describe('Foo', () => {
  it('Expects foo return a foo type', async () => {
    const foo: FooType = { ... };
    const result = returnFoo();
    expect(result).to.deep.equal(foo);
  });
});

function returnFoo(): FooType {
  return { ... };
}
TypeScript
// ❌ Don't do that

type FooType = { ... };

describe('Foo', () => {
  it('Expects foo return a foo type', async () => {
    const foo: FooType = { ... };
    const result = returnFoo();
    expect(result).to.deep.equal(foo);
  });
});

function returnFoo(): FooType {
  return { ... };
}

2. Não simule os valores de retorno do que estão sendo testado

Pense nisso por um segundo. Simular os valores de retorno de nosso objeto de teste nem faz sentido. Imagine escrever testes para uma função sum(), e simular que ela sempre retornara um mesmo valor.

TypeScript
// ✅ Do that

describe('sum()', () => {
  it('Returns a sum of 1 and 2', async () => {
    const sumResult = sum(1, 2);
    expect(sumResult).to.equal(3);
  });
});
TypeScript
// ❌ Don't do that

describe('sum()', () => {
  it('Returns a sum of 1 and 2', async () => {
    const mockedSum = jasmine.createSpy('sum', sum).and.returnValue(3);
    const sumResult = mockedSum(1, 2);
    expect(sumResult).to.equal(3);
  });
});

3. Não simule tudo

Este é um anti-padrão. Se tudo for mockado, podemos testar algo bem diferente do ambiente de produção. É como conversamos antes. Quanto mais você simula, menos confiança você tem nesse teste. Se você simular de tudo, não haverá confiança de que funcionará no ambiente de produção.

Mockando excessivamente

4. Use testes de integração

Se você se preocupa com como seu código interage com outros módulos, você deve fazer testes de integração em vez de simular.

5. Teste os casos de falha

Ao criar seus (mocks) simulações, não se esqueça de simular erros e testar o tratamento de erros. Você pode até adicionar expectativas de que alguns métodos ou chamadas de API não devem ser feitas em caso de erro.

TypeScript
describe('getUserName()', () => {
  it('Returns an Error when the req fails', async () => {
    // Fake data
    const fakeUser: User = { id: '1', name: 'Joe' };
    const wrongId = '2';

    // Fake interactions
    const fakeDatabase: DatabaseConnection<User> =
      createMockedDatabaseConnection([fakeUser]);

    // Assertions
    const actualUserNamePromise = getUserName(fakeDatabase, wrongId);
    await expectAsync(actualUserNamePromise).toBeRejectedWithError(Error);
  });
});

LinkNão pare aqui

Isso é tudo para os conceitos. Nos artigos a seguir, falaremos mais sobre dados falsos e interações falsas.

Se você quiser se aprofundar nos testes de software, considere assinar nossa newsletter . É livre de spam. Mantemos a frequências de e-mails baixa e o conteúdo valioso. Se preferir assistir ao nosso conteúdo, também temos um canal no YouTube que você pode se inscrever .

E se sua empresa estiver procurando por desenvolvedores web, remotos, considere entrar em contato comigo e com minha equipe. Você pode fazer isso por e- mail , Twitter ou Instagram .

Tenha um ótimo dia!

– Lucas

LinkReferências

  1. How to test software, part I: mocking, stubbing, and contract testing
  2. What is Mocking in Testing?
  3. What Is Mocking? - Typemock Blog
  4. Hand-rolled mocks made easy | InfoWorld
  5. xUnit Test Patterns: Refactoring Test Code - Gerard Meszaros.
  6. Generate dynamic mock data with Mockoon templating system
  7. request | Cypress Documentation
  8. Mock Testing
  9. Faker - Generate massive amounts of fake (but realistic) data for testing and development
  10. Retry, Rerun, Repeat
  11. Test Doubles: Can You Tell a Fake From a Mock? - WWT
  12. What's the difference between faking, mocking, and stubbing? - Stack Overflow.
  13. Mocks Aren't Stubs, by Martin Fowler

Assine a nossa Newsletter e seja avisado quando eu lançar um curso, postar um vídeo ou escrever um artigo.

Campo obrigatório
Campo obrigatório