Fumadocs + Code Hike
Backend

Vitest

Framework de testes moderno e rápido com suporte nativo ao Vite e TypeScript

O que é Vitest?

Vitest é um framework de testes unitários ultrarrápido alimentado pelo Vite. Oferece uma API compatível com Jest, mas com melhor performance, suporte nativo ao ESM, TypeScript e Hot Module Replacement (HMR) para testes.

Por que utilizamos Vitest na IngenioLab?

  • Performance extrema: Até 10x mais rápido que Jest
  • TypeScript nativo: Zero configuração para TypeScript
  • ESM first: Suporte completo a ES Modules
  • HMR para testes: Re-execução instantânea durante desenvolvimento
  • API familiar: Compatível com Jest/Jasmine
  • Vite integration: Compartilha configuração com Vite
  • Coverage nativo: Relatórios de cobertura integrados

Instalação

# Vitest core
npm install -D vitest
# Utilitários de teste adicionais
npm install -D @vitest/ui @vitest/coverage-v8
npm install -D happy-dom jsdom # Para testes de DOM
# Para testes de API/HTTP
npm install -D supertest @types/supertest

Configuração IngenioLab

1. vitest.config.ts:

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import path from 'node:path'
export default defineConfig({
test: {
// Ambiente global
globals: true,
environment: 'node', // ou 'jsdom' para testes frontend
// Configuração de arquivos
include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/cypress/**',
'**/.{idea,git,cache,output,temp}/**'
],
// Setup files
setupFiles: ['./tests/setup.ts'],
// Coverage
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'coverage/**',
'dist/**',
'packages/*/test{,s}/**',
'**/*.d.ts',
'cypress/**',
'test{,s}/**',
'test{,-*}.{js,cjs,mjs,ts,tsx,jsx}',
'**/*{.,-}test.{js,cjs,mjs,ts,tsx,jsx}',
'**/__tests__/**',
'**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc}.config.*',
'**/.{eslint,mocha}rc.{js,cjs}'
],
thresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
},
// Reporters
reporter: ['verbose', 'json', 'html'],
outputFile: {
json: './coverage/test-results.json',
html: './coverage/index.html'
}
},
// Resolução de imports
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@/tests': path.resolve(__dirname, './tests')
}
}
})

2. Setup file para configurações globais:

// tests/setup.ts
import { vi, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'
import { PrismaClient } from '@prisma/client'
// Mock do Prisma Client
vi.mock('@prisma/client', () => ({
PrismaClient: vi.fn()
}))
// Mock de variáveis de ambiente
beforeAll(() => {
process.env.NODE_ENV = 'test'
process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test'
process.env.JWT_SECRET = 'test-secret-key'
})
// Limpar mocks entre testes
afterEach(() => {
vi.clearAllMocks()
})
// Global test utilities
global.console = {
...console,
log: vi.fn(), // Mock console.log em testes se necessário
error: console.error,
warn: console.warn,
info: vi.fn()
}

3. Scripts no package.json:

{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest --watch",
"test:related": "vitest related"
}
}

Estrutura de Testes IngenioLab

src/
├── services/
│ ├── userService.ts
│ └── __tests__/
│ └── userService.test.ts
├── routes/
│ ├── users.ts
│ └── __tests__/
│ └── users.test.ts
├── utils/
│ ├── validation.ts
│ └── __tests__/
│ └── validation.test.ts
└── __tests__/
├── integration/
│ └── api.test.ts
└── mocks/
├── prisma.ts
└── fastify.ts

Padrões de Teste

1. Testes Unitários (Services):

// src/services/__tests__/userService.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { userService } from '../userService'
import { prisma } from '../../lib/prisma'
// Mock do Prisma
vi.mock('../../lib/prisma', () => ({
prisma: {
user: {
create: vi.fn(),
findUnique: vi.fn(),
findMany: vi.fn(),
update: vi.fn(),
delete: vi.fn()
}
}
}))
describe('UserService', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('createUser', () => {
it('should create user successfully', async () => {
// Arrange
const userData = {
name: 'João Silva',
email: 'joao@exemplo.com',
password: 'senha123'
}
const expectedUser = {
id: '123',
...userData,
createdAt: new Date()
}
vi.mocked(prisma.user.create).mockResolvedValue(expectedUser)
// Act
const result = await userService.createUser(userData)
// Assert
expect(prisma.user.create).toHaveBeenCalledWith({
data: userData,
select: expect.any(Object)
})
expect(result).toEqual(expectedUser)
})
it('should throw error for duplicate email', async () => {
// Arrange
const userData = {
name: 'João Silva',
email: 'joao@exemplo.com',
password: 'senha123'
}
const duplicateError = new Error('Unique constraint failed')
duplicateError.code = 'P2002'
vi.mocked(prisma.user.create).mockRejectedValue(duplicateError)
// Act & Assert
await expect(userService.createUser(userData))
.rejects.toThrow('Email já está em uso')
})
})
describe('getUserById', () => {
it('should return user when found', async () => {
// Arrange
const userId = '123'
const expectedUser = {
id: userId,
name: 'João Silva',
email: 'joao@exemplo.com'
}
vi.mocked(prisma.user.findUnique).mockResolvedValue(expectedUser)
// Act
const result = await userService.getUserById(userId)
// Assert
expect(prisma.user.findUnique).toHaveBeenCalledWith({
where: { id: userId },
include: expect.any(Object)
})
expect(result).toEqual(expectedUser)
})
it('should throw error when user not found', async () => {
// Arrange
const userId = '999'
vi.mocked(prisma.user.findUnique).mockResolvedValue(null)
// Act & Assert
await expect(userService.getUserById(userId))
.rejects.toThrow('Usuário não encontrado')
})
})
})

