2009-06-11 5 views
32

Supponiamo che sto definendo una funzione Haskell f (sia pura che un'azione) e da qualche parte all'interno di f chiamo la funzione g. Ad esempio:Come fingere di provare in Haskell?

f = ... 
    g someParms 
    ... 

Come si sostituisce la funzione g con una versione fittizia per il test dell'unità?

Se stavo lavorando in Java, g sarebbe un metodo sulla classe SomeServiceImpl che implementa l'interfaccia SomeService. Quindi, userei l'iniezione di dipendenza per dire a f o usare SomeServiceImpl o MockSomeServiceImpl. Non sono sicuro di come farlo in Haskell.

è il modo migliore per farlo di introdurre un tipo di classe 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 

Poi, ridefinire F come segue:

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

Sembra che questo dovrebbe funzionare, ma sono semplicemente imparando Haskell e chiedendosi se questo è il modo migliore per farlo? Più in generale, mi piace l'idea dell'iniezione di dipendenza non solo per il derisione, ma anche per rendere il codice più personalizzabile e riutilizzabile. In generale, mi piace l'idea di non essere bloccato in una singola implementazione per nessuno dei servizi utilizzati da un pezzo di codice. Sarebbe considerata una buona idea usare il trucco sopra in modo estensivo nel codice per ottenere i benefici dell'iniezione di dipendenza?

EDIT:

Prendiamo questo un ulteriore passo avanti. Supponiamo che io abbia una serie di funzioni a, b, c, d, e ed f in un modulo che tutti devono essere in grado di fare riferimento alle funzioni g, h, i e j da un modulo diverso. E supponiamo di voler essere in grado di simulare le funzioni g, h, i e j. Potrei chiaramente passare le 4 funzioni come parametri ad a-f, ma è un po 'difficile aggiungere i 4 parametri a tutte le funzioni. Inoltre, se avessi mai avuto bisogno di cambiare l'implementazione di uno qualsiasi di a-f per chiamare ancora un altro metodo, avrei bisogno di cambiare la sua firma, che potrebbe creare un brutto esercizio di refactoring.

Eventuali trucchi per rendere questo tipo di situazione funzionano facilmente? Ad esempio, in Java, potrei costruire un oggetto con tutti i suoi servizi esterni. Il costruttore memorizzerebbe i servizi nelle variabili membro. Quindi, qualsiasi metodo potrebbe accedere a tali servizi tramite le variabili membro. Quindi, man mano che i metodi vengono aggiunti ai servizi, nessuna delle firme dei metodi cambia. E se sono necessari nuovi servizi, cambia solo la firma del metodo del costruttore.

+3

perché uno vorrebbe prendere in giro le funzioni pure? – yairchu

+0

Buon punto, yairchu. Probabilmente potresti solo prendere in giro le azioni. –

+3

@yairchu Lo farei per motivi di efficienza. Se i test prendono x o 100 volte il tempo è estremamente importante per la produttività. Quindi vorrei dire che factorial 100000000 = a scopo di test (quindi come input/mock a un altro test). Ma quel fattoriale 100000000 = potrebbe essere un test di per sé quando il test runner gira sul modulo fattoriale. – user239558

risposta

20

Il test dell'unità è per i blocchi, quando è possibile avere Test basato su specifica automatica. Puoi generare funzioni arbitrarie (simulate) usando la classe di caratteri Arbitrary fornita da QuickCheck (il concetto che stai cercando è coarbitrario), e verifica QuickCheck della tua funzione utilizzando tutte le funzioni "simulate" che desideri.

"Iniezione di dipendenza" è una forma degenerata di passaggio di parametro implicito. In Haskell, puoi semplicemente usare Reader o Free per ottenere lo stesso risultato con molto meno clamore.

+4

'" Iniezione delle dipendenze "è una forma degenerata di passaggio dei parametri implicito. –

1

Una soluzione semplice sarebbe quella di cambiare il vostro

f x = ... 

a

f2 g x = ... 

e poi

f = f2 g 
ftest = f2 gtest 
+1

Diventa più "divertente" quando ci si aspetta che g possa chiamare f ricorsivamente, ma ciò è risolvibile sia in questa che nella soluzione basata su classi. – ephemient

+0

Questo ha senso. Suppongo che se sto usando solo una funzione da un servizio (qualche raggruppamento logico di funzioni), allora il passaggio della funzione come parametro è più semplice. Ma se sto usando diverse funzioni, la costruzione di una classe ridurrebbe il numero di parametri. –

3

Non potevi semplicemente passare una funzione denominata g-f? Finché g soddisfa l'interfaccia typeOfSomeParms -> gReturnType, allora dovresti essere in grado di passare la funzione reale o una funzione fittizia.

esempio

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

Non ho usato l'iniezione di dipendenza in Java me stesso, ma i testi che ho letto reso il suono un po 'come passare funzioni di ordine superiore, così forse questo farà quello che vuoi.


Risposta da modificare: la risposta di ephemient è meglio se avete bisogno di risolvere il problema in modo enterprisey, perché si definisce un tipo che contiene molteplici funzioni. La modalità di prototipazione che propongo passerebbe semplicemente una tupla di funzioni senza definire un tipo di contenuto. Ma poi non scrivo quasi mai annotazioni di tipo, quindi il refactoring non è molto difficile.

+1

"Enterprisey" dovrebbe essere un complimento? ;) – ephemient

