Funções puras e por que elas tornam seu código mais testável

1. O que são funções puras? Definição e características fundamentais

Funções puras são o alicerce da programação funcional e um dos conceitos mais transformadores para quem busca código confiável e testável. Uma função é considerada pura quando satisfaz dois critérios essenciais: determinismo e ausência de efeitos colaterais.

1.1. Determinismo: mesma entrada, mesma saída, sempre

O determinismo significa que, para um mesmo conjunto de argumentos, a função retornará exatamente o mesmo resultado, independentemente de quantas vezes for chamada ou do contexto em que estiver inserida. Não há variáveis ocultas, estado interno ou fatores externos que influenciem o resultado.

// Função pura: determinística
function somar(a, b) {
    return a + b;
}

// Chamadas sempre produzem o mesmo resultado
somar(2, 3);  // sempre 5
somar(2, 3);  // sempre 5

1.2. Ausência de efeitos colaterais: sem mutação de estado externo

Efeitos colaterais são qualquer modificação no estado do programa ou interação com o mundo exterior que não seja o retorno da função. Uma função pura não altera variáveis globais, não modifica objetos recebidos como parâmetro, não escreve em arquivos, não faz chamadas de rede e não imprime nada no console.

// Função impura: modifica estado externo
let contador = 0;
function incrementar() {
    contador++;
    return contador;
}

// Função pura: sem efeitos colaterais
function incrementarPuro(valor) {
    return valor + 1;
}

1.3. Exemplos práticos em código: pura vs. impura lado a lado

// IMPURA: depende de estado global e tem efeito colateral
let taxaJuros = 0.05;
function calcularJuros(saldo) {
    taxaJuros = 0.07;  // efeito colateral: altera estado global
    return saldo * taxaJuros;
}

// PURA: recebe tudo que precisa como parâmetro
function calcularJurosPuro(saldo, taxa) {
    return saldo * taxa;
}

2. Por que funções puras são naturalmente mais testáveis?

2.1. Isolamento total: sem dependências ocultas

Funções puras não dependem de banco de dados, APIs externas, sistema de arquivos ou estado global. Isso significa que o teste unitário testa exclusivamente a lógica da função, sem necessidade de configurar ambientes complexos.

// Testar função pura é trivial
function calcularDesconto(preco, percentual) {
    return preco * (1 - percentual / 100);
}

// Teste unitário
// Entrada: preco = 200, percentual = 10
// Saída esperada: 180
// Não precisa mockar nada, não precisa de banco

2.2. Facilidade de mock: teste sem frameworks complexos

Com funções impuras, você frequentemente precisa de bibliotecas de mocking para simular bancos de dados, requisições HTTP ou arquivos. Funções puras eliminam essa necessidade — os parâmetros já são os "mocks" naturais.

2.3. Previsibilidade: testes que nunca falham por causas externas

Testes de funções puras falham apenas quando a lógica está errada, nunca por problemas de rede, arquivo inexistente ou estado global corrompido. Isso reduz drasticamente falsos positivos e aumenta a confiança no conjunto de testes.

3. Efeitos colaterais comuns que quebram a pureza

3.1. I/O: leitura de arquivos, chamadas de rede, console.log

// Impura: depende de I/O
function lerConfiguracao() {
    return fs.readFileSync('config.json', 'utf8');
}

// Impura: efeito colateral de saída
function logar(mensagem) {
    console.log(mensagem);
    return true;
}

3.2. Mutação de estado global: variáveis compartilhadas, singletons

// Impura: modifica array recebido como parâmetro
function adicionarItem(lista, item) {
    lista.push(item);  // mutação!
    return lista;
}

// Pura: retorna nova cópia
function adicionarItemPuro(lista, item) {
    return [...lista, item];
}

3.3. Dependência de tempo: Date.now(), Math.random(), timers

// Impura: resultado varia a cada chamada
function gerarTimestamp() {
    return Date.now();
}

// Impura: não determinística
function sortearNumero(max) {
    return Math.floor(Math.random() * max);
}

4. Como isolar efeitos colaterais sem perder a pureza

4.1. Estratégia de "empurrar" efeitos para as bordas do sistema

A abordagem mais prática é concentrar efeitos colaterais nas camadas externas da aplicação (entrada/saída) e manter o núcleo puro. O padrão "Functional Core, Imperative Shell" é referência nesse sentido.

4.2. Uso de injeção de dependência para tornar funções puras

Em vez de uma função buscar seus dados, ela recebe os dados prontos como parâmetro. Isso transforma funções impuras em puras.

4.3. Exemplo prático: refatorando uma função impura em pura

// Versão impura original
function processarPedido(idPedido) {
    const pedido = banco.buscarPedido(idPedido);  // dependência externa
    pedido.status = 'processado';                 // mutação
    banco.salvarPedido(pedido);                   // efeito colateral
    return pedido;
}

// Versão pura refatorada
function processarPedidoPuro(pedido) {
    return { ...pedido, status: 'processado' };
}

// O efeito colateral fica na camada externa
function handlerProcessarPedido(idPedido) {
    const pedido = banco.buscarPedido(idPedido);
    const pedidoProcessado = processarPedidoPuro(pedido);
    banco.salvarPedido(pedidoProcessado);
}

5. Benefícios além da testabilidade: manutenibilidade e composição

5.1. Reutilização sem medo: funções puras são previsíveis em qualquer contexto

Por não dependerem de estado externo, funções puras podem ser usadas em qualquer parte do sistema sem risco de efeitos inesperados.

5.2. Composição simples: combinando funções puras como blocos LEGO

// Composição natural de funções puras
function dobrar(x) { return x * 2; }
function somarUm(x) { return x + 1; }

function processar(valor) {
    return somarUm(dobrar(valor));
}

5.3. Debugging facilitado: rastreamento de bugs sem efeitos colaterais

Quando um bug aparece, você pode reproduzi-lo isoladamente apenas com os parâmetros de entrada, sem precisar recriar o estado completo do sistema.

6. Limitações e críticas realistas ao purismo funcional

6.1. Nem todo código pode ser puro: a realidade dos sistemas reais

Aplicações precisam interagir com o mundo: bancos de dados, APIs, interfaces de usuário. O objetivo não é pureza absoluta, mas sim isolar a lógica pura das impurezas necessárias.

6.2. Performance: cópia de dados vs. mutação in-place

Criar novas cópias de objetos para evitar mutação pode ser custoso em termos de memória e processamento, especialmente em estruturas de dados grandes.

6.3. Quando a pureza excessiva vira overengineering

Forçar pureza em toda função pode levar a código com muitos parâmetros, estruturas complexas e perda de legibilidade. É preciso equilíbrio.

7. Estratégia prática para adotar funções puras em projetos existentes

7.1. Identificando funções candidatas: regras de bolso

  • Funções que transformam dados (cálculos, formatações, validações)
  • Funções que não realizam I/O
  • Funções que você já testa com mocks complexos

7.2. Refatoração incremental: sem "big bang" funcional

Comece pelas funções mais simples e de menor risco. Extraia a lógica pura de funções impuras gradualmente, mantendo o sistema funcionando durante a transição.

7.3. Métricas para medir o impacto na testabilidade do time

  • Redução no número de mocks por teste
  • Aumento na velocidade de execução dos testes
  • Diminuição de testes quebrados por alterações em dependências externas

Referências