Para extender un poco el título, estoy tratando de llegar a una conclusión sobre si es necesario o no declarar explícitamente (es decir, inyectar) funciones puras de las que depende otra función o clase.
¿Hay algún código menos comprobable o peor diseñado si utiliza funciones puras sin pedirlas? Me gustaría llegar a una conclusión al respecto, para cualquier tipo de función pura, desde funciones simples y nativas (por ejemplo max()
, min()
independientemente del idioma) hasta personalizadas y más complicadas que a su vez pueden depender implícitamente de otras funciones puras.
La preocupación habitual es que si algún código solo usa una dependencia directamente, no podrá probarlo de forma aislada, es decir, probará al mismo tiempo todas las cosas que trajo en silencio. Pero esto agrega un poco de repetitivo si tiene que hacerlo para cada pequeña función, por lo que me pregunto si esto aún es válido para funciones puras, y por qué o por qué no.
Respuestas:
No es malo
Las pruebas que escriba no deberían importarle cómo se implementa una determinada clase o función. Más bien, debe garantizar que produzcan los resultados que desea, independientemente de cómo se implementen exactamente.
Como ejemplo, considere la siguiente clase:
Debería probar la función 'Redondear' para asegurarse de que devuelve lo que espera, independientemente de cómo se implemente realmente la función. Probablemente escribirías una prueba similar a la siguiente;
Desde el punto de vista de la prueba, siempre que su función Coord2d :: Round () devuelva lo que espera, no le importa cómo se implemente.
En algunos casos, inyectar una función pura podría ser una forma realmente brillante de hacer que una clase sea más comprobable o extensible.
Sin embargo, en la mayoría de los casos, como la función Coord2d :: Round () anterior, no hay necesidad real de inyectar una función min / max / floor / ceil / trunc. La función está diseñada para redondear sus valores al número entero más cercano. Las pruebas que escriba deben verificar que la función haga esto.
Por último, si desea probar el código del que depende la implementación de una clase / función, puede hacerlo simplemente escribiendo pruebas para la dependencia.
Por ejemplo, si la función Coord2d :: Round () se implementó así ...
Si desea probar la función 'piso', puede hacerlo en una prueba de unidad separada.
fuente
Desde el punto de vista de la prueba: no, pero solo en el caso de funciones puras , cuando la función devuelve siempre la misma salida para la misma entrada.
¿Cómo prueba la unidad que usa la función inyectada explícitamente?
Inyectará la función simulada (falsa) y comprobará que se llamó a la función con los argumentos correctos.
Pero debido a que tenemos una función pura , que siempre devuelve el mismo resultado para los mismos argumentos, verificar los argumentos de entrada es igual para verificar el resultado de salida.
Con funciones puras, no necesita configurar dependencias / estado adicionales.
Como beneficio adicional obtendrá una mejor encapsulación, probará el comportamiento real de la unidad.
Y muy importante desde el punto de vista de la prueba: podrá refactorizar el código interno de la unidad sin cambiar las pruebas, por ejemplo, si decide agregar un argumento más para la función pura, con la función inyectada explícitamente (burlada en las pruebas) necesitará cambie la configuración de prueba, donde con la función implícitamente utilizada no hace nada.
Puedo imaginar una situación en la que necesita inyectar una función pura, es decir, cuando desea ofrecer a los consumidores que cambien el comportamiento de la unidad.
Para el método anterior, espera que los consumidores puedan cambiar el cálculo del descuento, por lo que debe excluir la prueba de su lógica de las pruebas unitarias del
CalcualteTotalPrice
método.fuente
En un mundo ideal de programación SOLID OO, estaría inyectando cada comportamiento externo. En la práctica, siempre utilizará algunas funciones simples (o no tan simples) directamente.
Si está calculando el máximo de un conjunto de números, probablemente sería excesivo inyectar una
max
función y simularla en pruebas unitarias. Por lo general, no le importa desacoplar su código de una implementación específicamax
y probarlo de forma aislada.Si está haciendo algo complejo como encontrar una ruta de costo mínimo en un gráfico, es mejor que inyecte la implementación concreta para obtener flexibilidad y simularla en pruebas unitarias para un mejor rendimiento. En este caso, podría valer la pena desacoplar el algoritmo de búsqueda de ruta del código que lo usa.
No puede haber una respuesta para "cualquier tipo de función pura", debe decidir dónde dibujar la línea. Hay demasiados factores involucrados en esta decisión. Al final, tiene que sopesar los beneficios contra los problemas que el desacoplamiento le brinda, y eso depende de usted y su contexto.
Veo en los comentarios que pregunta sobre la diferencia entre inyectar clases (en realidad inyecta objetos) y funciones. No hay diferencia, solo las características del lenguaje hacen que se vea diferente. Desde un punto de vista abstracto, llamar a una función no es diferente de llamar al método de algún objeto y usted inyecta (o no) la función por las mismas razones por las que inyecta (o no) el objeto: para desacoplar ese comportamiento del código de llamada y tener el capacidad de usar diferentes implementaciones del comportamiento y decidir en otro lugar qué implementación usar.
Entonces, básicamente, cualquier criterio que encuentre válido para la inyección de dependencia puede usarlo independientemente de que la dependencia sea un objeto o una función. Puede haber algunos matices dependiendo del idioma (es decir, si está utilizando Java, esto no es un problema).
fuente
max()
(como opuesto a otra cosa)?Las funciones puras no afectan la capacidad de prueba de la clase debido a sus propiedades:
Esto significa que están aproximadamente en el mismo ámbito que los métodos privados, que están completamente bajo el control de la clase, con la ventaja adicional de no depender ni siquiera del estado actual de la instancia (es decir, "esto"). La clase de usuario "controla" la salida porque controla completamente las entradas.
Los ejemplos que dio, max () y min (), son deterministas. Sin embargo, random () y currentTime () son procedimientos, no funciones puras (dependen / modifican el estado mundial fuera de banda), por ejemplo. Estos dos últimos tendrían que inyectarse para que las pruebas sean posibles.
fuente