Dependendo implicitamente de funções puras, é ruim (em particular, para testes)?

8

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.

DanielM
fonte
2
Eu, pelo menos não entendo essa pergunta. O que importa se uma função é pura para propósitos de DI?
Telastyn
Então você propõe que funções puras, funções impuras e classes sejam injetadas da mesma forma, independentemente do tamanho, dependências transitivas ou qualquer outra coisa. Você poderia elaborar isso?
DanielM
Não, eu simplesmente não entendo o que você está perguntando. As funções raramente são injetadas em qualquer coisa e, quando são, o consumidor não pode garantir sua pureza.
Telastyn
1
Você pode injetar todas as funcionalidades, mas geralmente não há valor em zombar dessas funcionalidades em um teste, o que significa que injetá-las não tem valor. Por exemplo, como os operadores são basicamente funções estáticas, pode-se decidir injetar o operador + e zombar dele em um teste. A menos que isso tenha algum valor, você não faria isso.
user2180613
1
Os operadores também passaram pela minha cabeça. Certamente seria inconveniente, mas o ponto que eu estava tentando encontrar é como tomar a decisão do que injetar e do que não. Em outras palavras, como decidir quando há valor e quando não. Inspirado na resposta de @ Goyo, acho que agora um bom critério é injetar tudo o que não é uma parte intrínseca do sistema, para manter o sistema e suas dependências fracamente acoplados; e, por outro lado, basta usar (portanto, não injetar) coisas que realmente fazem parte da identidade do sistema, com as quais o sistema é altamente coeso.
DanielM

Respostas:

10

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:

Coord2d{
    float x, y;

    ///Will round to the nearest whole number
    Coord2d Round();
}

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;

Coord2d testValue(0.6, 0.1)
testValueRounded = testValue.Round()
CHECK( testValueRounded.x == 1.0 )
CHECK( testValueRounded.y == 0.0 )

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 ...

Coord2d Round(){
    return Coord2d( floor(x + 0.5f),  floor(y + 0.5f))
}

Se você quiser testar a função 'piso', poderá fazê-lo em um teste de unidade separado.

CHECK( floor (1.436543) == 1.0)
CHECK( floor (134.936) == 134.0)
Ryan
fonte
Tudo o que você disse se refere a funções puras, certo? Em caso afirmativo, que diferença faz com classes ou classes não puras? Quero dizer, você pode apenas codificar tudo e aplicar a mesma lógica ("tudo de que dependo já foi testado"). E uma pergunta diferente: você poderia expandir quando seria brilhante injetar funções puras e por quê? Obrigado!
DanielM
3
@DanielM Absolutamente - os testes de unidade devem abranger alguma unidade isolada de comportamento significativo - se a validade do comportamento da "unidade" depender estritamente do retorno do máximo de algo, então abstrair o "max" não é útil e reduz a utilidade do teste também. Por outro lado, se estou retornando o "máximo" de tudo o que vem de algum provedor, é útil stubar o provedor, porque o que ele faz não é diretamente pertinente ao comportamento pretendido desta unidade e sua correção pode ser verificada em outro lugar. Lembre-se - "unit"! = "Class."
Ant #
2
Em última análise, porém, pensar demais nesse tipo de coisa geralmente é contraproducente. É aqui que o teste primeiro tende a se desenvolver. Defina o comportamento isolado que você deseja testar e, em seguida, escreva o teste e a decisão sobre o que injetar e o que não injetar é feita para você.
Ant #
4

É implicitamente dependendo funções pura má

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.

public decimal CalculateTotalPrice(Order order, Func<Customer, decimal> calculateDiscount)
{
    // Do some calculation based on order data
    var totalPrice = order.Lines.Sum(line => line.Price);

    // Then
    var discountAmount = calculateDiscount(order.Customer);

    return totalPrice - discountAmount;
}

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 CalcualteTotalPricemétodo.

Fabio
fonte
Injetar a função não significa que você precisa testar as chamadas de função. Na verdade, eu afirmaria que o SUT faz a coisa certa (em relação ao estado observável) quando é passado o escárnio. OTOH zombar de uma dependência que não faz parte da interface do SUT seria estranho aos meus olhos.
Pare de prejudicar Monica
@ Boyu, você configurará o mock para retornar o valor esperado apenas para uma entrada específica. Portanto, seu mock "guardará" que os argumentos de entrada corretos foram fornecidos.
Fabio
4

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 maxfunção e zombar dela em testes de unidade. Geralmente, você não se preocupa em separar seu código de uma implementação específica maxe 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).

Pare de prejudicar Monica
fonte
3
Injetar nada tem a ver com programação orientada a objetos.
precisa
@FrankHileman Melhor? Você precisa da injeção de dependência para depender das abstrações porque não pode instancia-las.
Pare de prejudicar Monica
Seria definitivamente benéfico ter uma lista aproximada dos fatores mais importantes envolvidos na prática. Relacionado a isso, por que você não se importaria com o acoplamento max()(em oposição a outra coisa)?
21817 DanielM em
O SOLID não é "ideal" nem tem nada a ver com programação orientada a objetos.
31817 Frank Hileman #
@FrankHileman Como o SOLID não tem nada a ver com OOP? Os cinco princípios foram propostos por Robert Martin em seu artigo de 2000 em um capítulo chamado Princípios de Design de Classe Orientada a Objetos?
Nickl
3

As funções puras não afetam a capacidade de teste da classe devido às suas propriedades:

  • sua saída (ou erro / exceção) depende apenas de suas entradas;
  • sua produção não depende do estado mundial;
  • sua operação não modifica o estado mundial.

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.

carlosdavidepto
fonte