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.
- Así como
a|bes un tipo de unión y a&bes un tipo de intersección, a≡bes un tipo de igualdad.
- Un valor
xde un tipo de igualdad a≡bes una prueba de la igualdad de ay b.
- Si dos valores,
ay b, no son iguales, entonces es imposible construir un valor de tipo a≡b.
- 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.
- 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.
- 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.