2. Testes de Integração (API Routes):

// src/routes/__tests__/users.test.ts
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'
import { build } from '../../app'
import { FastifyInstance } from 'fastify'
import { prisma } from '../../lib/prisma'
import jwt from 'jsonwebtoken'
describe('Users API', () => {
let app: FastifyInstance
let authToken: string
beforeAll(async () => {
app = await build({ logger: false })
// Criar token de teste
authToken = jwt.sign(
{ id: '123', role: 'ADMIN' },
process.env.JWT_SECRET!
)
})
afterAll(async () => {
await app.close()
})
beforeEach(async () => {
// Limpar dados de teste
await prisma.user.deleteMany({
where: { email: { contains: 'test' } }
})
})
describe('POST /users', () => {
it('should create user successfully', async () => {
// Arrange
const userData = {
name: 'João Test',
email: 'joao.test@exemplo.com',
password: 'MinhaSenh@123'
}
// Act
const response = await app.inject({
method: 'POST',
url: '/users',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json'
},
payload: userData
})
// Assert
expect(response.statusCode).toBe(201)
const responseBody = response.json()
expect(responseBody).toHaveProperty('id')
expect(responseBody.name).toBe(userData.name)
expect(responseBody.email).toBe(userData.email)
expect(responseBody).not.toHaveProperty('password') // Não deve retornar senha
})
it('should return 400 for invalid email', async () => {
// Arrange
const userData = {
name: 'João Test',
email: 'email-inválido',
password: 'MinhaSenh@123'
}
// Act
const response = await app.inject({
method: 'POST',
url: '/users',
headers: {
authorization: `Bearer ${authToken}`,
'content-type': 'application/json'
},
payload: userData
})
// Assert
expect(response.statusCode).toBe(400)
const responseBody = response.json()
expect(responseBody).toHaveProperty('message')
expect(responseBody.errors).toBeDefined()
})
it('should return 401 without auth token', async () => {
// Act
const response = await app.inject({
method: 'POST',
url: '/users',
headers: {
'content-type': 'application/json'
},
payload: {}
})
// Assert
expect(response.statusCode).toBe(401)
})
})
describe('GET /users/:id', () => {
it('should return user by id', async () => {
// Arrange - Criar usuário de teste
const testUser = await prisma.user.create({
data: {
name: 'João Test',
email: 'joao.get.test@exemplo.com',
password: 'hashed-password'
}
})
// Act
const response = await app.inject({
method: 'GET',
url: `/users/${testUser.id}`,
headers: {
authorization: `Bearer ${authToken}`
}
})
// Assert
expect(response.statusCode).toBe(200)
const responseBody = response.json()
expect(responseBody.id).toBe(testUser.id)
expect(responseBody.name).toBe(testUser.name)
expect(responseBody.email).toBe(testUser.email)
})
it('should return 404 for non-existent user', async () => {
// Act
const response = await app.inject({
method: 'GET',
url: '/users/999',
headers: {
authorization: `Bearer ${authToken}`
}
})
// Assert
expect(response.statusCode).toBe(404)
})
})
})

3. Testes com Mocks Avançados:

// tests/mocks/prisma.ts
import { vi } from 'vitest'
import { PrismaClient } from '@prisma/client'
import { mockDeep, DeepMockProxy } from 'vitest-mock-extended'
export const prismaMock = mockDeep<PrismaClient>()
// Mock factory para criar dados de teste
export const createMockUser = (overrides = {}) => ({
id: '123',
name: 'João Silva',
email: 'joao@exemplo.com',
role: 'USER',
isActive: true,
createdAt: new Date('2023-01-01'),
updatedAt: new Date('2023-01-01'),
...overrides
})
export const createMockPost = (overrides = {}) => ({
id: '456',
title: 'Post de Teste',
content: 'Conteúdo do post',
published: true,
authorId: '123',
createdAt: new Date('2023-01-01'),
updatedAt: new Date('2023-01-01'),
...overrides
})

