Domine mocking e test doubles para tRPC: mocks avançados, spies, stubs, fakes e dependency injection para testes isolados, rápidos e confiáveis.
Testes Rápidos: Mocks eliminam dependências externas, tornando testes 10x mais rápidos.
Isolamento: Permitem testar unidades de código sem interferência de sistemas externos.
// 📁 src/test/doubles/test-doubles-examples.ts
import { vi } from 'vitest';
// 🎯 1. DUMMY - Objetos que não são usados, apenas preenchem parâmetros
class DummyLogger {
log() {} // Não faz nada
error() {} // Não faz nada
warn() {} // Não faz nada
}
// 🎯 2. STUB - Retorna respostas predefinidas
class StubUserRepository {
async findById(id: string) {
// Sempre retorna o mesmo usuário para testes
return {
id: 'test-id',
name: 'Test User',
email: 'test@example.com',
};
}
}
// 🎯 3. SPY - Grava informações sobre como foi chamado
class SpyEmailService {
private calls: any[] = [];
async sendEmail(to: string, subject: string, body: string) {
this.calls.push({ to, subject, body, timestamp: Date.now() });
return { messageId: 'test-message-id' };
}
getCallHistory() {
return this.calls;
}
wasCalledWith(to: string) {
return this.calls.some(call => call.to === to);
}
}
// 🎯 4. MOCK - Combina stub + spy + verificações
class MockPaymentService {
private expectedCalls: any[] = [];
private actualCalls: any[] = [];
expectCall(method: string, args: any[], returnValue: any) {
this.expectedCalls.push({ method, args, returnValue });
}
async processPayment(amount: number, token: string) {
this.actualCalls.push({ method: 'processPayment', args: [amount, token] });
const expected = this.expectedCalls.find(
call => call.method === 'processPayment'
);
if (!expected) {
throw new Error('Unexpected call to processPayment');
}
return expected.returnValue;
}
verify() {
expect(this.actualCalls).toEqual(
this.expectedCalls.map(call => ({
method: call.method,
args: call.args
}))
);
}
}
// 🎯 5. FAKE - Implementação simplificada mas funcional
class FakeDatabase {
private data: Map<string, any> = new Map();
async save(table: string, id: string, data: any) {
const key = `${table}:${id}`;
this.data.set(key, { ...data, id, createdAt: new Date() });
return data;
}
async findById(table: string, id: string) {
const key = `${table}:${id}`;
return this.data.get(key) || null;
}
async findMany(table: string, filter?: any) {
const results = [];
for (const [key, value] of this.data.entries()) {
if (key.startsWith(`${table}:`)) {
if (!filter || this.matchesFilter(value, filter)) {
results.push(value);
}
}
}
return results;
}
private matchesFilter(item: any, filter: any): boolean {
return Object.entries(filter).every(
([key, value]) => item[key] === value
);
}
clear() {
this.data.clear();
}
}
// 📁 src/test/mocks/advanced-mocking.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { mockDeep, mockReset } from 'vitest-mock-extended';
import { PrismaClient } from '@prisma/client';
import { Redis } from 'ioredis';
// 🎯 Mocks profundos para objetos complexos
const prismaMock = mockDeep<PrismaClient>();
const redisMock = mockDeep<Redis>();
// 🔧 Mock de módulos inteiros
vi.mock('@/lib/prisma', () => ({
prisma: prismaMock,
}));
vi.mock('ioredis', () => ({
default: vi.fn(() => redisMock),
Redis: vi.fn(() => redisMock),
}));
// 📧 Mock de serviços externos
vi.mock('@/lib/email-service', () => ({
EmailService: vi.fn().mockImplementation(() => ({
sendEmail: vi.fn().mockResolvedValue({ messageId: 'test-123' }),
sendBulkEmail: vi.fn().mockResolvedValue({ sent: 100, failed: 0 }),
getDeliveryStatus: vi.fn().mockResolvedValue('delivered'),
})),
}));
describe('Advanced Mocking Patterns', () => {
beforeEach(() => {
vi.clearAllMocks();
mockReset(prismaMock);
mockReset(redisMock);
});
describe('Conditional Mocking', () => {
it('🔄 deve simular diferentes cenários de resposta', async () => {
// 🎯 Arrange - Mock que retorna diferentes valores baseado no input
prismaMock.user.findUnique
.mockImplementation(async ({ where }) => {
if (where.id === 'admin-id') {
return {
id: 'admin-id',
role: 'ADMIN',
email: 'admin@test.com',
name: 'Admin User',
} as any;
}
if (where.id === 'banned-id') {
return {
id: 'banned-id',
role: 'USER',
status: 'BANNED',
email: 'banned@test.com',
name: 'Banned User',
} as any;
}
return null; // Usuário não encontrado
});
// 🎬 Act & Assert
const adminUser = await prismaMock.user.findUnique({
where: { id: 'admin-id' }
});
expect(adminUser?.role).toBe('ADMIN');
const bannedUser = await prismaMock.user.findUnique({
where: { id: 'banned-id' }
});
expect(bannedUser?.status).toBe('BANNED');
const notFound = await prismaMock.user.findUnique({
where: { id: 'invalid-id' }
});
expect(notFound).toBeNull();
});
it('📊 deve simular paginação complexa', async () => {
// 🎯 Arrange
const mockUsers = Array.from({ length: 100 }, (_, i) => ({
id: `user-${i}`,
name: `User ${i}`,
email: `user${i}@test.com`,
}));
prismaMock.user.findMany
.mockImplementation(async ({ skip = 0, take = 10 }) => {
return mockUsers.slice(skip, skip + take) as any;
});
prismaMock.user.count
.mockResolvedValue(100);
// 🎬 Act
const page1 = await prismaMock.user.findMany({ skip: 0, take: 10 });
const page2 = await prismaMock.user.findMany({ skip: 10, take: 10 });
const totalCount = await prismaMock.user.count();
// ✅ Assert
expect(page1).toHaveLength(10);
expect(page1[0].id).toBe('user-0');
expect(page2[0].id).toBe('user-10');
expect(totalCount).toBe(100);
});
});
describe('Time-based Mocking', () => {
it('⏰ deve simular operações dependentes de tempo', async () => {
// 🎯 Arrange - Mock do Date.now()
const mockDate = new Date('2024-01-15T10:00:00Z');
vi.setSystemTime(mockDate);
// Mock de operação que depende do tempo
const mockOperation = vi.fn().mockImplementation(() => {
const now = Date.now();
return {
timestamp: now,
expired: now > new Date('2024-01-15T09:00:00Z').getTime(),
};
});
// 🎬 Act
const result1 = mockOperation();
// Avançar tempo
vi.advanceTimersByTime(2 * 60 * 60 * 1000); // 2 horas
const result2 = mockOperation();
// ✅ Assert
expect(result1.expired).toBe(true);
expect(result2.timestamp).toBeGreaterThan(result1.timestamp);
// 🧹 Cleanup
vi.useRealTimers();
});
it('🔄 deve simular retry com backoff', async () => {
// 🎯 Arrange
let attemptCount = 0;
const mockApiCall = vi.fn().mockImplementation(async () => {
attemptCount++;
if (attemptCount < 3) {
throw new Error('Temporary failure');
}
return { success: true, attempt: attemptCount };
});
// Função com retry
const retryOperation = async (maxAttempts = 3) => {
for (let i = 0; i < maxAttempts; i++) {
try {
return await mockApiCall();
} catch (error) {
if (i === maxAttempts - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 100 * (i + 1)));
}
}
};
// 🎬 Act
const result = await retryOperation();
// ✅ Assert
expect(result.success).toBe(true);
expect(result.attempt).toBe(3);
expect(mockApiCall).toHaveBeenCalledTimes(3);
});
});
describe('Async Mock Patterns', () => {
it('🌊 deve simular streams de dados', async () => {
// 🎯 Arrange
const mockEventEmitter = {
events: [] as any[],
emit(event: string, data: any) {
this.events.push({ event, data, timestamp: Date.now() });
},
on: vi.fn(),
off: vi.fn(),
};
const mockStream = {
async *getData() {
yield { id: 1, data: 'first chunk' };
yield { id: 2, data: 'second chunk' };
yield { id: 3, data: 'final chunk' };
}
};
// 🎬 Act
const results = [];
for await (const chunk of mockStream.getData()) {
results.push(chunk);
mockEventEmitter.emit('data', chunk);
}
// ✅ Assert
expect(results).toHaveLength(3);
expect(mockEventEmitter.events).toHaveLength(3);
expect(mockEventEmitter.events[0].data.id).toBe(1);
});
it('🔄 deve simular operações concorrentes', async () => {
// 🎯 Arrange
const mockConcurrentOperation = vi.fn()
.mockImplementationOnce(async () => {
await new Promise(resolve => setTimeout(resolve, 100));
return { result: 'operation-1', duration: 100 };
})
.mockImplementationOnce(async () => {
await new Promise(resolve => setTimeout(resolve, 50));
return { result: 'operation-2', duration: 50 };
})
.mockImplementationOnce(async () => {
await new Promise(resolve => setTimeout(resolve, 200));
return { result: 'operation-3', duration: 200 };
});
// 🎬 Act
const start = Date.now();
const results = await Promise.all([
mockConcurrentOperation(),
mockConcurrentOperation(),
mockConcurrentOperation(),
]);
const totalDuration = Date.now() - start;
// ✅ Assert
expect(results).toHaveLength(3);
expect(totalDuration).toBeLessThan(250); // Rodou em paralelo
expect(mockConcurrentOperation).toHaveBeenCalledTimes(3);
});
});
});
// 📁 src/lib/di-container.ts
import { PrismaClient } from '@prisma/client';
import Redis from 'ioredis';
import { EmailService } from './email-service';
import { Logger } from './logger';
// 🎯 Container de dependências
export class DIContainer {
private dependencies = new Map<string, any>();
register<T>(key: string, dependency: T): void {
this.dependencies.set(key, dependency);
}
resolve<T>(key: string): T {
const dependency = this.dependencies.get(key);
if (!dependency) {
throw new Error(`Dependency '${key}' not found`);
}
return dependency;
}
clear(): void {
this.dependencies.clear();
}
}
// 🔧 Factory para criar container de produção
export function createProductionContainer(): DIContainer {
const container = new DIContainer();
container.register('prisma', new PrismaClient());
container.register('redis', new Redis(process.env.REDIS_URL));
container.register('emailService', new EmailService());
container.register('logger', new Logger());
return container;
}
// 🧪 Factory para criar container de testes
export function createTestContainer(): DIContainer {
const container = new DIContainer();
// Registrar mocks ao invés de dependências reais
container.register('prisma', mockDeep<PrismaClient>());
container.register('redis', mockDeep<Redis>());
container.register('emailService', {
sendEmail: vi.fn().mockResolvedValue({ messageId: 'test' }),
sendBulkEmail: vi.fn().mockResolvedValue({ sent: 1, failed: 0 }),
});
container.register('logger', {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
});
return container;
}
// 📁 src/server/services/user-service.ts
import { DIContainer } from '@/lib/di-container';
import { TRPCError } from '@trpc/server';
export class UserService {
constructor(private container: DIContainer) {}
async createUser(data: {
email: string;
name: string;
organizationId?: string;
}) {
const prisma = this.container.resolve('prisma');
const emailService = this.container.resolve('emailService');
const logger = this.container.resolve('logger');
try {
// 🔄 Verificar se email já existe
const existingUser = await prisma.user.findUnique({
where: { email: data.email },
});
if (existingUser) {
throw new TRPCError({
code: 'CONFLICT',
message: 'Email já está em uso',
});
}
// 🔧 Criar usuário
const user = await prisma.user.create({
data: {
email: data.email,
name: data.name,
organizationId: data.organizationId,
},
});
// 📧 Enviar email de boas-vindas
await emailService.sendEmail(
user.email,
'Bem-vindo!',
'Sua conta foi criada com sucesso.'
);
logger.info('User created', { userId: user.id });
return user;
} catch (error) {
logger.error('Error creating user', error);
throw error;
}
}
async updateUser(userId: string, data: Partial<{
name: string;
email: string;
}>) {
const prisma = this.container.resolve('prisma');
const logger = this.container.resolve('logger');
try {
const user = await prisma.user.update({
where: { id: userId },
data,
});
logger.info('User updated', { userId });
return user;
} catch (error) {
logger.error('Error updating user', error);
throw error;
}
}
}
// 📁 src/test/services/user-service.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { UserService } from '@/server/services/user-service';
import { createTestContainer } from '@/lib/di-container';
import { TRPCError } from '@trpc/server';
describe('UserService with DI', () => {
let userService: UserService;
let container: DIContainer;
let prisma: any;
let emailService: any;
let logger: any;
beforeEach(() => {
// 🔧 Setup do container de teste
container = createTestContainer();
userService = new UserService(container);
// 🎯 Obter referências dos mocks
prisma = container.resolve('prisma');
emailService = container.resolve('emailService');
logger = container.resolve('logger');
});
describe('createUser', () => {
it('✅ deve criar usuário com sucesso', async () => {
// 🎯 Arrange
const userData = {
email: 'test@example.com',
name: 'Test User',
organizationId: 'org-123',
};
const mockUser = { id: 'user-123', ...userData };
prisma.user.findUnique.mockResolvedValue(null); // Email não existe
prisma.user.create.mockResolvedValue(mockUser);
emailService.sendEmail.mockResolvedValue({ messageId: 'email-123' });
// 🎬 Act
const result = await userService.createUser(userData);
// ✅ Assert
expect(result).toEqual(mockUser);
expect(prisma.user.findUnique).toHaveBeenCalledWith({
where: { email: userData.email },
});
expect(prisma.user.create).toHaveBeenCalledWith({
data: userData,
});
expect(emailService.sendEmail).toHaveBeenCalledWith(
userData.email,
'Bem-vindo!',
'Sua conta foi criada com sucesso.'
);
expect(logger.info).toHaveBeenCalledWith(
'User created',
{ userId: 'user-123' }
);
});
it('🚫 deve falhar se email já existir', async () => {
// 🎯 Arrange
const userData = {
email: 'existing@example.com',
name: 'Test User',
};
prisma.user.findUnique.mockResolvedValue({ id: 'existing-user' });
// 🎬 Act & Assert
await expect(userService.createUser(userData)).rejects.toThrow(
new TRPCError({
code: 'CONFLICT',
message: 'Email já está em uso',
})
);
expect(prisma.user.create).not.toHaveBeenCalled();
expect(emailService.sendEmail).not.toHaveBeenCalled();
});
it('📧 deve lidar com falha no envio de email', async () => {
// 🎯 Arrange
const userData = {
email: 'test@example.com',
name: 'Test User',
};
const mockUser = { id: 'user-123', ...userData };
prisma.user.findUnique.mockResolvedValue(null);
prisma.user.create.mockResolvedValue(mockUser);
emailService.sendEmail.mockRejectedValue(new Error('Email service down'));
// 🎬 Act & Assert
await expect(userService.createUser(userData)).rejects.toThrow('Email service down');
expect(logger.error).toHaveBeenCalledWith(
'Error creating user',
expect.any(Error)
);
});
});
});
// 📁 src/test/mocks/msw-handlers.ts
import { rest } from 'msw';
import { createTRPCMsw } from 'msw-trpc';
import { appRouter } from '@/server/trpc/router';
// 🎯 Criar MSW handlers para tRPC
const trpcMsw = createTRPCMsw(appRouter);
export const trpcHandlers = [
// 👤 User handlers
trpcMsw.user.getProfile.query((req, res, ctx) => {
return res(
ctx.status(200),
ctx.data({
id: 'test-user-id',
name: 'Test User',
email: 'test@example.com',
createdAt: new Date(),
})
);
}),
trpcMsw.user.updateProfile.mutation((req, res, ctx) => {
const { name, email } = req.body;
return res(
ctx.status(200),
ctx.data({
id: 'test-user-id',
name: name || 'Test User',
email: email || 'test@example.com',
updatedAt: new Date(),
})
);
}),
// 🏢 Organization handlers
trpcMsw.organization.getAnalytics.query((req, res, ctx) => {
const { period } = req.query;
const baseMetrics = {
totalUsers: 100,
activeUsers: 75,
apiCalls: 10000,
};
// Simular dados diferentes baseado no período
const multiplier = period === '30d' ? 1 : period === '7d' ? 0.3 : 0.1;
return res(
ctx.status(200),
ctx.data({
...baseMetrics,
apiCalls: Math.floor(baseMetrics.apiCalls * multiplier),
period,
charts: {
userGrowth: Array.from({ length: 7 }, (_, i) => ({
date: new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString(),
value: Math.floor(baseMetrics.totalUsers * (1 + Math.random() * 0.1)),
})),
},
})
);
}),
];
// 🌐 REST API handlers para serviços externos
export const restHandlers = [
// 📧 Email service
rest.post('https://api.emailservice.com/send', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
messageId: 'mock-message-id',
status: 'sent',
deliveredAt: new Date().toISOString(),
})
);
}),
// 💳 Payment service
rest.post('https://api.stripe.com/v1/charges', (req, res, ctx) => {
const amount = req.body?.amount;
if (amount && amount > 100000) { // Simular falha para valores altos
return res(
ctx.status(402),
ctx.json({
error: {
type: 'card_error',
code: 'card_declined',
message: 'Your card was declined.',
},
})
);
}
return res(
ctx.status(200),
ctx.json({
id: 'ch_mock_charge_id',
amount: amount || 2000,
currency: 'usd',
status: 'succeeded',
paid: true,
created: Math.floor(Date.now() / 1000),
})
);
}),
// 📊 Analytics service
rest.get('https://api.analytics.com/events', (req, res, ctx) => {
const startDate = req.url.searchParams.get('start_date');
const endDate = req.url.searchParams.get('end_date');
return res(
ctx.status(200),
ctx.json({
events: Array.from({ length: 50 }, (_, i) => ({
id: `event-${i}`,
type: ['page_view', 'click', 'form_submit'][i % 3],
timestamp: new Date(Date.now() - i * 60000).toISOString(),
userId: `user-${Math.floor(i / 10)}`,
properties: {
page: '/dashboard',
browser: 'Chrome',
},
})),
total: 50,
startDate,
endDate,
})
);
}),
];
// 📁 src/test/setup-msw.ts
import { beforeAll, afterEach, afterAll } from 'vitest';
import { setupServer } from 'msw/node';
import { trpcHandlers, restHandlers } from './mocks/msw-handlers';
// 🔧 Setup do MSW server
const server = setupServer(...trpcHandlers, ...restHandlers);
// 🚀 Iniciar server antes de todos os testes
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' });
});
// 🧹 Reset handlers após cada teste
afterEach(() => {
server.resetHandlers();
});
// 🔥 Fechar server após todos os testes
afterAll(() => {
server.close();
});
// 📁 src/test/integration/msw-integration.test.ts
import { describe, it, expect } from 'vitest';
import { rest } from 'msw';
import { server } from '../setup-msw';
import { createTestCallerWithContext } from '@/test/helpers/trpc-test-utils';
describe('MSW Integration Tests', () => {
describe('Dynamic Handler Override', () => {
it('🔄 deve permitir override de handlers durante teste', async () => {
// 🎯 Arrange - Override handler para simular erro
server.use(
rest.post('https://api.emailservice.com/send', (req, res, ctx) => {
return res(
ctx.status(500),
ctx.json({ error: 'Service temporarily unavailable' })
);
})
);
const caller = createTestCallerWithContext();
// 🎬 Act & Assert
await expect(
caller.user.createWithEmail({
email: 'test@example.com',
name: 'Test User',
})
).rejects.toThrow('Failed to send welcome email');
});
it('📊 deve simular diferentes respostas baseado em parâmetros', async () => {
// 🎯 Arrange
const caller = createTestCallerWithContext();
// 🎬 Act - Diferentes períodos devem retornar dados diferentes
const analytics30d = await caller.organization.getAnalytics({ period: '30d' });
const analytics7d = await caller.organization.getAnalytics({ period: '7d' });
// ✅ Assert
expect(analytics30d.apiCalls).toBeGreaterThan(analytics7d.apiCalls);
expect(analytics30d.period).toBe('30d');
expect(analytics7d.period).toBe('7d');
});
it('🌊 deve simular network delays', async () => {
// 🎯 Arrange - Simular delay de 500ms
server.use(
rest.get('https://api.analytics.com/events', (req, res, ctx) => {
return res(
ctx.delay(500),
ctx.status(200),
ctx.json({ events: [], total: 0 })
);
})
);
const caller = createTestCallerWithContext();
// 🎬 Act
const start = Date.now();
await caller.analytics.getExternalEvents();
const duration = Date.now() - start;
// ✅ Assert
expect(duration).toBeGreaterThanOrEqual(500);
});
});
});
Mock o Mínimo:Mocke apenas as dependências necessárias, não toda a aplicação.
Comportamento Realista:Mocks devem simular o comportamento real, incluindo erros.
Reset Consistente:Sempre limpe mocks entre testes para evitar interferências.
Verificação de Chamadas:Verifique não apenas o retorno, mas como as dependências foram chamadas.