Pular para o conteúdo
TypeScript

Testar TypeScript com Jest:

Configure Jest com TypeScript do jeito certo. De ts-jest e jest.config.ts até mocks tipados e testes assíncronos, tudo com exemplos práticos.

Por que isso é importante

Testar TypeScript com Jest:. Configure Jest com TypeScript do jeito certo. De ts-jest e jest.config.ts até mocks tipados e testes assíncronos, tudo com exemplos práticos.

Por Que Testar TypeScript com Jest

Jest é o framework de testes mais popular do ecossistema JavaScript. Roda testes em paralelo, tem mocks embutidos, cobertura de código nativa e uma API que qualquer dev aprende em minutos. O problema: ele foi feito pra JavaScript.

TypeScript precisa ser transpilado antes de executar. O Jest não sabe ler arquivos .ts nativamente. É aí que entra o ts-jest — um transformer que compila TypeScript on-the-fly durante a execução dos testes. Sem step de build separado.

A vantagem de testar com TypeScript é que seus mocks, fixtures e assertions também são tipados. Se uma função muda a assinatura, o teste quebra na compilação, não em runtime. Isso pega erros que em JavaScript só apareceriam quando o CI rodasse.

Como Configurar Jest com TypeScript Passo a Passo

Setup completo do zero. Cada passo te leva de um projeto sem testes a uma suíte rodando com tipagem completa.

  1. Passo 1 - Instale as dependências: npm install -D jest ts-jest @types/jest. O ts-jest faz a transpilação, e @types/jest dá as tipagens pra describe, it, expect e todos os matchers.
  2. Passo 2 - Crie o jest.config.ts: Use npx ts-jest config:init pra gerar a config base, ou crie manualmente com preset: 'ts-jest' e testEnvironment: 'node'.
  3. Passo 3 - Configure os scripts no package.json: Adicione "test": "jest", "test:watch": "jest --watch" e "test:coverage": "jest --coverage" nos scripts.
  4. Passo 4 - Crie a pasta de testes: Use __tests__/ na raiz ou coloque arquivos .test.ts junto dos módulos. O Jest detecta ambos os padrões automaticamente.
  5. Passo 5 - Escreva o primeiro teste: Crie um arquivo .test.ts, importe a função a ser testada e use describe/it/expect. Rode npm test e veja o resultado.
  6. Passo 6 - Configure path aliases no Jest: Se seu tsconfig tem paths, espelhe no moduleNameMapper do jest.config pra evitar erros de resolução de módulo.

Configuração Completa do jest.config.ts

Vamos à configuração que funciona em 95% dos projetos TypeScript.

// jest.config.ts
import type { Config } from 'jest';

const config: Config = {
  // ts-jest transpila TypeScript on-the-fly
  preset: 'ts-jest',

  // 'node' pra backend, 'jsdom' pra frontend
  testEnvironment: 'node',

  // Onde ficam os testes
  roots: ['<rootDir>/src'],

  // Padrões de arquivo de teste
  testMatch: [
    '**/__tests__/**/*.test.ts',
    '**/*.spec.ts',
  ],

  // Path aliases (espelhar tsconfig.json)
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },

  // Cobertura de código
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/**/index.ts',
  ],

  // Limites de cobertura
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

export default config;

O coverageThreshold é opcional, mas recomendado. Ele faz o Jest falhar se a cobertura cair abaixo de 80%. Funciona como uma trava de segurança no CI: ninguém merga código que reduz a cobertura.

Escrevendo Testes Tipados

A grande vantagem de testar com TypeScript é ter tipagem nos testes. Veja como isso funciona na prática.

Teste Básico com Tipagem

// src/utils/math.ts
export function soma(a: number, b: number): number {
  return a + b;
}

export function divide(a: number, b: number): number {
  if (b === 0) throw new Error('Divisão por zero');
  return a / b;
}

// src/utils/__tests__/math.test.ts
import { soma, divide } from '../math';

describe('math utils', () => {
  describe('soma', () => {
    it('soma dois números positivos', () => {
      const resultado: number = soma(2, 3);
      expect(resultado).toBe(5);
    });

    it('soma números negativos', () => {
      expect(soma(-1, -2)).toBe(-3);
    });

    it('soma com zero', () => {
      expect(soma(5, 0)).toBe(5);
    });
  });

  describe('divide', () => {
    it('divide corretamente', () => {
      expect(divide(10, 2)).toBe(5);
    });

    it('lança erro ao dividir por zero', () => {
      expect(() => divide(10, 0)).toThrow('Divisão por zero');
    });
  });
});

Testando Interfaces e Tipos Complexos

// src/models/user.ts
export interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

export function createUser(data: Omit<User, 'id'>): User {
  return {
    id: crypto.randomUUID(),
    ...data,
  };
}

