<h2>Introdução: Por que Mocks, Spies e Stubs são Essenciais</h2>
<p>Testes unitários avançados vão além de simples assertions. Quando você testa uma função que depende de APIs externas, bancos de dados ou módulos complexos, precisa de técnicas sofisticadas para isolar o comportamento do código sob teste. Mocks, spies e stubs são as ferramentas que permitem controlar dependências, simular comportamentos e validar interações entre componentes. O Vitest, moderno e rápido, oferece suporte nativo a essas técnicas através de sua API robusta, tornando-se a escolha ideal para projetos TypeScript e JavaScript contemporâneos.</p>
<h2>Entendendo os Conceitos Fundamentais</h2>
<h3>Diferenças Práticas Entre Mock, Spy e Stub</h3>
<p>Um <strong>stub</strong> substitui uma função real por uma versão simulada que retorna dados pré-configurados, sem registrar chamadas. Um <strong>spy</strong> envolve uma função existente, permitindo monitorar como ela foi chamada enquanto mantém seu comportamento original. Um <strong>mock</strong> é uma versão completamente controlada que define comportamentos esperados e valida se foram chamados corretamente.</p>
<p>Na prática, considere uma função que busca dados de um servidor:</p>
<pre><code class="language-javascript">// Função sob teste
async function fetchUserData(userId) {
const response = await fetch(/api/users/${userId});
return response.json();
}
// Teste com stub (retorna dados fixos)
import { describe, it, expect, vi } from 'vitest';
describe('fetchUserData com Stub', () => {
it('deve processar dados do usuário corretamente', async () => {
global.fetch = vi.fn().mockResolvedValue({
json: () => Promise.resolve({ id: 1, name: 'João' })
});
const result = await fetchUserData(1);
expect(result.name).toBe('João');
});
});</code></pre>
<p>Aqui, <code>fetch</code> foi substituído (stub) por uma versão que retorna sempre o mesmo resultado, isolando a função de teste da rede real.</p>
<h3>Quando Usar Cada Técnica</h3>
<p>Use <strong>stubs</strong> quando precisa apenas de valores de retorno previsíveis e não se importa com chamadas. Use <strong>spies</strong> quando a função original importa, mas você quer monitorar seu comportamento. Use <strong>mocks</strong> quando precisa validar que certas funções foram chamadas com argumentos específicos e em sequência correta. A escolha depende do que você está testando: lógica (stub), integração superficial (spy), ou contrato entre módulos (mock).</p>
<h2>Técnicas Avançadas com Vitest</h2>
<h3>Spies: Monitorando Comportamento Real</h3>
<p>Spies permitem verificar como uma função foi utilizada mantendo sua implementação. Imagine um serviço de notificação:</p>
<pre><code class="language-javascript">class NotificationService {
send(message) {
console.log(Email enviado: ${message});
return true;
}
}
const service = new NotificationService();
const spy = vi.spyOn(service, 'send');
service.send('Bem-vindo!');
service.send('Confirmação');
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenNthCalledWith(1, 'Bem-vindo!');
expect(spy).toHaveBeenNthCalledWith(2, 'Confirmação');
spy.mockRestore();</code></pre>
<p>O spy registra todas as chamadas e permite assertions detalhadas sobre argumentos e quantidade de invocações. Note que <code>mockRestore()</code> remove a instrumentação, importante para evitar efeitos colaterais entre testes.</p>
<h3>Mocks: Validando Contratos</h3>
<p>Mocks são ideais para verificar se uma dependência foi usada corretamente. Considere um repositório que precisa comunicar com um banco de dados:</p>
<pre><code class="language-javascript">class UserRepository {
constructor(database) {
this.db = database;
}
createUser(user) {
return this.db.insert('users', user);
}
findById(id) {
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
}
}
describe('UserRepository', () => {
it('deve inserir usuário corretamente', () => {
const mockDb = {
insert: vi.fn().mockResolvedValue({ id: 1 })
};
const repo = new UserRepository(mockDb);
repo.createUser({ name: 'Maria' });
expect(mockDb.insert).toHaveBeenCalledWith('users', { name: 'Maria' });
});
it('deve executar query com parâmetros corretos', () => {
const mockDb = {
query: vi.fn().mockResolvedValue([{ id: 1, name: 'Maria' }])
};
const repo = new UserRepository(mockDb);
repo.findById(1);
expect(mockDb.query).toHaveBeenCalledWith(
'SELECT * FROM users WHERE id = ?',
[1]
);
});
});</code></pre>
<p>Este padrão isola completamente a camada de dados, permitindo testar a lógica de negócio sem um banco de dados real.</p>
<h3>Padrão Avançado: Simulando Erros</h3>
<p>Um cenário comum é validar comportamento quando dependências falham:</p>
<pre><code class="language-javascript">class PaymentProcessor {
constructor(gateway) {
this.gateway = gateway;
}
async processPayment(amount) {
try {
const result = await this.gateway.charge(amount);
return { success: true, transactionId: result.id };
} catch (error) {
return { success: false, error: error.message };
}
}
}
describe('PaymentProcessor', () => {
it('deve tratar falha de gateway graciosamente', async () => {
const mockGateway = {
charge: vi.fn().mockRejectedValue(
new Error('Gateway indisponível')
)
};
const processor = new PaymentProcessor(mockGateway);
const result = await processor.processPayment(100);
expect(result.success).toBe(false);
expect(result.error).toBe('Gateway indisponível');
});
});</code></pre>
<p>Usando <code>mockRejectedValue</code>, simulamos exceções que a dependência pode lançar, validando se a função sob teste as trata corretamente.</p>
<h2>Padrões de Uso e Boas Práticas</h2>
<h3>Limpeza e Isolamento</h3>
<p>Cada teste deve ser independente. Use <code>beforeEach</code> e <code>afterEach</code> para gerenciar estado:</p>
<pre><code class="language-javascript">describe('API Service', () => {
let mockApi;
beforeEach(() => {
mockApi = {
get: vi.fn(),
post: vi.fn()
};
});
afterEach(() => {
vi.clearAllMocks();
});
it('teste 1', () => {
mockApi.get.mockResolvedValue({ data: 'test' });
expect(mockApi.get).toHaveBeenCalled();
});
it('teste 2', () => {
// mockApi está limpo aqui
expect(mockApi.get).not.toHaveBeenCalled();
});
});</code></pre>
<p><code>vi.clearAllMocks()</code> reseta o histórico de chamadas, evitando contaminação entre testes.</p>
<h3>Evitar Over-Mocking</h3>
<p>Não mocke tudo. Se uma função é simples e sem dependências externas, teste-a de verdade. Mocke apenas dependências que causam efeitos colaterais (I/O, rede, tempo):</p>
<pre><code class="language-javascript">// ✗ Ruim: mocka lógica pura
const add = (a, b) => a + b;
const mockAdd = vi.fn().mockReturnValue(5);
// ✓ Bom: testa diretamente
expect(add(2, 3)).toBe(5);</code></pre>
<h2>Conclusão</h2>
<p>Dominar mocks, spies e stubs transforma você em um testador profissional. O Vitest oferece APIs claras e poderosas para cada cenário. Lembre-se: <strong>spies monitoram comportamento real</strong>, <strong>stubs substituem com valores fixos</strong>, <strong>mocks validam contratos entre componentes</strong>. Use cada um em seu contexto apropriado, sempre mantendo testes rápidos, isolados e legíveis. A prática leva à excelência — comece a experimentar essas técnicas em seu próximo projeto.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://vitest.dev/api/vi.html" target="_blank" rel="noopener noreferrer">Vitest Documentation - Mocking</a></li>
<li><a href="https://testing-library.com/docs/" target="_blank" rel="noopener noreferrer">Testing Library Best Practices</a></li>
<li><a href="https://github.com/vitest-dev/vitest" target="_blank" rel="noopener noreferrer">JavaScript Testing with Jest and Vitest</a></li>
<li><a href="https://www.pluralsight.com/courses/unit-testing-principles" target="_blank" rel="noopener noreferrer">Unit Testing Principles by Vladimir Khorikov</a></li>
<li><a href="https://martinfowler.com/articles/mocksArentStubs.html" target="_blank" rel="noopener noreferrer">Mock Objects Pattern</a></li>
</ul>