Português

Domine padrões de teste avançados do Jest para construir software mais confiável e de fácil manutenção. Explore técnicas como mocking, testes de snapshot e mais.

Jest: Padrões de Teste Avançados para um Software Robusto

No cenário atual de desenvolvimento de software acelerado, garantir a confiabilidade e a estabilidade da sua base de código é primordial. Embora o Jest tenha se tornado um padrão de fato para testes em JavaScript, ir além dos testes de unidade básicos desbloqueia um novo nível de confiança em suas aplicações. Este post aprofunda-se em padrões de teste avançados do Jest que são essenciais para construir software robusto, atendendo a um público global de desenvolvedores.

Por Que Ir Além dos Testes de Unidade Básicos?

Testes de unidade básicos verificam componentes individuais de forma isolada. No entanto, aplicações do mundo real são sistemas complexos onde os componentes interagem. Padrões de teste avançados abordam essas complexidades, permitindo-nos:

Dominando Mocking e Spies

O "mocking" (simulação) é crucial para isolar a unidade em teste, substituindo suas dependências por substitutos controlados. O Jest fornece ferramentas poderosas para isso:

jest.fn(): A Base de Mocks e Spies

jest.fn() cria uma função mock. Você pode rastrear suas chamadas, argumentos e valores de retorno. Este é o bloco de construção para estratégias de mocking mais sofisticadas.

Exemplo: Rastreando Chamadas de Função

// component.js
export const fetchData = () => {
  // Simula uma chamada de API
  return Promise.resolve({ data: 'some data' });
};

export const processData = async (fetcher) => {
  const result = await fetcher();
  return `Processed: ${result.data}`;
};

// component.test.js
import { processData } from './component';

test('should process data correctly', async () => {
  const mockFetcher = jest.fn().mockResolvedValue({ data: 'mocked data' });
  const result = await processData(mockFetcher);
  expect(result).toBe('Processed: mocked data');
  expect(mockFetcher).toHaveBeenCalledTimes(1);
  expect(mockFetcher).toHaveBeenCalledWith();
});

jest.spyOn(): Observando Sem Substituir

jest.spyOn() permite observar chamadas a um método em um objeto existente sem necessariamente substituir sua implementação. Você também pode simular a implementação, se necessário.

Exemplo: Espionando um Método de Módulo

// logger.js
export const logInfo = (message) => {
  console.log(`INFO: ${message}`);
};

// service.js
import { logInfo } from './logger';

export const performTask = (taskName) => {
  logInfo(`Starting task: ${taskName}`);
  // ... lógica da tarefa ...
  logInfo(`Task ${taskName} completed.`);
};

// service.test.js
import { performTask } from './service';
import * as logger from './logger';

test('should log task start and completion', () => {
  const logSpy = jest.spyOn(logger, 'logInfo');

  performTask('backup');

  expect(logSpy).toHaveBeenCalledTimes(2);
  expect(logSpy).toHaveBeenCalledWith('Starting task: backup');
  expect(logSpy).toHaveBeenCalledWith('Task backup completed.');

  logSpy.mockRestore(); // Importante para restaurar a implementação original
});

Simulando (Mocking) Importações de Módulos

As capacidades de simulação de módulos do Jest são extensas. Você pode simular módulos inteiros ou exportações específicas.

Exemplo: Simulando um Cliente de API Externo

// api.js
import axios from 'axios';

export const getUser = async (userId) => {
  const response = await axios.get(`/api/users/${userId}`);
  return response.data;
};

// user-service.js
import { getUser } from './api';

export const getUserFullName = async (userId) => {
  const user = await getUser(userId);
  return `${user.firstName} ${user.lastName}`;
};

// user-service.test.js
import { getUserFullName } from './user-service';
import * as api from './api';

// Simula todo o módulo da api
jest.mock('./api');

test('should get full name using mocked API', async () => {
  // Simula a função específica do módulo simulado
  api.getUser.mockResolvedValue({ id: 1, firstName: 'Ada', lastName: 'Lovelace' });

  const fullName = await getUserFullName(1);

  expect(fullName).toBe('Ada Lovelace');
  expect(api.getUser).toHaveBeenCalledTimes(1);
  expect(api.getUser).toHaveBeenCalledWith(1);
});

Mocking Automático vs. Mocking Manual

