Guia de referência do sistema de testes do projeto. Cobre filosofia, padrões, edge cases e instruções para escrever novos testes.
- 1. Filosofia de Testes
- 2. Estrutura de Diretórios
- 3. Configuração do Vitest
- 4. Testes Unitários — Camada de Erros
- 5. Testes Unitários — UserService
- 6. Testes Unitários — UUID
- 7. Testes Unitários — Error Handler
- 8. Testes E2E — Estratégia
- 9. Testes E2E — Status e Check
- 10. Testes E2E — User CRUD
- 11. Padrões de Mock — Referência Rápida
- 12. Como Rodar os Testes
- 13. Como Escrever Novos Testes
- 14. Decisões Técnicas e Trade-offs
O projeto usa o padrão Either para tratamento de erros nos services. Isso muda fundamentalmente o que os testes verificam:
// Ao invés de testar se uma exceção é lançada:
expect(() => service.getUser('invalid')).toThrow()
// Testamos o valor retornado:
const result = await service.getUser('invalid')
expect(result.success).toBe(false)
expect(result.error.code).toBe('NOT_FOUND')Cada método de service retorna Either<AppError, T> — o teste verifica a forma do retorno, não se algo explodiu. Isso torna os fluxos de erro explícitos e previsíveis.
/ E2E \ ← Poucos: validam o pipeline HTTP completo
/ Unit \ ← Muitos: lógica de negócio isolada, rápidos
- Unitários: testam funções isoladas com mocks nas bordas. Rodam em milissegundos, sem I/O real.
- E2E: testam rotas reais com Fastify
inject(), service mockado. Validam schema Zod, error handler, serialização de resposta.
- Compatível com Bun e Node.js — requisito obrigatório do projeto (dual runtime)
- ESM nativo, sem configuração extra de transpilação
- API idêntica ao Jest (
describe,it,expect,vi.fn()) — curva de aprendizado zero para quem já conhece Jest - Execução rápida com paralelismo por padrão
Não buscamos 100% de coverage por linha. O foco é em edge cases e comportamentos críticos de negócio. Um teste que verifica "usuário com email duplicado retorna 409" vale mais que dez testes verificando getters triviais.
__tests__/
├── unit/ # Testes isolados, sem I/O real
│ ├── core/
│ │ ├── errors/
│ │ │ └── errors.test.ts # Either, AppError, SchemaError, mapeamento de códigos
│ │ ├── services/
│ │ │ └── user-service.test.ts # Lógica de negócio com repositório mockado
│ │ └── utils/
│ │ └── uuid.test.ts # UUIDv7: formato, unicidade, ordenação temporal
│ └── main/
│ └── infra/
│ └── error-handler.test.ts # Handler centralizado de erros do Fastify
└── e2e/
├── helpers/
│ └── build-app.ts # Factory: createTestApp()
└── routes/
├── status.test.ts # GET /api/status (health check)
├── check.test.ts # POST /api/v1/check/http
└── user.test.ts # CRUD completo /api/v1/user
Por que espelhar a estrutura de src/? Para que qualquer dev encontre o teste de um arquivo fonte sem pensar. Se o service está em src/core/services/user/, o teste está em __tests__/unit/core/services/.
Arquivo: vitest.config.js
| Configuração | Valor | Por quê |
|---|---|---|
globals |
true |
describe, it, expect disponíveis globalmente |
timeout |
30000 |
Margem para testes E2E que iniciam instância Fastify |
include |
__tests__/**/*.{test,spec}.{ts,js} |
Apenas arquivos de teste |
coverage.exclude |
migrations, seeds, knexfile, __tests__/ |
Sem inflar cobertura com código que não é lógica de negócio |
alias @ |
./src |
Mesmos imports do código de produção |
Mesmo com
globals: true, o projeto importa explicitamentedescribe,it,expectdo Vitest. Isso é intencional — torna as dependências visíveis e melhora o suporte da IDE.
bun run test # vitest run — execução única (ideal para CI)
bun run test:coverage # vitest run --coverage — com relatório v8
bun run test:watch # vitest — modo watch para desenvolvimentoArquivo: __tests__/unit/core/errors/errors.test.ts
Source: src/core/errors/index.ts
Mapeamento bidirecional entre códigos semânticos e status HTTP:
it.each([
['BAD_REQUEST', 400],
['UNAUTHORIZED', 401],
['NOT_FOUND', 404],
['CONFLICT', 409],
['INTERNAL_SERVER_ERROR', 500]
] as const)('should map %s to %d', (code, status) => {
expect(codeToStatus(code)).toBe(status)
})Por que it.each? Evita duplicação — cada par código/status vira um teste isolado com uma única linha. Se adicionarmos um novo código de erro, basta adicionar uma linha no array.
Edge case — status desconhecido:
expect(statusToCode(999)).toBe('INTERNAL_SERVER_ERROR')
expect(statusToCode(0)).toBe('INTERNAL_SERVER_ERROR')Qualquer status não mapeado é tratado como erro interno. Isso previne que status inesperados vazem para o cliente — se não sabemos o que é, é problema do servidor.
const error = new AppError('NOT_FOUND', 'User not found')
expect(error).toBeInstanceOf(Error) // Herda de Error (stack trace funciona)
expect(error.name).toBe('AppError') // Identificável em logs
expect(error.code).toBe('NOT_FOUND') // Código semântico
expect(error.statusCode).toBe(404) // Calculado automaticamentePor que testar instanceof Error? O error handler usa instanceof para identificar tipos de erro. Se a cadeia de protótipos estiver quebrada, o handler não consegue distinguir AppError de um erro genérico.
SchemaError herda de AppError com código fixo BAD_REQUEST. O método estático fromZod() transforma erros do Zod em mensagens legíveis:
Issue único:
const zodError = new ZodError([{
code: 'too_small',
origin: 'string',
minimum: 6,
message: 'String must contain at least 6 character(s)',
path: ['password']
} as z.core.$ZodIssue])
const error = SchemaError.fromZod(zodError)
expect(error.message).toBe('password: String must contain at least 6 character(s)')Múltiplos issues (separados por ;):
// email: Required; username: Too shortPaths aninhados (notação de ponto):
// path: ['address', 'street'] → "address.street: Expected string"Por que esses três casos? Cobrem as situações reais de validação: campo simples com regra violada, formulário com múltiplos erros simultâneos, e objetos com campos aninhados. O formato campo: mensagem é parseável por frontends para exibir erros inline.
Nota sobre Zod 4: O tipo
ZodIssueimportado diretamente de'zod'está deprecated. O projeto usaz.core.$ZodIssue(o tipo atual). Issues do tipotoo_smallagora exigem a propriedadeorigin(ex:'string','number','array') no lugar do antigotype.
left('something went wrong') → { success: false, error: 'something went wrong' }
right(42) → { success: true, data: 42 }Type narrowing — o teste mais importante:
const result = Math.random() > 0.5 ? right('ok') : left(new Error('fail'))
if (result.success) {
expect(result.data).toBe('ok') // TypeScript sabe que .data existe
} else {
expect(result.error).toBeInstanceOf(Error) // TypeScript sabe que .error existe
}Esse teste prova que o discriminated union funciona: o TypeScript força o dev a checar result.success antes de acessar .data ou .error. Se alguém tentar acessar .data sem a verificação, o compilador impede.
Arquivo: __tests__/unit/core/services/user-service.test.ts
Source: src/core/services/user/user-service.ts
O service contém lógica de negócio. Seus testes devem verificar decisões lógicas (ex: "se email já existe, retorna CONFLICT"), não comportamento do banco de dados.
Mockar o repository traz:
- Velocidade: testes rodam em milissegundos, sem PostgreSQL
- Isolamento: falhas no banco não quebram testes de lógica
- Controle: podemos simular cenários impossíveis em dev (ex: race conditions)
function createMockRepository() {
return {
findById: vi.fn(),
findByEmail: vi.fn(),
findByUsername: vi.fn(),
findAll: vi.fn(),
create: vi.fn(),
update: vi.fn(),
softDelete: vi.fn()
}
}A cada teste, o mock é resetado via beforeEach com uma nova instância do service. Isso impede vazamento de estado entre testes.
Todo teste de service segue o mesmo fluxo:
it('should return user when found', async () => {
// Arrange: configure o que o mock vai retornar
vi.mocked(repo.findById).mockResolvedValue(mockUser)
// Act: chame o método do service
const result = await service.getUser('some-id')
// Assert: verifique o resultado
expect(result).toEqual({ success: true, data: mockUser })
})| Cenário | Mock | Resultado esperado |
|---|---|---|
| Usuário encontrado | findById → mockUser |
right(mockUser) |
| Usuário não existe | findById → null |
left(NOT_FOUND) |
O caso "não encontrado" é o erro mais comum em qualquer API. Todo endpoint de busca por ID deve tratar esse cenário.
| Cenário | Mock | Resultado esperado |
|---|---|---|
| Com usuários | findAll → [mockUser] |
right([mockUser]) |
| Sem usuários | findAll → [] |
right([]) |
Detalhe importante: lista vazia retorna right([]), não um erro. Uma coleção vazia é um resultado válido — diferente de "recurso não encontrado". Confundir esses dois conceitos é um bug comum em APIs.
| Cenário | Mock | Resultado esperado |
|---|---|---|
| Sucesso | findByEmail → null, findByUsername → null |
right(newUser) |
| Email duplicado | findByEmail → existingUser |
left(CONFLICT) |
| Username duplicado | findByEmail → null, findByUsername → existingUser |
left(CONFLICT) |
Ordem importa: email é verificado primeiro, depois username. O teste de username duplicado mocka findByEmail retornando null para garantir que o service avança até a verificação de username.
Asserção crítica: nos casos de conflito, o teste verifica que repo.create NÃO foi chamado:
expect(repo.create).not.toHaveBeenCalled()Isso prova que o service interrompe a execução antes de tentar criar o registro.
| Cenário | Mock | Resultado esperado |
|---|---|---|
| Sucesso | update → 1, findById → updatedUser |
right(updatedUser) |
| ID não existe | update → 0 |
left(NOT_FOUND) |
| Race condition | update → 1, findById → null |
left(NOT_FOUND) |
O terceiro caso é uma race condition: entre o UPDATE e o SELECT subsequente, outro request pode ter deletado o usuário. Sem esse teste, o service retornaria right(null) — quebrando o contrato de tipos. Esse edge case só aparece em sistemas com concorrência real.
| Cenário | Mock | Resultado esperado |
|---|---|---|
| Sucesso | softDelete → 1 |
right(undefined) |
| ID não existe | softDelete → 0 |
left(NOT_FOUND) |
O service usa soft delete (marca deleted_at). O retorno de sucesso é right(undefined) por convenção — operações void não têm dados para retornar.
Arquivo: __tests__/unit/core/utils/uuid.test.ts
Source: src/core/utils/uuid.ts
O projeto usa UUIDv7 (RFC 9562) gerado internamente, sem dependência externa. Compatível com Bun e Node.js.
| Teste | O que valida | Por que importa |
|---|---|---|
| Formato (8-4-4-4-12) | String com 36 caracteres em hex | Bancos de dados e validadores aceitam o formato |
Version bit = 7 |
Posição 14 do UUID é 7 |
Compliance com UUIDv7 (diferencia de v4) |
Variant bits = 8/9/a/b |
Posição 19 do UUID | RFC 9562 exige variant 10xx em binário |
| Regex em 50 UUIDs | Formato correto em volume | Confiança estatística (não só uma chamada sortuda) |
| Unicidade em 100 UUIDs | Set.size === 100 |
Detecta bugs de entropia no gerador de random |
| Comprimento = 36 | .length === 36 |
Guarda contra truncamento |
const first = uuidv7()
await sleep(5) // 5ms entre cada
const second = uuidv7()
await sleep(5)
const third = uuidv7()
const sorted = [third, first, second].sort()
expect(sorted).toEqual([first, second, third])Por que isso importa: UUIDv7 codifica o timestamp nos primeiros 48 bits. Isso significa que ordenação lexicográfica (.sort()) = ordenação cronológica. Em índices de banco de dados, IDs UUIDv7 mantêm ordem de inserção — diferente de UUIDv4 que é completamente aleatório e fragmenta índices B-tree.
Arquivo: __tests__/unit/main/infra/error-handler.test.ts
Source: src/main/infra/error-handler.ts
O error handler é registrado via app.setErrorHandler() e é a última linha de defesa — todo erro não tratado em qualquer rota passa por aqui. Por isso ele precisa cobrir todos os tipos de erro possíveis.
function createMockReply() {
const reply = {
status: vi.fn().mockReturnThis(), // Permite chaining: reply.status(404).send(...)
send: vi.fn().mockReturnThis()
}
return reply as any
}Logger e env são mockados para evitar output de log nos testes e dependência de variáveis de ambiente.
| Tipo de Erro | Status HTTP | Código | Por que esse teste existe |
|---|---|---|---|
ZodError |
400 | BAD_REQUEST |
Validação Zod falha na rota → handler converte via SchemaError.fromZod() |
AppError(NOT_FOUND) |
404 | NOT_FOUND |
Erro de negócio com status mapeado automaticamente |
AppError(CONFLICT) |
409 | CONFLICT |
Prova que o mapeamento funciona para todos os códigos, não só 404 |
| Fastify validation error | 400 | BAD_REQUEST |
Validação nativa do Fastify (separada do Zod) |
Erro com statusCode: 404 |
404 | NOT_FOUND |
Rota não encontrada pelo router do Fastify |
SyntaxError (JSON) |
400 | BAD_REQUEST |
Body JSON malformado → mensagem amigável sobre Content-Type |
| Erro genérico | 500 | INTERNAL_SERVER_ERROR |
Fallback: mensagem genérica, nunca expõe detalhes internos |
Erro com statusCode < 500 |
Preservado (ex: 429) | ERROR |
Rate limiting e erros de middleware mantêm seu status original |
requestId sempre presente:
it('should always include requestId in response', () => {
const sent = reply.send.mock.calls[0][0]
expect(sent.requestId).toBe('test-request-id')
})Todo response de erro inclui requestId — essencial para correlacionar erros nos logs em produção. Sem isso, debugar em produção seria impossível.
Por que o erro genérico esconde a mensagem: para status >= 500, o handler retorna "Internal Server Error" ao invés da mensagem real. Isso previne vazamento de stack traces, queries SQL, ou detalhes internos para o cliente.
O projeto usa app.inject() do Fastify para simular requests HTTP sem abrir porta TCP:
const response = await app.inject({
method: 'GET',
url: '/api/v1/user'
})Vantagens sobre Supertest:
- Mais rápido: sem overhead de rede TCP
- Sem conflito de portas: múltiplos testes em paralelo sem problema
- Nativo do Fastify: response tipada, sem dependência extra
Supertest está disponível como dependência para casos que precisem de semântica HTTP real.
// __tests__/e2e/helpers/build-app.ts
import { buildApp } from '@/main/app'
export function createTestApp() {
const app = buildApp()
return app
}Usa o mesmo buildApp() do código de produção. Isso garante que os testes E2E exercitem a configuração real: plugins (Helmet, CORS, Rate Limit), middleware (error handler), e rotas. Todo teste E2E deve chamar app.close() no afterAll.
Rota (real) → Zod (real) → Handler (real) → Service (MOCKADO) → Repository (não chega aqui)
O mock fica no service, não no repository. Isso significa:
- O que roda de verdade: routes, validação Zod, error handler, serialização de resposta
- O que é controlado: retornos do service (Either com left/right)
Assim testamos o pipeline HTTP completo sem precisar de banco de dados.
Arquivo: __tests__/e2e/routes/status.test.ts
it('should return 200 with status ok', async () => {
const response = await app.inject({ method: 'GET', url: '/api/status' })
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({ status: 'ok' })
})Parece trivial, mas esse teste valida que toda a inicialização do Fastify funciona: plugins carregaram, rotas registraram, server responde.
Arquivo: __tests__/e2e/routes/check.test.ts
O endpoint faz requests HTTP externos. Para testar sem rede real:
const fetchSpy = vi.spyOn(globalThis, 'fetch')| Cenário | Mock do fetch | Resultado |
|---|---|---|
| URL acessível | mockResolvedValue({ status: 200 }) |
status: 'success', responseTime numérico |
| URL inacessível | mockRejectedValue(new Error('Network error')) |
status: 'failed' (HTTP 200 — o check em si não falhou) |
| Sem método especificado | - | fetch chamado com method: 'GET' (default) |
| Método POST | - | fetch chamado com method: 'POST' |
| URL inválida | Não chega ao fetch | HTTP 400 (Zod valida formato com z.url()) |
Detalhe: URL inacessível retorna HTTP 200, não 500. O endpoint de check reporta o resultado da verificação — se o alvo está fora, o check em si foi executado com sucesso, o resultado é 'failed'.
Arquivo: __tests__/e2e/routes/user.test.ts
const mockUserService = {
getUser: vi.fn(),
getAllUsers: vi.fn(),
createUser: vi.fn(),
updateUser: vi.fn(),
deleteUser: vi.fn()
}
vi.mock('@/core/services', () => ({
makeUserService: () => mockUserService
}))O vi.mock substitui a factory makeUserService() para retornar o mock. O mock é registrado antes do app ser construído.
| Cenário | Service retorna | HTTP Status | Body |
|---|---|---|---|
| Com usuários | right([mockUser]) |
200 | [{ username: 'johndoe', ... }] |
| Sem usuários | right([]) |
200 | [] |
| Cenário | Service retorna | HTTP Status |
|---|---|---|
| Encontrado | right(mockUser) |
200 |
| Não encontrado | left(AppError('NOT_FOUND')) |
404 |
| UUID inválido | Não chega ao service | 400 |
O teste de UUID inválido é chave: o schema UserParamsSchema usa z.uuidv7(), que rejeita strings fora do formato UUIDv7. A validação acontece antes do handler executar — o mock do service não é nem chamado. Isso prova que a validação está na borda.
| Cenário | Input | Service retorna | HTTP Status |
|---|---|---|---|
| Sucesso | { username, email, password } |
right(newUser) |
201 |
| Email duplicado | { username, email, password } |
left(CONFLICT) |
409 |
| Campos faltando | { username } (sem email/password) |
Não chega | 400 |
| Senha curta | { ..., password: '123' } |
Não chega | 400 |
| Email inválido | { ..., email: 'not-email' } |
Não chega | 400 |
| Username curto | { ..., username: 'ab' } |
Não chega | 400 |
Todos os casos 400 são validação Zod pura — o mock do service nunca é chamado. Isso prova a separação: validação na borda, lógica no service.
| Cenário | Service retorna | HTTP Status |
|---|---|---|
| Sucesso | right(updatedUser) |
200 |
| Não encontrado | left(NOT_FOUND) |
404 |
| Cenário | Service retorna | HTTP Status | Body |
|---|---|---|---|
| Sucesso | right(undefined) |
200 | { message: 'User deleted successfully' } |
| Não encontrado | left(NOT_FOUND) |
404 | { code: 'NOT_FOUND', ... } |
Os handlers usam unwrap() para converter Either em exceções:
function unwrap<T>(result: Either<Error, T>): T {
if (!result.success) throw result.error // Lança AppError
return result.data
}
// No handler:
const user = unwrap(await userService.getUser(id))
return user // Só chega aqui se result.success === trueQuando unwrap lança, o error handler centralizado captura e formata a resposta. Assim os services mantêm o padrão Either puro, e as rotas não precisam de if/else em cada handler.
const mockFn = vi.fn()
mockFn.mockReturnValue('sync value')
mockFn.mockResolvedValue('async value') // Para funções asyncvi.mocked(repo.findById).mockResolvedValue(mockUser)
// TypeScript sabe que mockResolvedValue aceita User | nullvi.mock('@/core/services', () => ({
makeUserService: () => mockUserService
}))Substitui o módulo inteiro. Útil para trocar factories de DI nos testes E2E.
const fetchSpy = vi.spyOn(globalThis, 'fetch')
fetchSpy.mockResolvedValue(new Response('ok'))
// Depois do teste:
fetchSpy.mockRestore() // Restaura implementação originalIdeal para interceptar globais como fetch sem afetar outros testes.
const reply = {
status: vi.fn().mockReturnThis(), // reply.status(404) retorna reply
send: vi.fn().mockReturnThis() // reply.send({}) retorna reply
}
// Permite: reply.status(404).send({ code: 'NOT_FOUND' })| Método | Quando usar |
|---|---|
mockReturnValue(x) |
Função síncrona que retorna x |
mockResolvedValue(x) |
Função async que retorna Promise.resolve(x) |
mockRejectedValue(e) |
Função async que retorna Promise.reject(e) |
# Todos os testes (modo CI)
bun run test
# Com relatório de cobertura
bun run test:coverage
# Modo watch (desenvolvimento — re-executa ao salvar)
bun run test:watch
# Apenas testes unitários
bunx vitest run __tests__/unit
# Apenas testes E2E
bunx vitest run __tests__/e2e
# Um arquivo específico
bunx vitest run __tests__/unit/core/errors/errors.test.ts
# Testes que correspondem a um padrão
bunx vitest run -t "UserService"
# Com Node.js (ao invés de Bun)
npx vitest runPasso 1 — Crie o arquivo espelhando a estrutura de src/:
src/core/services/order/order-service.ts
→ __tests__/unit/core/services/order-service.test.ts
Passo 2 — Monte o mock do repository:
import { describe, expect, it, beforeEach, vi } from 'vitest'
import { OrderService } from '@/core/services/order/order-service'
function createMockRepository() {
return {
findById: vi.fn(),
create: vi.fn(),
// ... cada método da interface do repository
}
}Passo 3 — Instancie o service com o mock em beforeEach:
let service: OrderService
let repo: ReturnType<typeof createMockRepository>
beforeEach(() => {
repo = createMockRepository()
service = new OrderService(repo as any)
})Passo 4 — Escreva o happy path primeiro:
it('should return order when found', async () => {
vi.mocked(repo.findById).mockResolvedValue(mockOrder)
const result = await service.getOrder('order-id')
expect(result).toEqual({ success: true, data: mockOrder })
})Passo 5 — Adicione os caminhos de erro:
it('should return NOT_FOUND when order does not exist', async () => {
vi.mocked(repo.findById).mockResolvedValue(null)
const result = await service.getOrder('non-existent-id')
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error.code).toBe('NOT_FOUND')
}
})Passo 6 — Verifique que o repository foi chamado corretamente:
expect(repo.findById).toHaveBeenCalledWith('order-id')Passo 1 — Mock do service (se a rota usa um):
const mockOrderService = {
getOrder: vi.fn(),
createOrder: vi.fn()
}
vi.mock('@/core/services', () => ({
makeOrderService: () => mockOrderService
}))Passo 2 — Crie a instância do app:
import { createTestApp } from '../helpers/build-app'
const app = createTestApp()
afterAll(async () => {
await app.close()
})Passo 3 — Teste com app.inject():
it('should return 200 with order data', async () => {
mockOrderService.getOrder.mockResolvedValue(right(mockOrder))
const response = await app.inject({
method: 'GET',
url: `/api/v1/order/${mockOrder.id}`
})
expect(response.statusCode).toBe(200)
expect(response.json().id).toBe(mockOrder.id)
})Passo 4 — Teste validação na borda:
it('should return 400 for invalid id format', async () => {
const response = await app.inject({
method: 'GET',
url: '/api/v1/order/not-a-uuid'
})
expect(response.statusCode).toBe(400)
// Mock do service NÃO deve ter sido chamado
})- Todos os testes passam:
bun run test - Cobertura não diminuiu:
bun run test:coverage - Edge cases cobertos: not found, conflict, input inválido
- Mocks tipados corretamente
- Sem
console.lognos testes -
app.close()chamado noafterAll(testes E2E) - Mocks restaurados quando usar
vi.spyOn(mockRestore())
Repositories são wrappers finos sobre Knex. Seus testes dependem de SQL real executando contra PostgreSQL. Mockar o query builder do Knex criaria testes frágeis e de baixo valor — qualquer mudança na query quebraria o mock sem provar que a query funciona.
A estratégia correta para repositories é testes de integração contra um banco real (futuro).
Rota → Zod → Handler → Service (mock aqui) → Repository → DB
Se mockássemos no repository, estaríamos testando o service de novo (já coberto nos unit tests). Mockando no service, testamos o que é exclusivo dos E2E: pipeline HTTP, validação Zod, error handler, serialização.
// O projeto faz:
import { describe, expect, it } from 'vitest'
// Ao invés de usar os globais diretamente:
describe(...)Imports explícitos tornam dependências visíveis, melhoram autocomplete da IDE, e deixam claro de onde cada função vem. Em projetos grandes com múltiplos frameworks, isso evita ambiguidade.
inject() é nativo do Fastify, não abre porta TCP, e não causa conflito de portas em testes paralelos. Supertest existe como dependência para casos que precisem de semântica HTTP real (headers, cookies, redirects), mas para a maioria dos testes inject() é suficiente e mais rápido.
Números pragmáticos: suficientes para detectar bugs óbvios de entropia (seed fixo, overflow de timestamp), sem tornar os testes lentos. Não é um teste de carga — é uma verificação de sanidade.