Prueba de la función pura en el tipo de unión que delega a otras funciones puras

8

Suponga que tiene una función que toma un tipo de unión y luego reduce el tipo y delega a una de las otras dos funciones puras.

function foo(arg: string|number) {
    if (typeof arg === 'string') {
        return fnForString(arg)
    } else {
        return fnForNumber(arg)
    }
}

Suponga que fnForString()y fnForNumber()también son funciones puras, y ellas mismas ya han sido probadas.

¿Cómo se debe hacer una prueba foo()?

  • ¿Debería tratar el hecho de que delega fnForString()y fnForNumber()como un detalle de implementación, y esencialmente duplicar las pruebas para cada una de ellas al escribir las pruebas foo()? ¿Es aceptable esta repetición?
  • ¿Debería escribir pruebas que "sepan" que foo()delegan fnForString()y, fnForNumber()por ejemplo, burlándose de ellas y verificando que deleguen en ellas?
samfrances
fuente
2
Usted creó su propia función sobrecargada con dependencias codificadas. Otra forma de lograr este tipo de polimorfismo (el polimorfismo ad-hoc, para ser exactos) es pasar las dependencias de la función como un argumento (un directorio de tipo). Entonces podría usar funciones simuladas para fines de prueba.
bob
Ok, pero ese es más un caso de "cómo lograr burlarse", así que sí, podría pasar las funciones, o tener una función curry, etc. Pero mi pregunta fue más en el nivel de "¿cómo burlarse?" sino más bien "¿se está burlando del enfoque correcto en el contexto de funciones puras?".
Samfrances

Respuestas:

1

En un mundo ideal, escribirías pruebas en lugar de pruebas. Por ejemplo, considere las siguientes funciones.

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 quiere demostrar que transformaplicado dos veces es idempotente , es decir, para todas las entradas válidas x, transform(transform(x))es igual a x. Bueno, primero deberías probar que negatey reverseaplicado dos veces son idempotentes. Ahora, suponga que probar la idempotencia negatey reverseaplicar dos veces es trivial, es decir, el compilador puede resolverlo. Por lo tanto, tenemos los siguientes lemas .

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

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

Podemos usar estos dos lemas para demostrar que transformes idempotente de la siguiente manera.

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

Están sucediendo muchas cosas aquí, así que analicemos.

  1. Así como a|bes un tipo de unión y a&bes un tipo de intersección, a≡bes un tipo de igualdad.
  2. Un valor xde un tipo de igualdad a≡bes una prueba de la igualdad de ay b.
  3. Si dos valores, ay b, no son iguales, entonces es imposible construir un valor de tipo a≡b.
  4. El valor refl, la abreviatura de reflexividad , tiene el tipo a≡a. Es la prueba trivial de que un valor es igual a sí mismo.
  5. Usamos reflen la prueba de negateNegateIdempotenty reverseReverseIdempotent. Esto es posible porque las proposiciones son lo suficientemente triviales para que el compilador las pruebe automáticamente.
  6. Usamos los negateNegateIdempotenty reverseReverseIdempotentlemas para probar transformTransformIdempotent. Este es un ejemplo de una prueba no trivial.

La ventaja de escribir pruebas es que el compilador verifica la prueba. Si la prueba es incorrecta, el programa no puede escribir check y el compilador arroja un error. Las pruebas son mejores que las pruebas por dos razones. Primero, no tiene que crear datos de prueba. Es difícil crear datos de prueba que manejen todos los casos límite. En segundo lugar, no olvidará accidentalmente probar los casos límite. El compilador arrojará un error si lo haces.


Desafortunadamente, TypeScript no tiene un tipo de igualdad porque no admite tipos dependientes, es decir, tipos que dependen de valores. Por lo tanto, no puede escribir pruebas en TypeScript. Puede escribir pruebas en lenguajes de programación funcional de tipo dependiente como Agda .

Sin embargo, puede escribir proposiciones en 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);
  }
};

Luego puede usar una biblioteca como jsverify para generar automáticamente datos de prueba para múltiples casos de prueba.

const jsc = require("jsverify");

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

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

También puede llamar jsc.foralla "number | string"pero me parece que no puede conseguir que funcione.


Entonces para responder a sus preguntas.

¿Cómo se debe hacer una prueba foo()?

La programación funcional fomenta las pruebas basadas en propiedades. Por ejemplo, he probado el negate, reversey transformFunciones de aplicación dos veces para idempotencia. Si sigue las pruebas basadas en propiedades, las funciones de su propuesta deben ser similares en estructura a las funciones que está probando.