O Jest simula automaticamente os módulos do Node.js. Para módulos ES ou módulos personalizados, você pode precisar de jest.mock(). Para mais controle, você pode criar diretórios __mocks__.

Implementações de Mock

Você pode fornecer implementações personalizadas para seus mocks.

Exemplo: Simulando com uma Implementação Personalizada

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// calculator.js
import { add, subtract } from './math';

export const calculate = (operation, a, b) => {
  if (operation === 'add') {
    return add(a, b);
  } else if (operation === 'subtract') {
    return subtract(a, b);
  }
  return null;
};

// calculator.test.js
import { calculate } from './calculator';
import * as math from './math';

// Simula todo o módulo de matemática
jest.mock('./math');

test('should perform addition using mocked math add', () => {
  // Fornece uma implementação de mock para a função 'add'
  math.add.mockImplementation((a, b) => a + b + 10); // Adiciona 10 ao resultado
  math.subtract.mockReturnValue(5); // Simula também a subtração

  const result = calculate('add', 5, 3);

  expect(math.add).toHaveBeenCalledWith(5, 3);
  expect(result).toBe(18); // 5 + 3 + 10

  const subResult = calculate('subtract', 10, 2);
  expect(math.subtract).toHaveBeenCalledWith(10, 2);
  expect(subResult).toBe(5);
});

Teste de Snapshot: Preservando UI e Configuração

Testes de snapshot são um recurso poderoso para capturar a saída de seus componentes ou configurações. Eles são particularmente úteis para testes de UI ou para verificar estruturas de dados complexas.

Como o Teste de Snapshot Funciona

Na primeira vez que um teste de snapshot é executado, o Jest cria um arquivo .snap contendo uma representação serializada do valor testado. Em execuções subsequentes, o Jest compara a saída atual com o snapshot armazenado. Se eles diferirem, o teste falha, alertando sobre alterações não intencionais. Isso é inestimável para detectar regressões em componentes de UI em diferentes regiões ou localidades.

Exemplo: Criando um Snapshot de um Componente React

Assumindo que você tem um componente React:

// UserProfile.js
import React from 'react';

const UserProfile = ({ name, email, isActive }) => (
  <div>
    <h2>{name}</h2>
    <p><strong>Email:</strong> {email}</p>
    <p><strong>Status:</strong> {isActive ? 'Active' : 'Inactive'}</p>
  </div>
);

export default UserProfile;

// UserProfile.test.js
import React from 'react';
import renderer from 'react-test-renderer'; // Para snapshots de componentes React
import UserProfile from './UserProfile';

test('renders UserProfile correctly', () => {
  const user = {
    name: 'Jane Doe',
    email: 'jane.doe@example.com',
    isActive: true,
  };
  const component = renderer.create(
    <UserProfile {...user} />
  );
  const tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});

test('renders inactive UserProfile correctly', () => {
  const user = {
    name: 'John Smith',
    email: 'john.smith@example.com',
    isActive: false,
  };
  const component = renderer.create(
    <UserProfile {...user} />
  );
  const tree = component.toJSON();
  expect(tree).toMatchSnapshot('inactive user profile'); // Snapshot nomeado
});

Após executar os testes, o Jest criará um arquivo UserProfile.test.js.snap. Quando você atualizar o componente, precisará revisar as alterações e, potencialmente, atualizar o snapshot executando o Jest com a flag --updateSnapshot ou -u.

Melhores Práticas para Testes de Snapshot

Matchers Personalizados: Melhorando a Legibilidade dos Testes

Os matchers integrados do Jest são extensos, mas às vezes você precisa validar condições específicas não cobertas. Matchers personalizados permitem que você crie sua própria lógica de asserção, tornando seus testes mais expressivos e legíveis.

Criando Matchers Personalizados

Você pode estender o objeto expect do Jest com seus próprios matchers.

Exemplo: Verificando um Formato de E-mail Válido

No seu arquivo de configuração do Jest (ex: jest.setup.js, configurado em jest.config.js):

// jest.setup.js

expect.extend({
  toBeValidEmail(received) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const pass = emailRegex.test(received);

    if (pass) {
      return {
        message: () => `esperava que ${received} não fosse um e-mail válido`,
        pass: true,
      };
    } else {
      return {
        message: () => `esperava que ${received} fosse um e-mail válido`,
        pass: false,
      };
    }
  },
});

