Padroes de Design de Estrategia de Testes - Da Piramide de Testes ao Design Pratico

Avancado | 2025.12.02

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:

NivelVelocidade de ExecucaoConfiabilidadeCusto
E2ELentoBaixaAlto
IntegracaoModeradaModeradaModerado
UnitarioRapidoAltaBaixo

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

TipoDescricao
Cobertura de StatementsSe cada linha foi executada
Cobertura de BranchSe cada ramificacao (if/else) foi executada
Cobertura de FunctionSe cada funcao foi chamada
Cobertura de LineSe cada linha foi executada

Metas Recomendadas:

AlvoMeta
Geral80% ou mais
Logica de negocio importante90% ou mais
Funcoes utilitarias100%
Componentes UI70% 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

CategoriaMelhor Pratica
NomenclaturaNome do teste deve ser “should + comportamento esperado”
EstruturaUsar padrao AAA (Arrange-Act-Assert)
IndependenciaEliminar dependencias entre testes
VelocidadeManter testes unitarios rapidos
ConfiabilidadeEliminar testes flakey
ManutenibilidadeGerenciar codigo de teste como codigo de producao
← Voltar para a lista