Conceitos de Mocking - Testing Series #2
O que é Mocking
O 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:
- O que é mocking?
- Quais são os principais conceitos por trás disso?
- Quando usar mocking e quando não usar mocking?
- 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:
- Dados falsos
- 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.
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';
};
TypeScriptexport 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.
export const createMockedDatabaseConnection = <T extends { id: string }>(
data: Array<T>
): DatabaseConnection<T> => ({
findOneByID: async (id) => data.find((el) => el.id === id) ?? null
});
TypeScriptexport 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).
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);
});
});
TypeScriptdescribe('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.
Quando 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;
- 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.
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.
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:
- Testes rápidos e simulados;
- 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.
Práticas recomendadas de testes simulados
Agora, supondo que você já decidiu usar (mocking) simulações, aqui estão algumas práticas recomendadas:
- 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.
// ✅ 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// ✅ 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 { ... };
}
// ❌ 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 { ... };
}
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.
// ✅ Do that
describe('sum()', () => {
it('Returns a sum of 1 and 2', async () => {
const sumResult = sum(1, 2);
expect(sumResult).to.equal(3);
});
});
TypeScript// ✅ Do that
describe('sum()', () => {
it('Returns a sum of 1 and 2', async () => {
const sumResult = sum(1, 2);
expect(sumResult).to.equal(3);
});
});
// ❌ 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);
});
});
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.
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.
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);
});
});
TypeScriptdescribe('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);
});
});
Nã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
Referências
- How to test software, part I: mocking, stubbing, and contract testing
- What is Mocking in Testing?
- What Is Mocking? - Typemock Blog
- Hand-rolled mocks made easy | InfoWorld
- xUnit Test Patterns: Refactoring Test Code - Gerard Meszaros.
- Generate dynamic mock data with Mockoon templating system
- request | Cypress Documentation
- Mock Testing
- Faker - Generate massive amounts of fake (but realistic) data for testing and development
- Retry, Rerun, Repeat
- Test Doubles: Can You Tell a Fake From a Mock? - WWT
- What's the difference between faking, mocking, and stubbing? - Stack Overflow.
- Mocks Aren't Stubs, by Martin Fowler