A Importancia da Estrategia de Testes
Para garantir a qualidade do software, uma estrategia de testes adequada e essencial. Um design de testes eficiente permite um desenvolvimento sustentavel, equilibrando velocidade de desenvolvimento e qualidade.
flowchart TB
subgraph Purpose["Objetivos dos Testes"]
subgraph QA["1. Garantia de Qualidade (Quality Assurance)"]
QA1["Deteccao precoce de bugs"]
QA2["Prevencao de regressao"]
QA3["Verificacao de especificacoes"]
end
subgraph Design["2. Melhoria de Design (Design Improvement)"]
D1["Aumento da testabilidade"]
D2["Promocao de design desacoplado"]
D3["Clareza de interfaces"]
end
subgraph Docs["3. Documentacao"]
Doc1["Especificacao executavel"]
Doc2["Exemplos de uso"]
Doc3["Explicitacao de casos limite"]
end
end
Piramide de Testes
flowchart TB
subgraph Pyramid["Piramide de Testes"]
E2E["Testes E2E<br/>(poucos, alto custo)"]
Integration["Testes de Integracao<br/>(quantidade moderada)"]
Unit["Testes Unitarios<br/>(muitos, baixo custo)"]
E2E --> Integration --> Unit
end
Caracteristicas:
| Nivel | Velocidade de Execucao | Confiabilidade | Custo |
|---|---|---|---|
| E2E | Lento | Baixa | Alto |
| Integracao | Moderada | Moderada | Moderado |
| Unitario | Rapido | Alta | Baixo |
Testes Unitarios
Principios Basicos (F.I.R.S.T)
// Testes unitarios seguindo os principios F.I.R.S.T
// Fast (Rapido) - Testes devem executar rapidamente
// Independent (Independente) - Sem dependencias entre testes
// Repeatable (Repetivel) - Mesmo resultado em qualquer execucao
// Self-Validating (Auto-validavel) - Sucesso/falha claramente definidos
// Timely (Oportuno) - Escrever antes ou depois do codigo de producao
import { describe, it, expect, beforeEach } from 'vitest';
// Funcao utilitaria a ser testada
function calculateTax(price: number, taxRate: number): number {
if (price < 0) throw new Error('Price cannot be negative');
if (taxRate < 0 || taxRate > 1) throw new Error('Invalid tax rate');
return Math.round(price * taxRate);
}
describe('calculateTax', () => {
// Casos de sucesso
it('should calculate tax correctly', () => {
expect(calculateTax(1000, 0.1)).toBe(100);
});
it('should round to nearest integer', () => {
expect(calculateTax(999, 0.1)).toBe(100); // 99.9 → 100
});
// Testes de valores limite
it('should return 0 for zero price', () => {
expect(calculateTax(0, 0.1)).toBe(0);
});
it('should handle zero tax rate', () => {
expect(calculateTax(1000, 0)).toBe(0);
});
it('should handle 100% tax rate', () => {
expect(calculateTax(1000, 1)).toBe(1000);
});
// Casos de erro
it('should throw for negative price', () => {
expect(() => calculateTax(-100, 0.1)).toThrow('Price cannot be negative');
});
it('should throw for invalid tax rate', () => {
expect(() => calculateTax(1000, 1.5)).toThrow('Invalid tax rate');
expect(() => calculateTax(1000, -0.1)).toThrow('Invalid tax rate');
});
});
Padrao AAA
// Padrao Arrange-Act-Assert
import { describe, it, expect } from 'vitest';
class ShoppingCart {
private items: Array<{ name: string; price: number; quantity: number }> = [];
addItem(name: string, price: number, quantity: number = 1): void {
this.items.push({ name, price, quantity });
}
removeItem(name: string): void {
this.items = this.items.filter(item => item.name !== name);
}
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
getItemCount(): number {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}
}
describe('ShoppingCart', () => {
it('should calculate total correctly', () => {
// Arrange (Preparar)
const cart = new ShoppingCart();
cart.addItem('Apple', 100, 3);
cart.addItem('Banana', 80, 2);
// Act (Agir)
const total = cart.getTotal();
// Assert (Verificar)
expect(total).toBe(460); // 100*3 + 80*2
});
it('should remove item correctly', () => {
// Arrange
const cart = new ShoppingCart();
cart.addItem('Apple', 100, 1);
cart.addItem('Banana', 80, 1);
// Act
cart.removeItem('Apple');
// Assert
expect(cart.getTotal()).toBe(80);
expect(cart.getItemCount()).toBe(1);
});
});
Test Doubles
flowchart TB
subgraph TestDoubles["Tipos de Test Doubles"]
subgraph Stub["1. Stub"]
S1["Retorna valores pre-definidos"]
S2["Simula dependencias externas"]
S3["Controla apenas saida para entrada"]
end
subgraph Mock["2. Mock"]
M1["Verifica chamadas esperadas"]
M2["Confirma numero de chamadas e argumentos"]
M3["Usado para verificar comportamento"]
end
subgraph Spy["3. Spy"]
Sp1["Usa implementacao real enquanto registra chamadas"]
Sp2["Permite verificar chamadas posteriormente"]
Sp3["Mock parcial"]
end
subgraph Fake["4. Fake"]
F1["Versao simplificada da implementacao"]
F2["Ex: banco de dados em memoria"]
F3["Mesma interface que producao"]
end
end
Exemplo de Implementacao de Test Doubles
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Interfaces das dependencias
interface EmailService {
send(to: string, subject: string, body: string): Promise<boolean>;
}
interface UserRepository {
findById(id: string): Promise<User | null>;
save(user: User): Promise<void>;
}
interface User {
id: string;
email: string;
name: string;
}
// Classe a ser testada
class UserService {
constructor(
private userRepo: UserRepository,
private emailService: EmailService
) {}
async notifyUser(userId: string, message: string): Promise<boolean> {
const user = await this.userRepo.findById(userId);
if (!user) {
throw new Error('User not found');
}
return this.emailService.send(
user.email,
'Notification',
message
);
}
}
describe('UserService', () => {
let userRepo: UserRepository;
let emailService: EmailService;
let userService: UserService;
beforeEach(() => {
// Criacao do Stub
userRepo = {
findById: vi.fn(),
save: vi.fn(),
};
// Criacao do Mock
emailService = {
send: vi.fn(),
};
userService = new UserService(userRepo, emailService);
});
it('should send notification to user', async () => {
// Configuracao do Stub (retorna valor pre-definido)
const mockUser: User = {
id: '1',
email: 'test@example.com',
name: 'Test User',
};
vi.mocked(userRepo.findById).mockResolvedValue(mockUser);
vi.mocked(emailService.send).mockResolvedValue(true);
// Execucao
const result = await userService.notifyUser('1', 'Hello!');
// Verificacao do resultado
expect(result).toBe(true);
// Verificacao do Mock (confirma chamada)
expect(emailService.send).toHaveBeenCalledWith(
'test@example.com',
'Notification',
'Hello!'
);
expect(emailService.send).toHaveBeenCalledTimes(1);
});
it('should throw error when user not found', async () => {
// Configuracao do Stub (retorna null)
vi.mocked(userRepo.findById).mockResolvedValue(null);
// Verificacao de excecao
await expect(userService.notifyUser('999', 'Hello!'))
.rejects.toThrow('User not found');
// Confirma que emailService nao foi chamado
expect(emailService.send).not.toHaveBeenCalled();
});
});
// Exemplo de Fake (repositorio em memoria)
class FakeUserRepository implements UserRepository {
private users: Map<string, User> = new Map();
async findById(id: string): Promise<User | null> {
return this.users.get(id) ?? null;
}
async save(user: User): Promise<void> {
this.users.set(user.id, user);
}
// Metodos auxiliares para teste
seed(users: User[]): void {
users.forEach(user => this.users.set(user.id, user));
}
clear(): void {
this.users.clear();
}
}
Testes de Integracao
// Testes de integracao de API
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createServer, Server } from 'http';
import { app } from '../src/app';
describe('API Integration Tests', () => {
let server: Server;
let baseURL: string;
beforeAll(async () => {
server = createServer(app);
await new Promise<void>(resolve => {
server.listen(0, () => resolve());
});
const address = server.address();
const port = typeof address === 'object' ? address?.port : 0;
baseURL = `http://localhost:${port}`;
});
afterAll(async () => {
await new Promise<void>(resolve => server.close(() => resolve()));
});
describe('POST /api/users', () => {
it('should create a new user', async () => {
const response = await fetch(`${baseURL}/api/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Test User',
email: 'test@example.com',
}),
});
expect(response.status).toBe(201);
const data = await response.json();
expect(data).toMatchObject({
name: 'Test User',
email: 'test@example.com',
});
expect(data.id).toBeDefined();
});
it('should return 400 for invalid data', async () => {
const response = await fetch(`${baseURL}/api/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: '', // nome vazio
}),
});
expect(response.status).toBe(400);
});
});
describe('GET /api/users/:id', () => {
it('should return user by id', async () => {
// Criar usuario previamente
const createResponse = await fetch(`${baseURL}/api/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Get Test',
email: 'get@example.com',
}),
});
const created = await createResponse.json();
// Buscar usuario criado
const response = await fetch(`${baseURL}/api/users/${created.id}`);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.id).toBe(created.id);
});
it('should return 404 for non-existent user', async () => {
const response = await fetch(`${baseURL}/api/users/non-existent-id`);
expect(response.status).toBe(404);
});
});
});
TDD (Desenvolvimento Orientado a Testes)
flowchart TB
subgraph TDD["Ciclo TDD (Red-Green-Refactor)"]
Red["RED<br/>(Falha)"]
Green["GREEN<br/>(Sucesso)"]
Refactor["REFACTOR<br/>(Melhoria)"]
Red -->|"Escrever teste que falha"| Green
Green -->|"Codigo minimo para passar"| Refactor
Refactor -->|"Melhorar codigo"| Red
end
Exemplo Pratico de TDD
// Passo 1: RED - Escrever teste que falha
describe('StringCalculator', () => {
it('should return 0 for empty string', () => {
const calc = new StringCalculator();
expect(calc.add('')).toBe(0);
});
});
// Passo 2: GREEN - Codigo minimo para passar
class StringCalculator {
add(numbers: string): number {
return 0;
}
}
// Passo 3: Adicionar proximo teste (RED)
describe('StringCalculator', () => {
it('should return 0 for empty string', () => {
const calc = new StringCalculator();
expect(calc.add('')).toBe(0);
});
it('should return number for single number', () => {
const calc = new StringCalculator();
expect(calc.add('1')).toBe(1);
expect(calc.add('5')).toBe(5);
});
});
// Passo 4: GREEN - Fazer teste passar
class StringCalculator {
add(numbers: string): number {
if (numbers === '') return 0;
return parseInt(numbers, 10);
}
}
// Passo 5: Adicionar mais testes
describe('StringCalculator', () => {
// ... testes anteriores
it('should return sum for multiple numbers', () => {
const calc = new StringCalculator();
expect(calc.add('1,2')).toBe(3);
expect(calc.add('1,2,3')).toBe(6);
});
it('should handle newlines as delimiter', () => {
const calc = new StringCalculator();
expect(calc.add('1\n2,3')).toBe(6);
});
it('should support custom delimiter', () => {
const calc = new StringCalculator();
expect(calc.add('//;\n1;2')).toBe(3);
});
it('should throw for negative numbers', () => {
const calc = new StringCalculator();
expect(() => calc.add('-1,2')).toThrow('Negatives not allowed: -1');
});
});
// Implementacao final
class StringCalculator {
add(numbers: string): number {
if (numbers === '') return 0;
let delimiter = /,|\n/;
let numString = numbers;
// Processamento de delimitador customizado
if (numbers.startsWith('//')) {
const delimiterEnd = numbers.indexOf('\n');
delimiter = new RegExp(numbers.slice(2, delimiterEnd));
numString = numbers.slice(delimiterEnd + 1);
}
const nums = numString.split(delimiter).map(n => parseInt(n, 10));
// Verificacao de numeros negativos
const negatives = nums.filter(n => n < 0);
if (negatives.length > 0) {
throw new Error(`Negatives not allowed: ${negatives.join(', ')}`);
}
return nums.reduce((sum, n) => sum + n, 0);
}
}
Estrategia de Cobertura
Tipos de Cobertura
| Tipo | Descricao |
|---|---|
| Cobertura de Statements | Se cada linha foi executada |
| Cobertura de Branch | Se cada ramificacao (if/else) foi executada |
| Cobertura de Function | Se cada funcao foi chamada |
| Cobertura de Line | Se cada linha foi executada |
Metas Recomendadas:
| Alvo | Meta |
|---|---|
| Geral | 80% ou mais |
| Logica de negocio importante | 90% ou mais |
| Funcoes utilitarias | 100% |
| Componentes UI | 70% ou mais |
Atencao: Cobertura e apenas um dos indicadores de qualidade. Mesmo com 100%, bugs podem existir.
Configuracao de Cobertura Vitest
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'**/*.d.ts',
'**/*.config.*',
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
});
Estrategia de Testes Frontend
// Testes de componentes React
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
// Componente a ser testado
function LoginForm({ onSubmit }: { onSubmit: (data: { email: string; password: string }) => void }) {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
onSubmit({
email: formData.get('email') as string,
password: formData.get('password') as string,
});
};
return (
<form onSubmit={handleSubmit}>
<label>
Email
<input type="email" name="email" required />
</label>
<label>
Password
<input type="password" name="password" required />
</label>
<button type="submit">Login</button>
</form>
);
}
describe('LoginForm', () => {
it('should render form fields', () => {
render(<LoginForm onSubmit={() => {}} />);
expect(screen.getByLabelText(/email/i)).toBeInTheDocument();
expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument();
});
it('should call onSubmit with form data', async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
await user.type(screen.getByLabelText(/email/i), 'test@example.com');
await user.type(screen.getByLabelText(/password/i), 'password123');
await user.click(screen.getByRole('button', { name: /login/i }));
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
});
Melhores Praticas
| Categoria | Melhor Pratica |
|---|---|
| Nomenclatura | Nome do teste deve ser “should + comportamento esperado” |
| Estrutura | Usar padrao AAA (Arrange-Act-Assert) |
| Independencia | Eliminar dependencias entre testes |
| Velocidade | Manter testes unitarios rapidos |
| Confiabilidade | Eliminar testes flakey |
| Manutenibilidade | Gerenciar codigo de teste como codigo de producao |