4. Testes de Utilitários:

// src/utils/__tests__/validation.test.ts
import { describe, it, expect } from 'vitest'
import { validateEmail, sanitizeString, generateSlug } from '../validation'
describe('Validation Utils', () => {
describe('validateEmail', () => {
it.each([
['test@example.com', true],
['user.name+tag@example.co.uk', true],
['invalid-email', false],
['@example.com', false],
['test@', false],
['', false]
])('should validate email %s as %s', (email, expected) => {
expect(validateEmail(email)).toBe(expected)
})
})
describe('sanitizeString', () => {
it('should remove HTML tags', () => {
const input = '<script>alert("xss")</script>Hello World'
const result = sanitizeString(input)
expect(result).toBe('Hello World')
})
it('should trim whitespace', () => {
const input = ' Hello World '
const result = sanitizeString(input)
expect(result).toBe('Hello World')
})
})
describe('generateSlug', () => {
it('should generate URL-friendly slug', () => {
const input = 'Meu Artigo Sobre TypeScript'
const result = generateSlug(input)
expect(result).toBe('meu-artigo-sobre-typescript')
})
it('should handle special characters', () => {
const input = 'Artigo com Acentos: São Paulo & Rio!'
const result = generateSlug(input)
expect(result).toBe('artigo-com-acentos-sao-paulo-rio')
})
})
})

5. Testes Assíncronos e Timers:

// src/services/__tests__/cacheService.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { cacheService } from '../cacheService'
describe('CacheService', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('should cache value with TTL', async () => {
// Arrange
const key = 'test-key'
const value = { data: 'test' }
const ttl = 1000 // 1 segundo
// Act
await cacheService.set(key, value, ttl)
const result1 = await cacheService.get(key)
// Assert
expect(result1).toEqual(value)
// Advance time beyond TTL
vi.advanceTimersByTime(1001)
const result2 = await cacheService.get(key)
expect(result2).toBeNull()
})
it('should handle concurrent cache operations', async () => {
// Arrange
const key = 'concurrent-key'
const promises = []
// Act - Simular múltiplas operações concorrentes
for (let i = 0; i < 10; i++) {
promises.push(cacheService.set(`${key}-${i}`, { value: i }))
}
await Promise.all(promises)
// Assert
for (let i = 0; i < 10; i++) {
const result = await cacheService.get(`${key}-${i}`)
expect(result).toEqual({ value: i })
}
})
})

Comandos de Desenvolvimento

# Executar testes em modo watch
npm run test
# Executar todos os testes uma vez
npm run test:run
# Executar testes com UI web
npm run test:ui
# Executar com coverage
npm run test:coverage
# Executar testes relacionados a arquivos alterados
npm run test:related
# Executar teste específico
npm run test -- users.test.ts
# Executar testes com pattern
npm run test -- --grep "should create user"
# Modo debug
npm run test -- --inspect-brk

Configurações Avançadas

1. Testes com diferentes ambientes:

// vitest.config.integration.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'node',
testTimeout: 30000, // Timeout maior para testes de integração
include: ['**/*.integration.test.ts'],
setupFiles: ['./tests/setup.integration.ts']
}
})

2. Parallel testing:

// vitest.config.ts
export default defineConfig({
test: {
pool: 'threads', // ou 'forks'
poolOptions: {
threads: {
singleThread: false,
maxThreads: 4,
minThreads: 2
}
}
}
})

3. Custom matchers:

// tests/matchers.ts
import { expect } from 'vitest'
expect.extend({
toBeValidUuid(received: string) {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
return {
pass: uuidRegex.test(received),
message: () => `Expected ${received} to be a valid UUID`
}
},
toHaveBeenCalledWithUser(received: any, expected: any) {
const calls = received.mock.calls
const found = calls.some((call: any) =>
call[0] && call[0].email === expected.email
)
return {
pass: found,
message: () => `Expected function to have been called with user ${expected.email}`
}
}
})
// Usar nos testes
expect('550e8400-e29b-41d4-a716-446655440000').toBeValidUuid()

Debugging e Troubleshooting

1. Debug de testes:

# Executar com debugging
npm run test -- --inspect-brk
# No VS Code, adicionar breakpoints e usar F5

2. Logging em testes:

import { it, expect } from 'vitest'
it('should debug test', () => {
const result = someFunction()
// Log temporário para debug
console.log('Debug result:', result)
expect(result).toBe('expected')
})