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()
yfnForNumber()
como un detalle de implementación, y esencialmente duplicar las pruebas para cada una de ellas al escribir las pruebasfoo()
? ¿Es aceptable esta repetición? - ¿Debería escribir pruebas que "sepan" que
foo()
deleganfnForString()
y,fnForNumber()
por ejemplo, burlándose de ellas y verificando que deleguen en ellas?
javascript
typescript
testing
functional-programming
samfrances
fuente
fuente
Respuestas:
En un mundo ideal, escribirías pruebas en lugar de pruebas. Por ejemplo, considere las siguientes funciones.
Digamos que quiere demostrar que
transform
aplicado dos veces es idempotente , es decir, para todas las entradas válidasx
,transform(transform(x))
es igual ax
. Bueno, primero deberías probar quenegate
yreverse
aplicado dos veces son idempotentes. Ahora, suponga que probar la idempotencianegate
yreverse
aplicar dos veces es trivial, es decir, el compilador puede resolverlo. Por lo tanto, tenemos los siguientes lemas .Podemos usar estos dos lemas para demostrar que
transform
es idempotente de la siguiente manera.Están sucediendo muchas cosas aquí, así que analicemos.
a|b
es un tipo de unión ya&b
es un tipo de intersección,a≡b
es un tipo de igualdad.x
de un tipo de igualdada≡b
es una prueba de la igualdad dea
yb
.a
yb
, no son iguales, entonces es imposible construir un valor de tipoa≡b
.refl
, la abreviatura de reflexividad , tiene el tipoa≡a
. Es la prueba trivial de que un valor es igual a sí mismo.refl
en la prueba denegateNegateIdempotent
yreverseReverseIdempotent
. Esto es posible porque las proposiciones son lo suficientemente triviales para que el compilador las pruebe automáticamente.negateNegateIdempotent
yreverseReverseIdempotent
lemas para probartransformTransformIdempotent
. 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.
Luego puede usar una biblioteca como jsverify para generar automáticamente datos de prueba para múltiples casos de prueba.
También puede llamar
jsc.forall
a"number | string"
pero me parece que no puede conseguir que funcione.Entonces para responder a sus preguntas.
La programación funcional fomenta las pruebas basadas en propiedades. Por ejemplo, he probado el
negate
,reverse
ytransform
Funciones 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.Sí, es aceptable Sin embargo, puede renunciar por completo a las pruebas
fnForString
yfnForNumber
porque las pruebas para esos están incluidas en las pruebas parafoo
. Sin embargo, para completar, recomendaría incluir todas las pruebas incluso si introduce redundancia.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.
fuente
La mejor solución sería simplemente probar
foo
.fnForString
yfnForNumber
son un detalle de implementación que puede cambiar en el futuro sin necesariamente cambiar el comportamiento defoo
. 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
fnForString
yfnForNumber
mantener este tipo de prueba aparte de sus pruebas de interfaz pública.Esta es mi interpretación del siguiente principio establecido por Kent Beck
fuente
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.
fuente
Bueno, ya que los detalles de implementación se delegan a
fnForString()
yfnForNumber()
parastring
ynumber
respectivamente, probarlo se reduce a simplemente asegurarse de quefoo
llama a la función correcta. Entonces, sí, me burlaría de ellos y me aseguraría de que se los llame en consecuencia.Ya que
fnForString()
yfnForNumber()
han sido probados individualmente, sabe que cuando llamafoo()
, 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
return
en su foo función).Y todas las cosas han sido cubiertas.
fuente
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
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.
fuente