// In your jest.config.js
// module.exports = { setupFilesAfterEnv: ['/jest.setup.js'] };

No seu arquivo de teste:

// validation.test.js

test('should validate email formats', () => {
  expect('test@example.com').toBeValidEmail();
  expect('invalid-email').not.toBeValidEmail();
  expect('another.test@sub.domain.co.uk').toBeValidEmail();
});

Benefícios dos Matchers Personalizados

Testando Operações Assíncronas

JavaScript é fortemente assíncrono. O Jest fornece um excelente suporte para testar promises e async/await.

Usando async/await

Esta é a maneira moderna e mais legível de testar código assíncrono.

Exemplo: Testando uma Função Assíncrona

// dataService.js
export const fetchUserData = async (userId) => {
  // Simula a busca de dados após um atraso
  await new Promise(resolve => setTimeout(resolve, 50));
  if (userId === 1) {
    return { id: 1, name: 'Alice' };
  } else {
    throw new Error('User not found');
  }
};

// dataService.test.js
import { fetchUserData } from './dataService';

test('fetches user data correctly', async () => {
  const user = await fetchUserData(1);
  expect(user).toEqual({ id: 1, name: 'Alice' });
});

test('throws error for non-existent user', async () => {
  await expect(fetchUserData(2)).rejects.toThrow('User not found');
});

Usando .resolves e .rejects

Esses matchers simplificam o teste de resoluções e rejeições de promises.

Exemplo: Usando .resolves/.rejects

// dataService.test.js (continued)

test('fetches user data with .resolves', () => {
  return expect(fetchUserData(1)).resolves.toEqual({ id: 1, name: 'Alice' });
});

test('throws error for non-existent user with .rejects', () => {
  return expect(fetchUserData(2)).rejects.toThrow('User not found');
});

Lidando com Timers

Para funções que usam setTimeout ou setInterval, o Jest fornece controle de timers.

Exemplo: Controlando Timers

// delayedGreeter.js
export const greetAfterDelay = (name, callback) => {
  setTimeout(() => {
    callback(`Hello, ${name}!`);
  }, 1000);
};

// delayedGreeter.test.js
import { greetAfterDelay } from './delayedGreeter';

jest.useFakeTimers(); // Habilita timers falsos

test('greets after delay', () => {
  const mockCallback = jest.fn();
  greetAfterDelay('World', mockCallback);

  // Avança os timers em 1000ms
  jest.advanceTimersByTime(1000);

  expect(mockCallback).toHaveBeenCalledTimes(1);
  expect(mockCallback).toHaveBeenCalledWith('Hello, World!');
});

// Restaura os timers reais se necessário em outro lugar
jest.useRealTimers();

Organização e Estrutura dos Testes

À medida que sua suíte de testes cresce, a organização se torna crítica para a manutenibilidade.

Blocos Describe e Blocos It

Use describe para agrupar testes relacionados e it (ou test) para casos de teste individuais. Essa estrutura espelha a modularidade da aplicação.

Exemplo: Testes Estruturados

describe('User Authentication Service', () => {
  let authService;

  beforeEach(() => {
    // Configura mocks ou instâncias de serviço antes de cada teste
    authService = require('./authService');
    jest.spyOn(authService, 'login').mockImplementation(() => Promise.resolve({ token: 'fake_token' }));
  });

  afterEach(() => {
    // Limpa os mocks
    jest.restoreAllMocks();
  });

  describe('login functionality', () => {
    it('should successfully log in a user with valid credentials', async () => {
      const result = await authService.login('user@example.com', 'password123');
      expect(result.token).toBeDefined();
      // ... mais asserções ...
    });

    it('should fail login with invalid credentials', async () => {
      jest.spyOn(authService, 'login').mockRejectedValue(new Error('Invalid credentials'));
      await expect(authService.login('user@example.com', 'wrong_password')).rejects.toThrow('Invalid credentials');
    });
  });

  describe('logout functionality', () => {
    it('should clear user session', async () => {
      // Testa a lógica de logout...
    });
  });
});

Hooks de Configuração e Desmontagem (Setup e Teardown)

Esses hooks são essenciais para configurar dados de mock, conexões de banco de dados ou limpar recursos entre os testes.

Testando para Públicos Globais