¿Debería tratar el hecho de que delega fnForString()y fnForNumber()como un detalle de implementación, y esencialmente duplicar las pruebas para cada una de ellas al escribir las pruebas foo()? ¿Es aceptable esta repetición?

Sí, es aceptable Sin embargo, puede renunciar por completo a las pruebas fnForStringy fnForNumberporque las pruebas para esos están incluidas en las pruebas para foo. Sin embargo, para completar, recomendaría incluir todas las pruebas incluso si introduce redundancia.

¿Debería escribir pruebas que "sepan" que foo()delegan fnForString()y, fnForNumber()por ejemplo, burlándose de ellas y verificando que deleguen en ellas?

Las proposiciones que escribe en las pruebas basadas en propiedades siguen la estructura de las funciones que está probando. Por lo tanto, "saben" acerca de las dependencias utilizando las proposiciones de las otras funciones que se están probando. No hay necesidad de burlarse de ellos. Solo necesitaría burlarse de cosas como llamadas de red, llamadas al sistema de archivos, etc.

Aadit M Shah
fuente
5

La mejor solución sería simplemente probar foo.

fnForStringy fnForNumberson un detalle de implementación que puede cambiar en el futuro sin necesariamente cambiar el comportamiento de foo. Si eso sucede, sus pruebas pueden romperse sin razón, este tipo de problema hace que su prueba sea demasiado expansiva e inútil.

Tu interfaz solo necesita foo, solo pruébalo.

Si tiene que probar fnForStringy fnForNumbermantener este tipo de prueba aparte de sus pruebas de interfaz pública.

Esta es mi interpretación del siguiente principio establecido por Kent Beck

Las pruebas del programador deben ser sensibles a los cambios de comportamiento e insensibles a los cambios de estructura. Si el comportamiento del programa es estable desde la perspectiva del observador, ninguna prueba debería cambiar.

Giuseppe Schembri
fuente
2

Respuesta corta: la especificación de una función determina la manera en que debe probarse.

Respuesta larga:

Prueba = utilizando un conjunto de casos de prueba (con suerte representativos de todos los casos que puedan encontrarse) para verificar que una implementación cumpla con sus especificaciones.

En el ejemplo, foo se establece sin especificación, por lo que uno debe hacer pruebas de foo sin hacer nada (o como mucho algunas pruebas tontas para verificar el requisito implícito de que "foo termina de una forma u otra").

Si la especificación es algo operativo como "esta función devuelve el resultado de aplicar args a fnForString o fnForNumber según el tipo de args", entonces burlarse de los delegados (opción 2) es el camino a seguir. Pase lo que pase con fnForString / Number, foo permanece de acuerdo con su especificación.

Si la especificación no depende de fnForType de tal manera, entonces volver a usar las pruebas para fnFortype (opción 1) es el camino a seguir (suponiendo que esas pruebas sean buenas).

Tenga en cuenta que las especificaciones operativas eliminan gran parte de la libertad habitual para reemplazar una implementación por otra (una que sea más elegante / legible / eficiente / etc.). Solo deben usarse después de una cuidadosa consideración.

Koen AIS
fuente
1

Suponga que fnForString () y fnForNumber () también son funciones puras, y ya se han probado.

Bueno, ya que los detalles de implementación se delegan a fnForString()y fnForNumber()parastring y numberrespectivamente, probarlo se reduce a simplemente asegurarse de que foollama a la función correcta. Entonces, sí, me burlaría de ellos y me aseguraría de que se los llame en consecuencia.

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

Ya que fnForString() y fnForNumber()han sido probados individualmente, sabe que cuando llama foo(), llama a la función correcta y sabe que la función hace lo que se supone que debe hacer.

foo debería devolver algo. Se podría devolver algo de su burla, cada una cosa diferente y asegurar que foo vuelve correctamente (por ejemplo, si ha olvidado una returnen su foo función).

Y todas las cosas han sido cubiertas.

jperl
fuente
0

Creo que es inútil probar el tipo de su función, el sistema puede hacerlo solo y le permite dar el mismo nombre a cada uno de los tipos de objetos que le interesan

Código de muestra

  //  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_

Probablemente no soy un gran teórico en técnicas de codificación, pero hice mucho mantenimiento de código. Creo que uno realmente puede juzgar la efectividad de una forma de codificación solo en casos concretos, la especulación no puede tener valor de prueba.

Señor jojo
fuente