export function isAdmin(user: User): boolean {
  return user.role === 'admin';
}

// src/models/__tests__/user.test.ts
import { User, createUser, isAdmin } from '../user';

describe('User model', () => {
  const mockUserData: Omit<User, 'id'> = {
    name: 'Maria Silva',
    email: 'maria@email.com',
    role: 'admin',
  };

  it('cria usuário com id gerado', () => {
    const user = createUser(mockUserData);

    expect(user.id).toBeDefined();
    expect(user.name).toBe('Maria Silva');
    expect(user.email).toBe('maria@email.com');
    expect(user.role).toBe('admin');
  });

  it('identifica admin corretamente', () => {
    const admin = createUser({ ...mockUserData, role: 'admin' });
    const regular = createUser({ ...mockUserData, role: 'user' });

    expect(isAdmin(admin)).toBe(true);
    expect(isAdmin(regular)).toBe(false);
  });
});

Repare como os tipos guiam os testes. Se createUser mudar a interface, os testes quebram na compilação, não na execução. Esse feedback rápido é o que faz TypeScript + Jest ser tão produtivo.

Mocks Tipados no Jest

Mocks são a parte mais confusa de testar com TypeScript. O Jest tem jest.fn() e jest.mock(), mas sem tipagem eles perdem toda a segurança. Veja como tipar mocks corretamente.

// src/services/email.ts
export interface EmailService {
  send(to: string, subject: string, body: string): Promise<boolean>;
}

// src/services/notification.ts
import { EmailService } from './email';

export class NotificationService {
  constructor(private emailService: EmailService) {}

  async notifyUser(email: string, message: string): Promise<boolean> {
    return this.emailService.send(
      email,
      'Nova Notificação',
      message
    );
  }
}

// src/services/__tests__/notification.test.ts
import { NotificationService } from '../notification';
import { EmailService } from '../email';