Ao desenvolver aplicações para um público global, as considerações de teste se expandem:

Internacionalização (i18n) e Localização (l10n)

Garanta que sua UI e mensagens se adaptem corretamente a diferentes idiomas e formatos regionais.

Exemplo: Testando formatação de data localizada

// dateUtils.js
export const formatLocalizedDate = (date, locale) => {
  return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', day: 'numeric' }).format(date);
};

// dateUtils.test.js
import { formatLocalizedDate } from './dateUtils';

test('formats date correctly for US locale', () => {
  const date = new Date(2023, 10, 15); // 15 de novembro de 2023
  expect(formatLocalizedDate(date, 'en-US')).toBe('11/15/2023');
});

test('formats date correctly for German locale', () => {
  const date = new Date(2023, 10, 15);
  expect(formatLocalizedDate(date, 'de-DE')).toBe('15.11.2023');
});

Consciência de Fuso Horário

Teste como sua aplicação lida com diferentes fusos horários, especialmente para recursos como agendamento ou atualizações em tempo real. Simular o relógio do sistema ou usar bibliotecas que abstraem fusos horários pode ser benéfico.

Nuances Culturais nos Dados

Considere como números, moedas e outras representações de dados podem ser percebidas ou esperadas de forma diferente entre culturas. Matchers personalizados podem ser particularmente úteis aqui.

Técnicas e Estratégias Avançadas

Desenvolvimento Orientado a Testes (TDD) e Desenvolvimento Orientado a Comportamento (BDD)

O Jest se alinha bem com as metodologias TDD (Vermelho-Verde-Refatorar) e BDD (Dado-Quando-Então). Escreva testes que descrevem o comportamento desejado antes de escrever o código de implementação. Isso garante que o código seja escrito com a testabilidade em mente desde o início.

Teste de Integração com Jest

Embora o Jest se destaque em testes de unidade, ele também pode ser usado para testes de integração. Simular menos dependências ou usar ferramentas como a opção runInBand do Jest pode ajudar.

Exemplo: Testando Interação com API (simplificado)

// apiService.js
import axios from 'axios';

const API_BASE_URL = 'https://api.example.com';

export const createProduct = async (productData) => {
  const response = await axios.post(`${API_BASE_URL}/products`, productData);
  return response.data;
};

// apiService.test.js (Teste de Integração)
import axios from 'axios';
import { createProduct } from './apiService';

// Simula o axios para testes de integração para controlar a camada de rede
jest.mock('axios');

test('creates a product via API', async () => {
  const mockProduct = { id: 1, name: 'Gadget' };
  const responseData = { success: true, product: mockProduct };

  axios.post.mockResolvedValue({
    data: responseData,
    status: 201,
    headers: { 'content-type': 'application/json' },
  });

  const newProductData = { name: 'Gadget', price: 99.99 };
  const result = await createProduct(newProductData);

  expect(axios.post).toHaveBeenCalledWith(`${process.env.API_BASE_URL || 'https://api.example.com'}/products`, newProductData);
  expect(result).toEqual(responseData);
});

Paralelismo e Configuração

O Jest pode executar testes em paralelo para acelerar a execução. Configure isso em seu jest.config.js. Por exemplo, definir maxWorkers controla o número de processos paralelos.

Relatórios de Cobertura

Use o relatório de cobertura integrado do Jest para identificar partes do seu código que não estão sendo testadas. Execute os testes com --coverage para gerar relatórios detalhados.

jest --coverage

A revisão dos relatórios de cobertura ajuda a garantir que seus padrões de teste avançados estão cobrindo efetivamente a lógica crítica, incluindo os caminhos de código de internacionalização e localização.

Conclusão

Dominar padrões de teste avançados do Jest é um passo significativo para construir software confiável, de fácil manutenção e de alta qualidade para um público global. Ao utilizar eficazmente mocking, testes de snapshot, matchers personalizados e técnicas de teste assíncrono, você pode aprimorar a robustez da sua suíte de testes e ganhar maior confiança no comportamento da sua aplicação em diversos cenários e regiões. Adotar esses padrões capacita equipes de desenvolvimento em todo o mundo a entregar experiências de usuário excepcionais.

Comece a incorporar essas técnicas avançadas em seu fluxo de trabalho hoje para elevar suas práticas de teste em JavaScript.