Para estender um pouco o título, estou tentando chegar a alguma conclusão sobre se é necessário ou não declarar explicitamente (por exemplo, injetar) funções puras das quais depende alguma outra função ou classe.
Algum pedaço de código é menos testável ou pior projetado se ele usa funções puras sem solicitá-las? Eu gostaria de chegar a uma conclusão sobre o assunto, para qualquer tipo de função pura, desde funções simples e nativas (por exemplo max()
, min()
independentemente da linguagem) até as personalizadas, mais complicadas que, por sua vez, podem implicitamente depender de outras funções puras.
A preocupação usual é que, se algum código usar apenas uma dependência diretamente, você não poderá testá-lo isoladamente, ou seja, estará testando ao mesmo tempo todas as coisas que você silenciosamente trouxe consigo. Mas isso adiciona bastante clichê, se você precisar fazer isso para todas as pequenas funções, então eu me pergunto se isso ainda vale para funções puras, e por que ou por que não.
Respostas:
Não é ruim
Os testes que você escreve não devem se importar como uma determinada classe ou função é implementada. Em vez disso, deve garantir que eles produzam os resultados desejados, independentemente de como exatamente eles são implementados.
Como exemplo, considere a seguinte classe:
Você deseja testar a função 'Round' para garantir que ela retorne o que você espera, independentemente de como a função é realmente implementada. Você provavelmente escreveria um teste semelhante ao seguinte;
Do ponto de vista de teste, desde que a função Coord2d :: Round () retorne o que você espera, você não se importa com a implementação.
Em alguns casos, injetar uma função pura pode ser uma maneira realmente brilhante de tornar uma classe mais testável ou extensível.
Na maioria dos casos, no entanto, como a função Coord2d :: Round () acima, não há necessidade real de injetar uma função min / max / floor / ceil / trunc. A função foi projetada para arredondar seus valores para o número inteiro mais próximo. Os testes que você escreve devem verificar se a função faz isso.
Por fim, se você deseja testar o código do qual depende uma implementação de classe / função, basta fazê-lo escrevendo testes para a dependência.
Por exemplo, se a função Coord2d :: Round () fosse implementada da seguinte maneira ...
Se você quiser testar a função 'piso', poderá fazê-lo em um teste de unidade separado.
fonte
Do ponto de vista de teste - Não, mas apenas no caso de funções puras , quando a função retorna sempre a mesma saída para a mesma entrada.
Como você testa a unidade que usa a função injetada explicitamente?
Você injeta a função falsificada (falsa) e verifica se a função foi chamada com argumentos corretos.
Mas, como temos uma função pura , que sempre retorna o mesmo resultado para os mesmos argumentos - verificar os argumentos de entrada é igual para verificar o resultado da saída.
Com funções puras, você não precisa configurar dependências / estado extras.
Como benefício adicional, você obterá um melhor encapsulamento, testará o comportamento real da unidade.
E muito importante do ponto de vista de teste - você poderá refatorar o código interno da unidade sem alterar os testes - por exemplo, você decide adicionar mais um argumento para a função pura - com a função injetada explicitamente (zombada nos testes), será necessário altere a configuração do teste, onde, com a função usada implicitamente, você não faz nada.
Eu posso imaginar uma situação em que você precisa injetar uma função pura - é quando você quer oferecer aos consumidores uma mudança no comportamento da unidade.
Para o método acima, você espera que o cálculo do desconto possa ser alterado pelos consumidores, portanto, você deve excluir o teste de sua lógica dos testes de unidade do
CalcualteTotalPrice
método.fonte
Em um mundo ideal de programação do SOLID OO, você estaria injetando todo comportamento externo. Na prática, você sempre usará diretamente algumas funções simples (ou não tão simples).
Se você estiver computando o máximo de um conjunto de números, provavelmente seria um exagero injetar uma
max
função e zombar dela em testes de unidade. Geralmente, você não se preocupa em separar seu código de uma implementação específicamax
e testá-lo isoladamente.Se você está fazendo algo complexo como encontrar um caminho de custo mínimo em um gráfico, é melhor injetar a implementação concreta para flexibilidade e zombar dela em testes de unidade para obter melhor desempenho. Nesse caso, pode valer a pena dissociar o algoritmo de localização de caminhos do código que o utiliza.
Não pode haver uma resposta para "qualquer tipo de função pura", você precisa decidir onde traçar a linha. Existem muitos fatores envolvidos nessa decisão. No final, você deve ponderar os benefícios contra os problemas que a dissociação oferece a você, e isso depende de você e do seu contexto.
Vejo nos comentários que você pergunta sobre a diferença entre classes de injeção (na verdade você injeta objetos) e funções. Não há diferença, apenas os recursos de idioma fazem com que pareça diferente. De um ponto de vista abstrato, chamar uma função não é diferente de chamar o método de algum objeto e você injeta (ou não) a função pelos mesmos motivos pelos quais injeta (ou não) o objeto: para desacoplar esse comportamento do código de chamada e ter o capacidade de usar diferentes implementações do comportamento e decidir em outro lugar qual implementação usar.
Portanto, basicamente, quaisquer que sejam os critérios que você considere válidos para injeção de dependência, você pode usá-lo independentemente da dependência ser um objeto ou uma função. Pode haver algumas nuances, dependendo do idioma (ou seja, se você estiver usando java, isso não é problema).
fonte
max()
(em oposição a outra coisa)?As funções puras não afetam a capacidade de teste da classe devido às suas propriedades:
Isso significa que eles estão aproximadamente no mesmo domínio que os métodos privados, que estão completamente sob o controle da classe, com o bônus adicional de nem mesmo depender do estado atual da instância (ou seja, "this"). A classe de usuário "controla" a saída porque controla totalmente as entradas.
Os exemplos que você deu, max () e min (), são determinísticos. No entanto, random () e currentTime () são procedimentos, não funções puras (eles dependem / modificam o estado mundial fora da banda), por exemplo. Esses dois últimos teriam que ser injetados para possibilitar o teste.
fonte