Kaique Mitsuo Silva Yamamoto
Arquitetura softwareFrontend webTypeScriptTestes

Unit & Integration Tests

Vitest, vi mocking, testes de hooks React, services e controllers — full-stack com exemplos práticos.

Unit & Integration Tests com Vitest

Configuração

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  test: {
    environment: 'node', // ou 'jsdom' para React
    globals: true,
    setupFiles: ['./src/__tests__/setup.ts'],
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

Unit Tests — Funções puras

Validação

// validators/email.validator.ts
export function validateEmail(email: unknown): Result<string, string> {
  if (typeof email !== 'string') return { ok: false, error: 'Email deve ser string' };
  if (!email.includes('@')) return { ok: false, error: 'Email inválido' };
  if (email.length < 5) return { ok: false, error: 'Email muito curto' };
  return { ok: true, value: email };
}
// validators/__tests__/email.validator.test.ts
import { describe, it, expect } from 'vitest';
import { validateEmail } from '../email.validator';

describe('validateEmail', () => {
  it('aceita email válido', () => {
    const result = validateEmail('[email protected]');
    expect(result.ok).toBe(true);
    if (result.ok) expect(result.value).toBe('[email protected]');
  });

  it('rejeita email sem @', () => {
    const result = validateEmail('userexample.com');
    expect(result.ok).toBe(false);
    if (!result.ok) expect(result.error).toBe('Email inválido');
  });

  it('rejeita valor não-string', () => {
    expect(validateEmail(123).ok).toBe(false);
    expect(validateEmail(null).ok).toBe(false);
    expect(validateEmail(undefined).ok).toBe(false);
  });

  it('rejeita email muito curto', () => {
    expect(validateEmail('a@b').ok).toBe(false);
  });
});

Transformação de dados

// utils/format-currency.ts
export function formatCurrencyBRL(amount: number): string {
  return new Intl.NumberFormat('pt-BR', {
    style: 'currency',
    currency: 'BRL',
  }).format(amount);
}
// utils/__tests__/format-currency.test.ts
import { describe, it, expect } from 'vitest';
import { formatCurrencyBRL } from '../format-currency';

describe('formatCurrencyBRL', () => {
  it('formata valores inteiros', () => {
    expect(formatCurrencyBRL(100)).toBe('R$\u00a0100,00');
    expect(formatCurrencyBRL(0)).toBe('R$\u00a00,00');
  });

  it('formata valores decimais', () => {
    expect(formatCurrencyBRL(29.9)).toBe('R$\u00a029,90');
    expect(formatCurrencyBRL(1234.56)).toBe('R$\u00a01.234,56');
  });

  it('formata valores negativos', () => {
    expect(formatCurrencyBRL(-50)).toBe('-R$\u00a050,00');
  });
});

Unit Tests — Backend (Services com Mocks)

Mocking com vi

// services/order.service.ts
export class OrderService {
  constructor(
    private repo: OrderRepository,
    private payment: PaymentService,
    private notifier: NotificationService,
  ) {}

  async create(dto: CreateOrderDto): Promise<Result<Order, string>> {
    const paymentResult = await this.payment.charge(dto.paymentMethod, dto.total);
    if (!paymentResult.success) {
      return { ok: false, error: 'Pagamento recusado' };
    }

    const order = await this.repo.create({ ...dto, paymentId: paymentResult.id });
    await this.notifier.sendOrderConfirmation(dto.userId, order.id);

    return { ok: true, value: order };
  }
}
// services/__tests__/order.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { OrderService } from '../order.service';

// Cria mocks tipados
const mockRepo = {
  create: vi.fn(),
  findById: vi.fn(),
};

const mockPayment = {
  charge: vi.fn(),
  refund: vi.fn(),
};

const mockNotifier = {
  sendOrderConfirmation: vi.fn(),
};

let service: OrderService;

beforeEach(() => {
  vi.clearAllMocks(); // limpa estado entre testes
  service = new OrderService(mockRepo, mockPayment, mockNotifier);
});

describe('OrderService.create', () => {
  const dto = { userId: '1', items: [{ productId: 'p1', qty: 1 }], total: 100, paymentMethod: 'pix' };

  it('cria pedido quando pagamento é aprovado', async () => {
    mockPayment.charge.mockResolvedValue({ success: true, id: 'pay-123' });
    mockRepo.create.mockResolvedValue({ id: 'order-1', ...dto });

    const result = await service.create(dto);

    expect(result.ok).toBe(true);
    expect(mockRepo.create).toHaveBeenCalledOnce();
    expect(mockNotifier.sendOrderConfirmation).toHaveBeenCalledWith('1', 'order-1');
  });

  it('falha quando pagamento é recusado', async () => {
    mockPayment.charge.mockResolvedValue({ success: false });

    const result = await service.create(dto);

    expect(result.ok).toBe(false);
    if (!result.ok) expect(result.error).toBe('Pagamento recusado');
    expect(mockRepo.create).not.toHaveBeenCalled(); // não criou pedido
  });
});

Unit Tests — Frontend (Componentes React)

Componente com Testing Library

// components/__tests__/UserCard.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserCard } from '../UserCard';

const mockUser = {
  id: '1',
  name: 'Kaique Yamamoto',
  email: '[email protected]',
  createdAt: '2025-01-15T10:00:00Z',
};

describe('UserCard', () => {
  it('renderiza nome e email do usuário', () => {
    render(<UserCard user={mockUser} onSelect={() => {}} />);

    expect(screen.getByText('Kaique Yamamoto')).toBeInTheDocument();
    expect(screen.getByText('[email protected]')).toBeInTheDocument();
  });

  it('chama onSelect ao clicar', async () => {
    const onSelect = vi.fn();
    render(<UserCard user={mockUser} onSelect={onSelect} />);

    await userEvent.click(screen.getByRole('button'));

    expect(onSelect).toHaveBeenCalledWith('1');
  });

  it('mostra iniciais do nome', () => {
    render(<UserCard user={mockUser} onSelect={() => {}} />);
    expect(screen.getByText('KY')).toBeInTheDocument();
  });
});

Integration Tests — Backend (Service + Repository)

// __tests__/integration/user.service.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { MongoClient } from 'mongodb';
import { UserService } from '@/services/user.service';

let mongo: MongoMemoryServer;
let client: MongoClient;
let service: UserService;

beforeAll(async () => {
  mongo = await MongoMemoryServer.create();
  client = new MongoClient(mongo.getUri());
  await client.connect();
  const db = client.db('test');
  service = new UserService(db.collection('users'));
});

afterAll(async () => {
  await client.close();
  await mongo.stop();
});

describe('UserService (integration)', () => {
  it('cria e recupera usuário', async () => {
    const user = await service.create({ name: 'Kaique', email: '[email protected]' });
    expect(user.id).toBeDefined();

    const found = await service.findById(user.id);
    expect(found?.name).toBe('Kaique');
  });

  it('rejeita email duplicado', async () => {
    await service.create({ name: 'Test', email: '[email protected]' });
    const result = await service.create({ name: 'Test2', email: '[email protected]' });

    expect(result.ok).toBe(false);
  });
});

Referências

On this page