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
- MDN Web Docs: Pure Functions — Definição oficial e exemplos de funções puras na documentação da Mozilla.
- Wikipedia: Pure Function — Artigo enciclopédico detalhando características matemáticas e computacionais de funções puras.
- Refactoring Guru: Pure Functions — Guia prático com exemplos de refatoração para tornar funções mais puras e testáveis.
- FreeCodeCamp: Pure vs Impure Functions — Tutorial comparativo com exemplos em JavaScript sobre os benefícios de testabilidade.
- Martin Fowler: Pure Functions — Artigo do renomado autor sobre o conceito e sua aplicação em design de software.
- GitHub: Awesome Functional Programming — Lista curada de recursos sobre programação funcional, incluindo funções puras.
- Stack Overflow: What is a pure function? — Discussão técnica com exemplos práticos e esclarecimento de dúvidas comuns.