0

Si potrebbe semplicemente avere le due implementazioni di funzioni con nomi diversi, e g sarebbe una variabile che può essere definita come l'una o l'altra di cui si ha bisogno.

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

Lo svantaggio di questo approccio è che devo cambiare il mio codice sorgente per alternare g avanti e indietro tra g_mock e g_real ogni volta che eseguo i miei test o il mio prodotto reale. –

15

Un'altra 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 

Questo è in realtà uno dei tanti modi per creare il codice OO-disegnata di Haskell.

+0

Questo è veramente bello - le esecuzioni principali in IO() ma test è una funzione pura. Le estensioni del compilatore mi spaventano un po ', e che cos'è Control.Monad.RWS? Google non è molto utile. Sembra che abbia ancora più da imparare ... – minimalis

+0

@minimalis 'Control.Monad.RWS' definisce le classi monade' Reader', 'Writer' e' State'. – ephemient

+0

Grazie, ora ha senso. Sembra anche che tu non abbia bisogno delle estensioni della lingua se cambi il tipo di mockImpl in 'mockImpl :: ServiceImplementation (RWS String String())' – minimalis

5

Per seguire la modifica che richiede più funzioni, un'opzione è quella di inserirle in un tipo di record e passare il record. Quindi è possibile aggiungerne di nuove semplicemente aggiornando il tipo di record. Ad esempio:

data FunctionGroup t = FunctionGroup { g :: Int -> Int, h :: t -> Int } 

a grp ... = ... g grp someThing ... h grp someThingElse ... 

Un'altra opzione che potrebbe essere valida in alcuni casi è l'uso di classi di tipi. Per esempio:

class HasFunctionGroup t where 
    g :: Int -> t 
    h :: t -> Int 

a :: HasFunctionGroup t => <some type involving t> 
a ... = ... g someThing ... h someThingElse 

Questo funziona solo se si può trovare un tipo (o tipi multipli se utilizzare le classi di tipo multi-parametro) che le funzioni hanno in comune, ma nei casi in cui è opportuno che vi darà bel idiomatico Haskell.

+0

Quale preferiresti? – David

+0

Se c'è un tipo naturale a cui collegarlo, preferirei la classe del tipo. Altrimenti, probabilmente il record. –

1

Se le funzioni dipendono da un altro modulo, è possibile giocare con configurazioni di moduli visibili in modo da importare il modulo reale o un modulo fittizio.

Tuttavia mi piacerebbe chiedere perché si sente la necessità di utilizzare le funzioni di simulazione per il test delle unità comunque. Vuoi semplicemente dimostrare che il modulo su cui stai lavorando fa il suo lavoro. Quindi, prima prova che il tuo modulo di livello inferiore (quello che vuoi deridere) funzioni, quindi costruisci il tuo nuovo modulo su di esso e dimostri che funziona anche lui.

Naturalmente questo presuppone che non si stia lavorando con valori monadici, quindi non importa ciò che viene chiamato o con quali parametri.In tal caso, probabilmente è necessario dimostrare che gli effetti collaterali giusti vengono invocati al momento giusto, in modo da monitorare ciò che viene chiamato quando è necessario.

O stai semplicemente lavorando ad uno standard aziendale che richiede che i test di unità esercitino un solo modulo con tutto il resto del sistema che viene deriso? Questo è un modo molto povero di test. Molto meglio costruire i moduli dal basso verso l'alto, dimostrando ad ogni livello che i moduli soddisfano le loro specifiche prima di passare al livello successivo. Quickcheck è tuo amico qui.