Este documento abrange todas as estratégias, ferramentas e práticas de teste implementadas no projeto Varion, garantindo qualidade, confiabilidade e manutenibilidade do código.
- Backend: ✅ 98.58% de cobertura (517 testes implementados)
- Frontend: 🟡 Configuração básica (em desenvolvimento)
- E2E: 🟡 Configuração planejada com Playwright
- Performance: 🟡 Configuração planejada com Artillery
- Escopo: Funções, métodos e componentes isolados
- Framework: Jest + TypeScript
- Cobertura Atual: 98.58% (Target: > 80%)
- Total de Testes: 517 testes
- Escopo: Interação entre módulos e APIs
- Framework: Jest + Supertest + SQLite em memória
- Foco: APIs completas e fluxos de dados
- Banco de Teste: SQLite em memória para isolamento
- Escopo: Fluxos completos de usuário
- Framework: Playwright
- Ambiente: Docker containers
- Escopo: Carga e stress
- Ferramentas: Artillery, Lighthouse
- Métricas: Response time, throughput
backend/
├── src/
│ ├── __tests__/
│ │ ├── basic.test.ts # Testes básicos de configuração
│ │ ├── integration.test.ts # Testes de integração principais
│ │ ├── integration/ # Testes de integração específicos
│ │ │ ├── states.integration.test.ts # 436 linhas - Testes completos de Estados
│ │ │ └── todo.integration.test.ts # Testes de TODO items
│ │ ├── examples/ # Exemplos e templates
│ │ │ ├── unit.example.test.ts # Exemplo de testes unitários
│ │ │ └── integration.example.test.ts # Exemplo de testes de integração
│ │ ├── setup/ # Configuração de testes
│ │ │ └── database.ts # Setup do banco de teste
│ │ ├── mocks/ # Mocks reutilizáveis
│ │ └── templates/ # Templates para novos testes
│ └── modules/ # Testes distribuídos por módulo
│ ├── projects/
│ │ ├── project.controller.test.ts # Testes do controller
│ │ ├── project.service.test.ts # Testes do service
│ │ ├── project.entity.test.ts # Testes da entidade
│ │ ├── project.route.test.ts # Testes das rotas
│ │ └── project.schema.test.ts # Testes de validação
│ ├── states/
│ │ ├── state.controller.test.ts
│ │ ├── state.service.test.ts
│ │ ├── state.entity.test.ts
│ │ └── state.schema.test.ts
│ ├── todo/
│ │ ├── todo.service.test.ts
│ │ ├── todo.entity.test.ts
│ │ └── todo.schema.test.ts
│ └── comment/
│ ├── comment.service.test.ts
│ ├── comment.entity.test.ts
│ └── comment.schema.test.ts
├── jest.config.js # Configuração principal do Jest
├── jest.setup.js # Setup global dos testes
└── coverage/ # Relatórios de cobertura
-----------------------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
-----------------------------------|---------|----------|---------|---------|
All files | 98.58 | 83.06 | 96.62 | 100 |
config | 100 | 100 | 100 | 100 |
modules/comment | 100 | 88.88 | 100 | 100 |
modules/projects | 96.78 | 72.16 | 89.65 | 100 |
modules/states | 100 | 88 | 100 | 100 |
modules/todo | 100 | 100 | 100 | 100 |
utils | 100 | 100 | 100 | 100 |
utils/tools | 100 | 100 | 100 | 100 |
-----------------------------------|---------|----------|---------|---------|
Test Suites: 33 passed, 33 total
Tests: 517 passed, 517 total
// backend/jest.config.js
/** @type {import('jest').Config} */
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: [
'**/__tests__/**/*.test.ts',
'**/*.test.ts'
],
transform: {
'^.+\\.ts$': ['ts-jest', {
tsconfig: {
experimentalDecorators: true,
emitDecoratorMetadata: true
}
}]
},
moduleFileExtensions: ['ts', 'js', 'json', 'node'],
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.test.ts',
'!src/**/*.spec.ts',
'!src/server.ts',
'!src/migrations/**',
'!src/__tests__/**'
],
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70
}
}
};// backend/jest.setup.js
// Jest setup file
require('reflect-metadata');
// Mock console.error para testes mais limpos
const originalError = console.error;
beforeAll(() => {
console.error = (...args) => {
if (
typeof args[0] === 'string' &&
args[0].includes('Warning') &&
(args[0].includes('deprecated') || args[0].includes('experimental'))
) {
return;
}
originalError.call(console, ...args);
};
});
afterAll(() => {
console.error = originalError;
});// backend/src/__tests__/setup/database.ts
import { DataSource } from 'typeorm';
import { Express } from 'express';
import { Project } from '../../modules/projects/project.entity';
import { State } from '../../modules/states/state.entity';
import { Comment } from '../../modules/comment/comment.entity';
import { TodoItem } from '../../modules/todo/todo.entity';
import { ProjectStatusHistory } from '../../modules/projects/project-status-history.entity';
import { App } from '../../config/app';
export const createTestDataSource = async (): Promise<DataSource> => {
const dataSource = new DataSource({
type: 'sqlite',
database: ':memory:',
entities: [Project, State, Comment, TodoItem, ProjectStatusHistory],
synchronize: true,
logging: false,
});
return await dataSource.initialize();
};
export const setupTestDatabase = async (): Promise<Express> => {
// Configuração do banco de teste e retorno da aplicação
const testDataSource = await createTestDataSource();
// Mock do AppDataSource para usar o banco de teste
jest.mock('../../config/data-source', () => ({
AppDataSource: testDataSource
}));
return App.getApp();
};
export const cleanupTestDatabase = async (dataSource: DataSource) => {
if (!dataSource.isInitialized) return;
const entities = dataSource.entityMetadatas;
for (const entity of entities) {
const repository = dataSource.getRepository(entity.name);
await repository.clear();
}
};// backend/src/__tests__/integration/states.integration.test.ts
import request from 'supertest';
import { Application } from 'express';
import { DataSource } from 'typeorm';
import { setupTestDatabase, teardownTestDatabase, cleanupTestDatabase } from '../setup/database';
import { State } from '../../modules/states/state.entity';
describe('States Integration Tests', () => {
let app: Application;
let dataSource: DataSource;
beforeAll(async () => {
app = await setupTestDatabase();
dataSource = require('../../config/data-source').AppDataSource;
});
afterAll(async () => {
await teardownTestDatabase();
});
beforeEach(async () => {
await cleanupTestDatabase(dataSource);
});
describe('POST /states', () => {
it('should create a new state successfully', async () => {
// Arrange
const stateData = {
name: 'In Development',
color: '#3498db',
order: 1
};
// Act
const response = await request(app)
.post('/states')
.send(stateData)
.expect(201);
// Assert
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('id');
expect(response.body.data.name).toBe(stateData.name);
expect(response.body.data.color).toBe(stateData.color);
// Verificar se foi salvo no banco
const stateRepo = dataSource.getRepository(State);
const savedState = await stateRepo.findOne({ where: { id: response.body.data.id } });
expect(savedState).toBeTruthy();
expect(savedState?.name).toBe(stateData.name);
});
it('should return 409 for duplicate state name', async () => {
// Arrange
const stateData = {
name: 'Duplicate State',
color: '#FF0000',
order: 1
};
// Criar o primeiro estado
await request(app)
.post('/states')
.send(stateData)
.expect(201);
// Act - Tentar criar estado com mesmo nome
const response = await request(app)
.post('/states')
.send(stateData)
.expect(409);
// Assert
expect(response.body.success).toBe(false);
expect(response.body.error).toContain('Nome de estado já existe');
});
});
});// backend/src/__tests__/examples/unit.example.test.ts
import { ProjectService } from '../../modules/projects/project.service';
import { AppDataSource } from '../../config/data-source';
import { Project } from '../../modules/projects/project.entity';
// Mock AppDataSource
jest.mock('../../config/data-source', () => ({
AppDataSource: {
getRepository: jest.fn(),
},
}));
describe('Unit Test Example - ProjectService', () => {
let mockRepository: jest.Mocked<any>;
beforeEach(() => {
mockRepository = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
delete: jest.fn(),
update: jest.fn(),
clear: jest.fn(),
};
(AppDataSource.getRepository as jest.Mock).mockReturnValue(mockRepository);
});
afterEach(() => {
jest.clearAllMocks();
});
describe('createProject', () => {
it('should create project successfully', async () => {
// Arrange
const projectData = {
title: 'Test Project',
description: 'Test Description',
stateId: 1
};
const savedProject = {
id: '1',
...projectData,
code: 'PRJ-ABC123',
createdAt: new Date(),
updatedAt: new Date(),
comments: [],
todo: [],
statusHistory: []
} as Project;
mockRepository.create.mockReturnValue(savedProject);
mockRepository.save.mockResolvedValue(savedProject);
// Act
const result = await ProjectService.createProject(projectData);
// Assert
expect(mockRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
title: projectData.title,
description: projectData.description,
stateId: projectData.stateId,
code: expect.any(String)
})
);
expect(mockRepository.save).toHaveBeenCalled();
expect(result).toEqual(savedProject);
});
it('should handle database error', async () => {
// Arrange
const projectData = {
title: 'Test Project',
description: 'Test Description',
stateId: 1
};
mockRepository.create.mockReturnValue({});
mockRepository.save.mockRejectedValue(new Error('Database error'));
// Act & Assert
await expect(ProjectService.createProject(projectData))
.rejects
.toThrow('Database error');
});
});
});frontend/
├── src/
│ ├── __tests__/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── utils/
│ │ └── pages/
│ ├── __mocks__/
│ └── test-utils.tsx
├── e2e/
│ ├── tests/
│ ├── fixtures/
│ └── playwright.config.ts
├── jest.config.js
└── jest.setup.js
// frontend/jest.config.js
const nextJest = require('next/jest');
const createJestConfig = nextJest({
dir: './',
});
const customJestConfig = {
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapping: {
'^@/(.*)$': '<rootDir>/src/$1',
},
testEnvironment: 'jest-environment-jsdom',
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/pages/api/**',
],
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
};
module.exports = createJestConfig(customJestConfig);// frontend/jest.setup.js
import '@testing-library/jest-dom';
import { server } from './src/__mocks__/server';
// Estabelecer API mocking antes de todos os testes
beforeAll(() => server.listen());
// Resetar handlers entre testes
afterEach(() => server.resetHandlers());
// Limpar após todos os testes
afterAll(() => server.close());
// Mock next/router
jest.mock('next/router', () => ({
useRouter() {
return {
route: '/',
pathname: '/',
query: '',
asPath: '',
push: jest.fn(),
pop: jest.fn(),
reload: jest.fn(),
back: jest.fn(),
prefetch: jest.fn().mockResolvedValue(undefined),
beforePopState: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
},
};
},
}));// frontend/src/test-utils.tsx
import React, { ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const AllTheProviders = ({ children }: { children: React.ReactNode }) => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
};
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>,
) => render(ui, { wrapper: AllTheProviders, ...options });
export * from '@testing-library/react';
export { customRender as render };// frontend/src/__mocks__/handlers.ts
import { rest } from 'msw';
export const handlers = [
rest.get('/api/projects', (req, res, ctx) => {
return res(
ctx.json({
success: true,
data: [
{
id: '1',
name: 'Test Project 1',
description: 'Test Description 1',
status: 'active',
},
{
id: '2',
name: 'Test Project 2',
description: 'Test Description 2',
status: 'completed',
},
],
})
);
}),
rest.post('/api/projects', (req, res, ctx) => {
return res(
ctx.status(201),
ctx.json({
success: true,
data: {
id: '3',
name: 'New Project',
description: 'New Description',
status: 'active',
},
})
);
}),
];// frontend/src/__tests__/components/ProjectForm.test.tsx
import { render, screen, fireEvent, waitFor } from '../test-utils';
import { ProjectForm } from '../../components/ProjectForm';
describe('ProjectForm', () => {
const mockOnSubmit = jest.fn();
beforeEach(() => {
mockOnSubmit.mockClear();
});
it('renders form fields correctly', () => {
render(<ProjectForm onSubmit={mockOnSubmit} />);
expect(screen.getByLabelText(/nome do projeto/i)).toBeInTheDocument();
expect(screen.getByLabelText(/descrição/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /criar projeto/i })).toBeInTheDocument();
});
it('submits form with valid data', async () => {
render(<ProjectForm onSubmit={mockOnSubmit} />);
const nameInput = screen.getByLabelText(/nome do projeto/i);
const descriptionInput = screen.getByLabelText(/descrição/i);
const submitButton = screen.getByRole('button', { name: /criar projeto/i });
fireEvent.change(nameInput, { target: { value: 'Test Project' } });
fireEvent.change(descriptionInput, { target: { value: 'Test Description' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockOnSubmit).toHaveBeenCalledWith({
name: 'Test Project',
description: 'Test Description',
});
});
});
it('shows validation error for empty name', async () => {
render(<ProjectForm onSubmit={mockOnSubmit} />);
const submitButton = screen.getByRole('button', { name: /criar projeto/i });
fireEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/nome é obrigatório/i)).toBeInTheDocument();
});
expect(mockOnSubmit).not.toHaveBeenCalled();
});
it('disables submit button while loading', () => {
render(<ProjectForm onSubmit={mockOnSubmit} isLoading={true} />);
const submitButton = screen.getByRole('button', { name: /criando.../i });
expect(submitButton).toBeDisabled();
});
});// frontend/src/__tests__/hooks/useProjects.test.tsx
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useProjects } from '../../hooks/useProjects';
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
};
describe('useProjects', () => {
it('fetches projects successfully', async () => {
const { result } = renderHook(() => useProjects(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toHaveLength(2);
expect(result.current.data?.[0].name).toBe('Test Project 1');
});
it('handles error state', async () => {
// Mock error response
const { result } = renderHook(() => useProjects(), {
wrapper: createWrapper(),
});
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
});
});// frontend/playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});// frontend/e2e/tests/project-management.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Project Management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should create a new project', async ({ page }) => {
// Navigate to new project page
await page.click('[data-testid="new-project-button"]');
await expect(page).toHaveURL('/projects/new');
// Fill project form
await page.fill('[data-testid="project-name"]', 'E2E Test Project');
await page.fill('[data-testid="project-description"]', 'Created by E2E test');
// Submit form
await page.click('[data-testid="submit-button"]');
// Verify success
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
await expect(page).toHaveURL('/projects');
await expect(page.locator('text=E2E Test Project')).toBeVisible();
});
test('should edit existing project', async ({ page }) => {
// Create project first
await page.goto('/projects/new');
await page.fill('[data-testid="project-name"]', 'Project to Edit');
await page.click('[data-testid="submit-button"]');
// Navigate to edit
await page.click('[data-testid="edit-project-button"]');
// Edit project
await page.fill('[data-testid="project-name"]', 'Edited Project');
await page.click('[data-testid="submit-button"]');
// Verify changes
await expect(page.locator('text=Edited Project')).toBeVisible();
});
test('should delete project', async ({ page }) => {
// Create project first
await page.goto('/projects/new');
await page.fill('[data-testid="project-name"]', 'Project to Delete');
await page.click('[data-testid="submit-button"]');
// Delete project
await page.click('[data-testid="delete-project-button"]');
await page.click('[data-testid="confirm-delete"]');
// Verify deletion
await expect(page.locator('text=Project to Delete')).not.toBeVisible();
});
});# performance/load-test.yml
config:
target: 'http://localhost:3001'
phases:
- duration: 60
arrivalRate: 1
name: "Warm up"
- duration: 300
arrivalRate: 5
rampTo: 50
name: "Ramp up load"
- duration: 600
arrivalRate: 50
name: "Sustained load"
processor: "./processor.js"
scenarios:
- name: "Project CRUD Operations"
weight: 70
flow:
- get:
url: "/api/projects"
- post:
url: "/api/projects"
json:
name: "Load Test Project {{ $randomString() }}"
description: "Created during load test"
- get:
url: "/api/projects/{{ id }}"
capture:
- json: "$.data.id"
as: "projectId"
- name: "State Management"
weight: 30
flow:
- get:
url: "/api/states"
- post:
url: "/api/states"
json:
name: "Test State {{ $randomString() }}"
color: "#{{ $randomHex() }}"# scripts/performance-test.sh
#!/bin/bash
echo "🚀 Iniciando testes de performance..."
# Subir aplicação em modo de produção
docker-compose -f docker-compose.prod.yml up -d
# Aguardar inicialização
sleep 30
# Executar testes de carga
cd performance
artillery run load-test.yml --output load-test-report.json
# Gerar relatório HTML
artillery report load-test-report.json
# Teste de performance frontend
cd ../frontend
pnpm lighthouse http://localhost:3000 --output html --output-path ./lighthouse-report.html
echo "✅ Testes de performance concluídos!"
echo "📊 Relatórios disponíveis em:"
echo " - performance/load-test-report.json.html"
echo " - frontend/lighthouse-report.html"// backend/package.json - Scripts disponíveis
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:verbose": "jest --coverage --verbose"
}
}# Executar todos os testes
cd backend && pnpm test
# Executar com cobertura (atual: 98.58%)
cd backend && pnpm test:coverage
# Executar em modo watch (desenvolvimento)
cd backend && pnpm test:watch
# Executar com informações detalhadas
cd backend && pnpm test:verbose
# Executar testes específicos
cd backend && pnpm test states.integration
cd backend && pnpm test project.service
cd backend && pnpm test basic.test✅ Test Suites: 33 passed, 33 total
✅ Tests: 517 passed, 517 total
✅ Coverage: 98.58% statements, 83.06% branches
✅ Time: ~4.1s para execução completa
✅ Snapshots: 0 total (não utilizados)
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Run backend tests
run: |
cd backend
pnpm test:ci
- name: Run frontend tests
run: |
cd frontend
pnpm test:ci
- name: Upload coverage
uses: codecov/codecov-action@v3
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Install Playwright
run: npx playwright install --with-deps
- name: Start application
run: |
docker-compose up -d
sleep 30
- name: Run E2E tests
run: |
cd frontend
pnpm test:e2e
- name: Upload test results
uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: frontend/playwright-report/- Backend: ✅ 98.58% (Target atingido: > 80%)
- Frontend: 🟡 Não implementado (Target: > 75%)
- E2E: 🟡 Não implementado (Target: > 90% dos fluxos críticos)
Módulo | Statements | Branches | Functions | Lines |
------------------------|------------|----------|-----------|-------|
config/ | 100% | 100% | 100% | 100% |
modules/comment/ | 100% | 88.88% | 100% | 100% |
modules/projects/ | 96.78% | 72.16% | 89.65% | 100% |
modules/states/ | 100% | 88% | 100% | 100% |
modules/todo/ | 100% | 100% | 100% | 100% |
utils/ | 100% | 100% | 100% | 100% |
utils/tools/ | 100% | 100% | 100% | 100% |
------------------------|------------|----------|-----------|-------|
TOTAL | 98.58% | 83.06% | 96.62% | 100% |
# Visualizar cobertura em HTML
open backend/coverage/lcov-report/index.html
# Cobertura em formato texto
cat backend/coverage/lcov.info
# Relatório JSON para CI/CD
cat backend/coverage/coverage-final.json- modules/projects/ - 72.16% branches
- Faltam testes para alguns cenários de erro
- Alguns métodos condicionais não cobertos
- modules/comment/ - 88.88% branches
- Casos edge de validação
- modules/states/ - 88% branches
- Validações específicas de estado
- Testes flaky: Usar
waitFore timeouts adequados - Mocks não funcionam: Verificar ordem de imports
- E2E falham: Verificar se aplicação está rodando
- Performance ruim: Verificar recursos do sistema
# Debug jest
node --inspect-brk node_modules/.bin/jest --runInBand
# Debug playwright
npx playwright test --debug
# Logs detalhados
DEBUG=* pnpm test- Testes unitários passando
- Cobertura mínima atingida
- Linting sem erros
- TypeScript sem erros
- Todos os testes passando
- Testes E2E executados
- Performance validada
- Smoke tests em produção