testing - serp - test seo tags
¿Cómo burlarse para probar en Haskell? (7)
¿No podría simplemente pasar una función llamada g
a f
? Siempre que g
satisfaga la interfaz typeOfSomeParms -> gReturnType
, entonces debería poder pasar la función real o una función simulada.
p.ej
f g = do
...
g someParams
...
No he usado inyección de dependencias en Java, pero los textos que he leído hacen que parezca mucho más que pasar funciones de orden superior, así que tal vez haga lo que quiera.
Respuesta a editar: la respuesta de ephemient es mejor si necesita resolver el problema de una manera empresarial, porque define un tipo que contiene múltiples funciones. La forma de creación de prototipos que propongo pasaría simplemente una tupla de funciones sin definir un tipo contenedor. Pero casi nunca escribo anotaciones tipo, así que refactorizar eso no es muy difícil.
Supongamos que estoy definiendo una función Haskell f (pura o una acción) y en alguna parte dentro de f llamo función g. Por ejemplo:
f = ...
g someParms
...
¿Cómo reemplazo la función g con una versión simulada para la prueba unitaria?
Si estuviera trabajando en Java, g sería un método en la clase SomeServiceImpl
que implementa la interfaz SomeService
. Luego, usaría la inyección de dependencia para decir f para usar SomeServiceImpl
o MockSomeServiceImpl
. No estoy seguro de cómo hacer esto en Haskell.
Es la mejor manera de hacerlo para introducir una clase de tipo SomeService:
class SomeService a where
g :: a -> typeOfSomeParms -> gReturnType
data SomeServiceImpl = SomeServiceImpl
data MockSomeServiceImpl = MockSomeServiceImpl
instance SomeService SomeServiceImpl where
g _ someParms = ... -- real implementation of g
instance SomeService MockSomeServiceImpl where
g _ someParms = ... -- mock implementation of g
Luego, redefina f de la siguiente manera:
f someService ... = ...
g someService someParms
...
Parece que esto funcionaría, pero estoy aprendiendo a Haskell y preguntándome si esta es la mejor manera de hacerlo. De manera más general, me gusta la idea de la inyección de dependencia no solo para burlarse, sino también para hacer que el código sea más personalizable y reutilizable. En general, me gusta la idea de no estar encerrado en una sola implementación para ninguno de los servicios que utiliza un fragmento de código. ¿Se consideraría una buena idea usar el truco anterior extensivamente en el código para obtener los beneficios de la inyección de dependencia?
EDITAR:
Avancemos un paso más. Supongamos que tengo una serie de funciones a, b, c, d, eyf en un módulo que necesitan poder hacer referencia a las funciones g, h, i y j desde un módulo diferente. Y supongamos que quiero poder simular las funciones g, h, i y j. Podría pasar las 4 funciones como parámetros a af, pero es un poco molesto agregar los 4 parámetros a todas las funciones. Además, si alguna vez tuviera que cambiar la implementación de cualquiera de af para llamar a otro método más, necesitaría cambiar su firma, lo que podría crear un desagradable ejercicio de refactorización.
¿Algún truco para hacer que este tipo de situación funcione fácilmente? Por ejemplo, en Java, podría construir un objeto con todos sus servicios externos. El constructor almacenaría los servicios en variables miembro. Entonces, cualquiera de los métodos podría acceder a esos servicios a través de las variables miembro. Por lo tanto, a medida que se agregan métodos a los servicios, ninguna de las firmas de método cambia. Y si se necesitan nuevos servicios, solo cambia la firma del método del constructor.
Las pruebas unitarias son para chumps, cuando puede tener pruebas automatizadas basadas en especificación . Puede generar funciones arbitrarias (simuladas) usando la clase de tipo Arbitrary
provista por QuickCheck (el concepto que está buscando es coarbitrario ), y haga que QuickCheck pruebe su función usando tantas funciones "simuladas" como desee.
La "Inyección de Dependencia" es una forma degenerada de paso de parámetros implícitos. En Haskell, puedes usar Reader
o Free
para lograr lo mismo con mucho menos alboroto.
Otra alternativa:
{-# LANGUAGE FlexibleContexts, RankNTypes #-}
import Control.Monad.RWS
data (Monad m) => ServiceImplementation m = ServiceImplementation
{ serviceHello :: m ()
, serviceGetLine :: m String
, servicePutLine :: String -> m ()
}
serviceHelloBase :: (Monad m) => ServiceImplementation m -> m ()
serviceHelloBase impl = do
name <- serviceGetLine impl
servicePutLine impl $ "Hello, " ++ name
realImpl :: ServiceImplementation IO
realImpl = ServiceImplementation
{ serviceHello = serviceHelloBase realImpl
, serviceGetLine = getLine
, servicePutLine = putStrLn
}
mockImpl :: (Monad m, MonadReader String m, MonadWriter String m) =>
ServiceImplementation m
mockImpl = ServiceImplementation
{ serviceHello = serviceHelloBase mockImpl
, serviceGetLine = ask
, servicePutLine = tell
}
main = serviceHello realImpl
test = case runRWS (serviceHello mockImpl) "Dave" () of
(_, _, "Hello, Dave") -> True; _ -> False
Esta es en realidad una de las muchas formas de crear código con estilo OO en Haskell.
Para realizar un seguimiento de la edición preguntando sobre funciones múltiples, una opción es ponerlas en un tipo de registro y pasar el registro. Luego puede agregar nuevas simplemente actualizando el tipo de registro. Por ejemplo:
data FunctionGroup t = FunctionGroup { g :: Int -> Int, h :: t -> Int }
a grp ... = ... g grp someThing ... h grp someThingElse ...
Otra opción que podría ser viable en algunos casos es usar clases de tipo. Por ejemplo:
class HasFunctionGroup t where
g :: Int -> t
h :: t -> Int
a :: HasFunctionGroup t => <some type involving t>
a ... = ... g someThing ... h someThingElse
Esto solo funciona si puedes encontrar un tipo (o múltiples tipos si utilizas clases de tipo multiparámetro) que las funciones tienen en común, pero en los casos en que sea apropiado, te dará un bonito Haskell idiomático.
Podrías tener tus dos implementaciones de funciones con diferentes nombres, g
sería una variable que está definida para ser una u otra según lo necesites.
g :: typeOfSomeParms -> gReturnType
g = g_mock -- change this to "g_real" when you need to
g_mock someParms = ... -- mock implementation of g
g_real someParms = ... -- real implementation of g
Si las funciones de las que depende están en otro módulo, entonces podría jugar juegos con configuraciones de módulos visibles para que se importe el módulo real o el módulo simulado.
Sin embargo, me gustaría preguntar por qué sientes la necesidad de usar funciones simuladas para las pruebas unitarias de todos modos. Simplemente quiere demostrar que el módulo en el que está trabajando hace su trabajo. Primero, compruebe que su módulo de nivel inferior (el que desea simular) funciona, y luego construya su nuevo módulo encima y demuestre que también funciona.
Por supuesto, esto supone que no está trabajando con valores monádicos, por lo que no importa qué se llame o con qué parámetros. En ese caso, probablemente necesite demostrar que los efectos secundarios correctos se están invocando en el momento adecuado, por lo que se debe controlar qué se llama cuando sea necesario.
¿O solo está trabajando con un estándar corporativo que exige que las pruebas unitarias solo ejerzan un solo módulo con el resto del sistema que se burla? Esa es una forma muy pobre de probar. Es mucho mejor construir sus módulos de abajo arriba, demostrando en cada nivel que los módulos cumplen con sus especificaciones antes de pasar al siguiente nivel. Quickcheck es tu amigo aquí.
Una solución simple sería cambiar su
f x = ...
a
f2 g x = ...
y entonces
f = f2 g
ftest = f2 gtest