2009-06-11 12 views
32

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:¿Cómo se burla de las pruebas en Haskell?

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. Entonces, 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 a introducir un tipo de clase 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 

Entonces, redefinir f de la siguiente manera:

f someService ... = ... 
        g someService someParms 
        ... 

Parece que esto funcionaría, pero estoy Acabo de enterarme de Haskell y me pregunto 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?

EDIT:

Tomemos un paso más allá. 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 a-f, 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 alguno de los a-f 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.

+3

¿por qué uno querría burlarse de las funciones puras? – yairchu

+0

Buen punto, yairchu. Probablemente solo te burles de las acciones. –

+3

@yairchu Lo haría por razones de eficiencia. Si las pruebas toman x o 100 veces el tiempo es muy importante para la productividad. Así que me gustaría decir que factorial 100000000 = para fines de prueba (por lo tanto, como entrada/simulacro de otra prueba). Pero ese factorial 100000000 = podría ser una prueba en sí misma cuando el corredor de prueba se ejecuta en el módulo factorial. – user239558

Respuesta

20

La prueba de la unidad es para chumps, cuando puede tener Pruebas automatizadas basadas en especificación. Puede generar funciones arbitrarias (simuladas) utilizando la clase de tipo Arbitrary proporcionada por QuickCheck (el concepto que está buscando es coarbitrary), y haga que QuickCheck pruebe su función utilizando tantas funciones "simuladas" como desee.

"Inyección de Dependencia" es una forma degenerada de paso de parámetros implícitos. En Haskell, puede usar Reader, o Free para lograr lo mismo con mucho menos alboroto.

+4

'" Dependency Injection "es una forma degenerada de paso de parámetros implícitos. –

1

Una solución sencilla sería la de cambiar su

f x = ... 

a

f2 g x = ... 

y luego

f = f2 g 
ftest = f2 gtest 
+1

Se vuelve más "divertido" cuando se espera que g sea capaz de invocar f recursivamente, pero eso se puede solucionar tanto en esta como en la solución basada en la clase. – ephemient

+0

Eso tiene sentido. Supongo que si solo estoy usando una función de un servicio (alguna agrupación lógica de funciones), pasar la función como parámetro es más fácil. Pero si estoy usando varias funciones, construir una clase reduciría la cantidad de parámetros. –

3

No se pudo que acaba de pasar a una función llamada a gf? Siempre que g satisfaga la interfaz typeOfSomeParms -> gReturnType, entonces debería poder pasar la función real o una función simulada.

por ejemplo

f g = do 
    ... 
    g someParams 
    ... 

No he utilizado la inyección de dependencia en Java a mí mismo, pero los textos que he leído hacía sonido muy parecido a paso las funciones de orden superior, así que tal vez esto va a hacer lo que quiere.


Respuesta a editar: La respuesta de ephemient es mejor si se necesita para resolver el problema de una manera enterprisey, porque se 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.

+1

¿Se supone que "empresarial" es un cumplido? ;) – ephemient

0

Podrías tener tus dos implementaciones de funciones con diferentes nombres, y 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 
+1

La desventaja de este enfoque es que tengo que cambiar mi código fuente para alternar g de ida y vuelta entre g_mock y g_real cada vez que ejecuto mis pruebas o mi producto real. –

15

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 

Esto es en realidad una de las muchas maneras de crear código OO-labrado en Haskell.

+0

Esto es realmente bueno: las ejecuciones principales en IO() pero la prueba es una función pura. Las extensiones del compilador me asustan un poco, ¿y qué es Control.Monad.RWS? Google no es muy útil. Parece que tengo más aprendizaje que hacer ... – minimalis

+0

@minimalis 'Control.Monad.RWS' define las clases de mónada' Reader', 'Writer' y' State'. – ephemient

+0

Gracias, eso tiene sentido ahora. También parece que no necesita las extensiones de idioma si cambia el tipo de mockImpl a 'mockImpl :: ServiceImplementation (RWS String String())' – minimalis

5

Para realizar un seguimiento de la edición preguntando sobre funciones múltiples, una opción es simplemente 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 utilizar clases de tipos. 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 sólo funciona si se puede encontrar un tipo (o tipos múltiples si se utiliza el tipo clases de múltiples parámetros) que las funciones que tienen en común, pero en los casos en los que conviene que le dará agradable idiomático Haskell.

+0

¿Cuál preferirías? – David

+0

Si hay un tipo natural para adjuntarlo, preferiría la clase de tipo. De lo contrario, probablemente el registro. –

1

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 utilizar las funciones simuladas para la prueba unitaria 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 llama 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 para 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í.

Cuestiones relacionadas