Suponha que você tenha uma função que pega um tipo de união e depois restringe o tipo e delega para uma das duas outras funções puras.
function foo(arg: string|number) {
if (typeof arg === 'string') {
return fnForString(arg)
} else {
return fnForNumber(arg)
}
}
Suponha que fnForString()
e fnForNumber()
também sejam funções puras, e elas já foram testadas.
Como se deve testar foo()
?
- Você deve tratar o fato de que ele delega para
fnForString()
efnForNumber()
como um detalhe de implementação e essencialmente duplicar os testes para cada um deles ao escrever os testesfoo()
? Esta repetição é aceitável? - Você deve escrever testes que "saibam" que
foo()
delegamfnForString()
e,fnForNumber()
por exemplo, zombando deles e verificando se ele delega a eles?
javascript
typescript
testing
functional-programming
samfrances
fonte
fonte
Respostas:
Em um mundo ideal, você escreveria provas em vez de testes. Por exemplo, considere as seguintes funções.
Digamos que você queira provar que o
transform
aplicado duas vezes é idempotente , ou seja, para todas as entradas válidasx
,transform(transform(x))
é igual ax
. Bem, você primeiro precisa provar issonegate
ereverse
aplicar duas vezes é idempotente. Agora, suponha que provar a idempotência denegate
ereverse
aplicado duas vezes seja trivial, ou seja, o compilador pode descobrir isso. Assim, temos os seguintes lemas .Podemos usar esses dois lemas para provar que
transform
é idempotente da seguinte maneira.Há muita coisa acontecendo aqui, então vamos detalhar.
a|b
um tipo de união ea&b
um tipo de interseção,a≡b
é um tipo de igualdade.x
de um tipo de igualdadea≡b
é uma prova da igualdade dea
eb
.a
eb
, não são iguais, então é impossível construir um valor do tipoa≡b
.refl
, abreviação de reflexividade , tem o tipoa≡a
. É a prova trivial de um valor ser igual a si mesmo.refl
na prova denegateNegateIdempotent
ereverseReverseIdempotent
. Isso é possível porque as proposições são triviais o suficiente para o compilador provar automaticamente.negateNegateIdempotent
ereverseReverseIdempotent
para provartransformTransformIdempotent
. Este é um exemplo de uma prova não trivial.A vantagem de escrever provas é que o compilador verifica a prova. Se a prova estiver incorreta, o programa falhará em check e o compilador emitirá um erro. As provas são melhores que os testes por dois motivos. Primeiro, você não precisa criar dados de teste. É difícil criar dados de teste que lidem com todos os casos extremos. Segundo, você não esquecerá acidentalmente de testar casos extremos. O compilador lançará um erro se você o fizer.
Infelizmente, o TypeScript não possui um tipo de igualdade porque não suporta tipos dependentes, ou seja, tipos que dependem de valores. Portanto, você não pode escrever provas no TypeScript. Você pode escrever provas em linguagens de programação funcional de tipo dependente, como o Agda .
No entanto, você pode escrever proposições no TypeScript.
Em seguida, você pode usar uma biblioteca como jsverify para gerar automaticamente dados de teste para vários casos de teste.
Você também pode ligar
jsc.forall
com"number | string"
, mas eu não consigo fazê-lo funcionar.Então, para responder suas perguntas.
A programação funcional incentiva o teste baseado em propriedades. Por exemplo, o I testado
negate
,reverse
etransform
funções aplicado duas vezes por idempotência. Se você seguir o teste baseado em propriedades, suas funções de proposição deverão ter estrutura semelhante às funções que você está testando.Sim, é aceitável. No entanto, você pode renunciar inteiramente aos testes
fnForString
efnForNumber
porque os testes para esses estão incluídos nos testes parafoo
. No entanto, para completar, eu recomendaria incluir todos os testes, mesmo que isso introduza redundância.As proposições que você escreve nos testes baseados em propriedades seguem a estrutura das funções que você está testando. Portanto, eles "sabem" sobre as dependências usando as proposições das outras funções que estão sendo testadas. Não há necessidade de zombar deles. Você só precisa zombar de coisas como chamadas de rede, chamadas do sistema de arquivos etc.
fonte
A melhor solução seria apenas testar
foo
.fnForString
efnForNumber
são um detalhe de implementação que você pode alterar no futuro sem necessariamente alterar o comportamento defoo
. Se isso acontecer, seus testes podem ser interrompidos sem motivo, esse tipo de problema torna seu teste muito expansivo e inútil.Sua interface só precisa
foo
, basta testar.Se você precisar testar
fnForString
efnForNumber
manter esse tipo de teste separado dos testes de interface pública.Esta é a minha interpretação do seguinte princípio declarado por Kent Beck
fonte
Resposta curta: a especificação de uma função determina a maneira pela qual ela deve ser testada.
Resposta longa:
Teste = usando um conjunto de casos de teste (esperançosamente representativo de todos os casos que podem ser encontrados) para verificar se uma implementação atende às suas especificações.
No exemplo, foo é declarado sem especificação, portanto, deve-se testar foo sem fazer absolutamente nada (ou no máximo alguns testes tolos para verificar o requisito implícito de que "foo termina de uma maneira ou de outra").
Se a especificação for algo operacional como "esta função retorna o resultado da aplicação de args para fnForString ou fnForNumber de acordo com o tipo de args", zombar dos delegados (opção 2) é o caminho a seguir. Não importa o que aconteça com fnForString / Number, foo permanece de acordo com sua especificação.
Se a especificação não depender do fnForType dessa maneira, reutilizar os testes para o fnFortype (opção 1) é o caminho a seguir (supondo que esses testes sejam bons).
Observe que as especificações operacionais removem grande parte da liberdade usual de substituir uma implementação por outra (mais elegante / legível / eficiente / etc). Eles só devem ser usados após cuidadosa consideração.
fonte
Bem, como os detalhes da implementação são delegados para
fnForString()
efnForNumber()
parastring
enumber
respectivamente, o teste se resume a apenas garantir quefoo
a função correta seja chamada. Então, sim, eu zombaria deles e assegurava que eles fossem chamados de acordo.Desde
fnForString()
efnForNumber()
foi testado individualmente, você sabe que, quando ligafoo()
, chama a função correta e sabe que a função faz o que deve fazer.foo deve retornar algo. Você pode devolver algo de suas zombarias, cada uma uma coisa diferente e garantir que o foo retorne corretamente (por exemplo, se você esqueceu um
return
na sua função foo ).E todas as coisas foram cobertas.
fonte
Eu acho que é inútil testar o tipo de sua função, o sistema pode fazer isso sozinho e permitir que você dê o mesmo nome a cada um dos tipos de objetos que lhe interessam
Código de amostra
Provavelmente não sou um grande teórico em técnicas de codificação, mas fiz muita manutenção de código. Penso que se pode realmente julgar a eficácia de uma maneira de codificar apenas em casos concretos, a especulação não pode ter valor de prova.
fonte