describe('NotificationService', () => {
  // Mock tipado da interface EmailService
  const mockEmailService: jest.Mocked<EmailService> = {
    send: jest.fn(),
  };

  const service = new NotificationService(mockEmailService);

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('envia email com dados corretos', async () => {
    mockEmailService.send.mockResolvedValue(true);

    const result = await service.notifyUser(
      'user@email.com',
      'Olá!'
    );

    expect(result).toBe(true);
    expect(mockEmailService.send).toHaveBeenCalledWith(
      'user@email.com',
      'Nova Notificação',
      'Olá!'
    );
  });

  it('retorna false quando email falha', async () => {
    mockEmailService.send.mockResolvedValue(false);

    const result = await service.notifyUser(
      'user@email.com',
      'Teste'
    );

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

O jest.Mocked é a chave. Ele transforma todos os métodos da interface em jest.Mock tipados. Quando você chama mockResolvedValue, o TypeScript garante que o valor de retorno bate com o tipo original. Se send retorna Promise, o mock também precisa retornar boolean.

Testando Funções Assíncronas

A maioria das funções reais é assíncrona: chamadas de API, queries no banco, leitura de arquivos. Testar async/await com Jest e TypeScript exige atenção com tipos e tratamento de erro.

// src/api/users.ts
import { User } from '@/models/user';

export async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);

  if (!response.ok) {
    throw new Error(`Usuário ${id} não encontrado`);
  }

  return response.json();
}

export async function fetchUsers(): Promise<User[]> {
  const response = await fetch('/api/users');
  return response.json();
}

// src/api/__tests__/users.test.ts
import { fetchUser, fetchUsers } from '../users';

// Mock global do fetch
global.fetch = jest.fn();
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;

describe('users API', () => {
  beforeEach(() => {
    mockFetch.mockReset();
  });

  describe('fetchUser', () => {
    it('retorna usuário quando API responde 200', async () => {
      const mockUser = {
        id: '1',
        name: 'João',
        email: 'joao@email.com',
        role: 'user' as const,
      };

      mockFetch.mockResolvedValue({
        ok: true,
        json: async () => mockUser,
      } as Response);

      const user = await fetchUser('1');

      expect(user).toEqual(mockUser);
      expect(mockFetch).toHaveBeenCalledWith('/api/users/1');
    });

    it('lança erro quando API responde 404', async () => {
      mockFetch.mockResolvedValue({
        ok: false,
        status: 404,
      } as Response);

      await expect(fetchUser('999'))
        .rejects
        .toThrow('Usuário 999 não encontrado');
    });
  });

  describe('fetchUsers', () => {
    it('retorna lista de usuários', async () => {
      const mockUsers = [
        { id: '1', name: 'Ana', email: 'ana@email.com', role: 'admin' },
        { id: '2', name: 'Pedro', email: 'pedro@email.com', role: 'user' },
      ];

      mockFetch.mockResolvedValue({
        ok: true,
        json: async () => mockUsers,
      } as Response);

      const users = await fetchUsers();

      expect(users).toHaveLength(2);
      expect(users[0].name).toBe('Ana');
    });
  });
});

O padrão expect(...).rejects.toThrow() testa erros em funções async sem precisar de try/catch no teste. Limpo e direto. Use mockResolvedValue pra simular respostas de sucesso e mockRejectedValue pra simular falhas de rede.

Utilities de Teste Tipadas

Conforme o projeto cresce, você repete código nos testes: fixtures, factories, helpers. Criar utilities tipadas centraliza essa lógica e garante consistência.

// src/__tests__/helpers/factories.ts
import { User } from '@/models/user';

// Factory com valores padrão e override tipado
export function createMockUser(
  overrides: Partial<User> = {}
): User {
  return {
    id: 'test-id-123',
    name: 'Test User',
    email: 'test@email.com',
    role: 'user',
    ...overrides,
  };
}

// Factory pra listas
export function createMockUsers(count: number): User[] {
  return Array.from({ length: count }, (_, i) =>
    createMockUser({
      id: `test-id-${i}`,
      name: `User ${i}`,
      email: `user${i}@email.com`,
    })
  );
}

// Uso nos testes:
import { createMockUser, createMockUsers } from '../helpers/factories';

it('processa admin corretamente', () => {
  // Override só o que importa pro teste
  const admin = createMockUser({ role: 'admin' });
  expect(isAdmin(admin)).toBe(true);
});

it('lista 10 usuários', () => {
  const users = createMockUsers(10);
  expect(users).toHaveLength(10);
  expect(users[0].email).toBe('user0@email.com');
});

Factories com Partial são poderosas. Nos testes, você só passa os campos relevantes pro cenário sendo testado. Os outros ficam com valores padrão sensatos. Menos código repetido, mais clareza no que cada teste valida.

Erros Comuns ao Testar TypeScript com Jest

Armadilhas nos testes

Não instalar @types/jest: sem esse pacote, describe, it e expect aparecem como undefined. O TypeScript não sabe que esses globais existem sem os types.

Usar jest.fn() sem tipar: jest.fn() retorna jest.Mock. Você perde toda a segurança. Use jest.fn() ou jest.Mocked pra manter tipos.

Esquecer de limpar mocks entre testes: sem beforeEach(() => jest.clearAllMocks()), os mocks acumulam chamadas. Um teste interfere no outro e os resultados ficam inconsistentes.

Mock de módulo com tipagem errada: jest.mock('./modulo') faz o mock funcionar, mas os tipos do import original continuam valendo. Cast o import com as jest.Mocked pra ter os métodos de mock disponíveis.

Não tratar rejeições de Promise: se uma função async lança erro e o teste não trata com rejects.toThrow(), o Jest marca como falha por timeout em vez de mostrar o erro real. Sempre teste os cenários de erro.

Checklist de Testes TypeScript com Jest

  • jest, ts-jest e @types/jest instalados como devDependencies
  • jest.config.ts criado com preset ts-jest
  • Scripts test, test:watch e test:coverage no package.json
  • Path aliases espelhados no moduleNameMapper
  • Mocks tipados com jest.Mocked
  • beforeEach com jest.clearAllMocks() em cada describe
  • Funções async testadas com expect.rejects pra erros
  • Factories tipadas criadas pra fixtures reutilizáveis
  • Cobertura acima de 80% com coverageThreshold configurado

Testes Profissionais em TypeScript

Testar código TypeScript com Jest é uma skill que separa devs júniors de profissionais. No CrazyStack, cada módulo tem testes tipados — mocks, factories, integração. Você aprende a construir uma suíte de testes que dá confiança real pra refatorar e fazer deploy.

Código testado é código confiável. Se quer evoluir como dev, testes são o caminho mais curto.

Perguntas frequentes

Por Que Testar TypeScript com Jest

Jest é o framework de testes mais popular do ecossistema JavaScript. Roda testes em paralelo, tem mocks embutidos, cobertura de código nativa e uma API que qualquer dev aprende em minutos. O problema: ele foi feito pra JavaScript. TypeScript precisa ser transpilado antes de executar. O Jest não sabe ler arquivos .ts nativamente. É aí que entra o ts-jest — um transformer que compila TypeScript on-the-fly durante a execução dos testes. Sem step de build separado. A vantagem de testar com TypeScript é que seus mocks, fixtures e assertions também são tipados. Se uma função muda a assinatura, o teste quebra na compilação, não em runtime. Isso pega erros que em JavaScript só apareceriam quando o CI rodasse.

Como Configurar Jest com TypeScript Passo a Passo

Setup completo do zero. Cada passo te leva de um projeto sem testes a uma suíte rodando com tipagem completa.