Testando a função pura no tipo de união que delega para outras funções puras

8

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()e fnForNumber()como um detalhe de implementação e essencialmente duplicar os testes para cada um deles ao escrever os testes foo()? Esta repetição é aceitável?
  • Você deve escrever testes que "saibam" que foo()delegam fnForString()e, fnForNumber()por exemplo, zombando deles e verificando se ele delega a eles?
samfrances
fonte
2
Você criou sua própria função sobrecarregada com dependências codificadas. Outra maneira de obter esse tipo de polimorfismo (para ser mais preciso o polimorfismo ad-hoc) é passar as dependências da função como argumento (um diretório de tipos). Então você pode usar funções simuladas para fins de teste.
bob
Ok, mas é mais um caso de "como conseguir zombaria" - então sim, você pode passar as funções ou ter uma função ao curry etc. Mas minha pergunta era mais sobre o nível de "como zombar?" mas sim "está zombando da abordagem correta no contexto de funções puras?".
samfrances

Respostas:

1

Em um mundo ideal, você escreveria provas em vez de testes. Por exemplo, considere as seguintes funções.

const negate = (x: number): number => -x;

const reverse = (x: string): string => x.split("").reverse().join("");

const transform = (x: number|string): number|string => {
  switch (typeof x) {
  case "number": return negate(x);
  case "string": return reverse(x);
  }
};

Digamos que você queira provar que o transformaplicado duas vezes é idempotente , ou seja, para todas as entradas válidas x, transform(transform(x))é igual a x. Bem, você primeiro precisa provar isso negatee reverseaplicar duas vezes é idempotente. Agora, suponha que provar a idempotência de negatee reverseaplicado duas vezes seja trivial, ou seja, o compilador pode descobrir isso. Assim, temos os seguintes lemas .

const negateNegateIdempotent = (x: number): negate(negate(x))≡x => refl;

const reverseReverseIdempotent = (x: string): reverse(reverse(x))≡x => refl;

Podemos usar esses dois lemas para provar que transformé idempotente da seguinte maneira.

const transformTransformIdempotent = (x: number|string): transform(transform(x))≡x => {
  switch (typeof x) {
  case "number": return negateNegateIdempotent(x);
  case "string": return reverseReverseIdempotent(x);
  }
};

Há muita coisa acontecendo aqui, então vamos detalhar.

  1. Assim como a|bum tipo de união e a&bum tipo de interseção, a≡bé um tipo de igualdade.
  2. Um valor xde um tipo de igualdade a≡bé uma prova da igualdade de ae b.
  3. Se dois valores, ae b, não são iguais, então é impossível construir um valor do tipo a≡b.
  4. O valor refl, abreviação de reflexividade , tem o tipo a≡a. É a prova trivial de um valor ser igual a si mesmo.
  5. Usamos reflna prova de negateNegateIdempotente reverseReverseIdempotent. Isso é possível porque as proposições são triviais o suficiente para o compilador provar automaticamente.
  6. Usamos os lemas negateNegateIdempotente reverseReverseIdempotentpara provar transformTransformIdempotent. 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.

const negateNegateIdempotent = (x: number): boolean => negate(negate(x)) === x;

const reverseReverseIdempotent = (x: string): boolean => reverse(reverse(x)) === x;

const transformTransformIdempotent = (x: number|string): boolean => {
  switch (typeof x) {
  case "number": return negateNegateIdempotent(x);
  case "string": return reverseReverseIdempotent(x);
  }
};

Em seguida, você pode usar uma biblioteca como jsverify para gerar automaticamente dados de teste para vários casos de teste.

const jsc = require("jsverify");

jsc.assert(jsc.forall("number", transformTransformIdempotent)); // OK, passed 100 tests

jsc.assert(jsc.forall("string", transformTransformIdempotent)); // OK, passed 100 tests

Você também pode ligar jsc.forallcom "number | string", mas eu não consigo fazê-lo funcionar.


Então, para responder suas perguntas.

Como se deve testar foo()?

A programação funcional incentiva o teste baseado em propriedades. Por exemplo, o I testado negate, reversee transformfunçõ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.

Você deve tratar o fato de que ele delega para fnForString()e fnForNumber()como um detalhe de implementação e essencialmente duplicar os testes para cada um deles ao escrever os testes foo()? Esta repetição é aceitável?

Sim, é aceitável. No entanto, você pode renunciar inteiramente aos testes fnForStringe fnForNumberporque os testes para esses estão incluídos nos testes para foo. No entanto, para completar, eu recomendaria incluir todos os testes, mesmo que isso introduza redundância.

Você deve escrever testes que "saibam" que foo()delegam fnForString()e, fnForNumber()por exemplo, zombando deles e verificando se ele delega a eles?

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.

Aadit M Shah
fonte
5

A melhor solução seria apenas testar foo.

fnForStringe fnForNumbersão um detalhe de implementação que você pode alterar no futuro sem necessariamente alterar o comportamento de foo. 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 fnForStringe fnForNumbermanter esse tipo de teste separado dos testes de interface pública.

Esta é a minha interpretação do seguinte princípio declarado por Kent Beck

Os testes do programador devem ser sensíveis às mudanças de comportamento e insensíveis às mudanças na estrutura. Se o comportamento do programa for estável da perspectiva de um observador, nenhum teste deve mudar.

Giuseppe Schembri
fonte
2

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.

Koen AIS
fonte
1

Suponha que fnForString () e fnForNumber () também sejam funções puras, e elas próprias já foram testadas.

Bem, como os detalhes da implementação são delegados para fnForString()e fnForNumber()para stringe numberrespectivamente, o teste se resume a apenas garantir que fooa função correta seja chamada. Então, sim, eu zombaria deles e assegurava que eles fossem chamados de acordo.

foo("a string")
fnForNumberMock.hasNotBeenCalled()
fnForStringMock.hasBeenCalled()

Desde fnForString()e fnForNumber()foi testado individualmente, você sabe que, quando liga foo(), 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 returnna sua função foo ).

E todas as coisas foram cobertas.

jperl
fonte
0

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

  //  fnForStringorNumber String Wrapper  
String.prototype.fnForStringorNumber = function() {
  return  this.repeat(3)
}
  //  fnForStringorNumber Number Wrapper  
Number.prototype.fnForStringorNumber = function() {
  return this *3
}

function foo( arg ) {
  return arg.fnForStringorNumber(4321)
}

console.log ( foo(1234) )       // 3702
console.log ( foo('abcd_') )   // abcd_abcd_abcd_

// or simply:
console.log ( (12).fnForStringorNumber() )     // 36
console.log ( 'xyz_'.fnForStringorNumber() )   // xyz_xyz_xyz_

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.

Senhor Jojo
fonte