Por Que Usar Fakes e Mocks?
- Isolamento: Testes não dependem de serviços externos
- Velocidade: Mocks são mais rápidos que serviços reais
- Confiabilidade: Não há flaky tests por dependências
- Cobertura: Testar edge cases difíceis de reproduzir
Tipos de Fakes
1. Fake Objects
Simulam comportamento de objetos reais.
# fakes/user_repository.py
class FakeUserRepository:
def __init__(self):
self.users = {}
self._id_counter = 1
def save(self, user):
if not user.id:
user.id = self._id_counter
self._id_counter += 1
self.users[user.id] = user
return user
def find_by_id(self, id):
return self.users.get(id)
def find_by_email(self, email):
for user in self.users.values():
if user.email == email:
return user
return None
def delete(self, id):
if id in self.users:
del self.users[id]
return True
return False
# Uso em testes
def test_create_user():
repo = FakeUserRepository()
service = UserService(repo)
user = service.create_user("João", "joao@example.com")
assert user.id is not None
assert user.name == "João"
assert repo.find_by_email("joao@example.com") == user
2. Fake APIs
Simulam endpoints HTTP.
# fakes/http_fake.py
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
from threading import Thread
from typing import Dict, Callable
class FakeAPIHandler(BaseHTTPRequestHandler):
routes: Dict[str, Callable] = {}
def do_GET(self):
self._handle_request('GET')
def do_POST(self):
self._handle_request('POST')
def do_PUT(self):
self._handle_request('PUT')
def do_DELETE(self):
self._handle_request('DELETE')
def _handle_request(self, method):
handler = self.routes.get((method, self.path))
if handler:
response = handler(self)
self._send_response(response)
else:
self._send_response({'error': 'Not found'}, 404)
def _send_response(self, data, status=200):
self.send_response(status)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(data).encode())
def log_message(self, format, *args):
pass # Silenciar logs
class FakeAPIServer:
def __init__(self, port=8080):
self.port = port
self.server = None
self.thread = None
self.routes = {}
def register(self, method, path, handler):
self.routes[(method, path)] = handler
FakeAPIHandler.routes = self.routes
def start(self):
self.server = HTTPServer(('localhost', self.port), FakeAPIHandler)
self.thread = Thread(target=self.server.serve_forever)
self.thread.daemon = True
self.thread.start()
def stop(self):
if self.server:
self.server.shutdown()
# Uso
def payment_handler(request):
return {
'transaction_id': 'tx_123',
'status': 'approved',
'amount': 99.90
}
fake_api = FakeAPIServer(8080)
fake_api.register('POST', '/api/payment', payment_handler)
fake_api.start()
# Seus testes aqui
fake_api.stop()
3. Fake Databases (SQLite)
# fakes/database.py
import sqlite3
import os
class FakeDatabase:
def __init__(self, in_memory=True):
if in_memory:
self.conn = sqlite3.connect(':memory:')
else:
self.db_path = 'test.db'
self.conn = sqlite3.connect(self.db_path)
self.conn.row_factory = sqlite3.Row
self._create_tables()
def _create_tables(self):
cursor = self.conn.cursor()
cursor.execute('''
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
''')
cursor.execute('''
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL
)
''')
self.conn.commit()
def execute(self, query, params=None):
cursor = self.conn.cursor()
if params:
cursor.execute(query, params)
else:
cursor.execute(query)
self.conn.commit()
return cursor
def fetchone(self, query, params=None):
cursor = self.execute(query, params)
return cursor.fetchone()
def fetchall(self, query, params=None):
cursor = self.execute(query, params)
return cursor.fetchall()
def close(self):
self.conn.close()
if hasattr(self, 'db_path') and os.path.exists(self.db_path):
os.remove(self.db_path)
# Uso
def test_user_crud():
db = FakeDatabase()
# Create
db.execute(
'INSERT INTO users (name, email) VALUES (?, ?)',
('João', 'joao@example.com')
)
# Read
user = db.fetchone('SELECT * FROM users WHERE email = ?', ('joao@example.com',))
assert user['name'] == 'João'
db.close()
Mocking Libraries
unittest.mock (Python)
from unittest.mock import Mock, patch, MagicMock
# Mock básico
def test_payment():
mock_gateway = Mock()
mock_gateway.charge.return_value = {'status': 'success'}
result = process_payment(100, mock_gateway)
mock_gateway.charge.assert_called_once_with(100)
assert result == 'success'
# Patch
@patch('myapp.services.payment_gateway')
def test_with_patch(mock_gateway):
mock_gateway.charge.return_value = {'status': 'success'}
result = process_payment(100)
mock_gateway.charge.assert_called_once()
# Spy (partial mock)
def test_spy():
real_service = PaymentService()
spy_service = Mock(wraps=real_service)
spy_service.process(100)
assert spy_service.process.called
assert real_service.internal_method.called # Real method foi chamado
WireMock (Java/Server)
// Java WireMock
import com.github.tomakehurst.wiremock.WireMockServer;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
WireMockServer wireMock = new WireMockServer(8080);
wireMock.start();
// Stub
wireMock.stubFor(post(urlEqualTo("/api/payment"))
.withRequestBody(containing("amount"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{"transaction_id":"tx_123","status":"approved"}")));
// Test
RestAssured.given()
.contentType("application/json")
.body("{"amount":100}")
.when()
.post("http://localhost:8080/api/payment")
.then()
.statusCode(200)
.body("status", equalTo("approved"));
wireMock.stop();
MSW (Mock Service Worker)
// Mock API com MSW
import { setupWorker, rest } from 'msw';
const handlers = [
rest.post('/api/payment', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
transaction_id: 'tx_123',
status: 'approved',
amount: req.body.amount
})
);
}),
rest.get('/api/users/:id', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
id: req.params.id,
name: 'João Silva',
email: 'joao@example.com'
})
);
}),
rest.post('/api/users', (req, res, ctx) => {
return res(
ctx.status(201),
ctx.json({
id: 'new-user-id',
...req.body
})
);
})
];
export const worker = setupWorker(...handlers);
// No test
import { worker } from './mocks/browser';
beforeAll(() => worker.start());
afterAll(() => worker.stop());
test('payment flow', async () => {
render();
// MSW intercepta requisição
await userEvent.type(screen.getByLabelText('Valor'), '100');
await userEvent.click(screen.getByRole('button', { name: /pagar/i }));
expect(await screen.findByText(/transação aprovada/i)).toBeInTheDocument();
});
TestContainers vs Fakes
| Aspecto | TestContainers | Fakes |
|---|---|---|
| Fidelidade | Real service | Simulado |
| Velocidade | Lento (minutos) | Rápido (ms) |
| Complexidade | Alta | Baixa |
| Bugs encontrados | Mais realistas | Menos |
| Manutenção | Baixa | Alta (manter fakes) |
Quando Usar Cada
# Use FAKE quando:
# - Precisa de respostas específicas/edge cases
# - Performance é crítica
# - Serviço externo é lento/não disponível
# - Quer testar comportamento, não implementação
# Use TestContainers quando:
# - Precisa de comportamento real
# - Testando integrações complexas
# - Quer detectar problemas realistas
# - Manutenção do fake seria muito complexa
Conclusão
Mocks e fakes são essenciais para testes isolados. As chaves são:
- Isolar – Testes não devem depender de externos
- Velocidade – Fakes são rápidos
- Verossimilhança – Fakes devem ser realistas
- Manutenção – Mocks automáticos vs manuais
- Combinar – Fakes + TestContainers quando necessário
