2009-06-11 136 views
32

假設我定義了一個Haskell函數f(純函數或者動作),並且在f中的某個地方調用函數g。例如:如何模擬在Haskell中測試?

f = ... 
    g someParms 
    ... 

如何用模擬版本替換函數g進行單元測試?

如果我在Java中工作,g將是類SomeServiceImpl上的一個方法,它實現接口SomeService。然後,我會使用依賴注入來告訴f使用SomeServiceImplMockSomeServiceImpl。我不知道如何在Haskell中做到這一點。

是做引進型類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 

然後,重新定義˚F如下:

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

看起來這會的工作,但我剛剛學習Haskell並想知道這是否是最好的方法?更一般地說,我喜歡依賴注入的想法,不僅僅是爲了嘲笑,而且是爲了使代碼更加可定製和可重用。一般來說,我喜歡這樣的想法:不被鎖定到一段代碼使用的任何服務的單個實現中。在代碼中廣泛使用上述技巧來獲得依賴注入的好處是否被認爲是一個好主意?

編輯:

讓我們再走一步。假設我在模塊中有一系列函數a,b,c,d,e和f,它們都需要能夠從不同模塊中引用函數g,h,i和j。假設我希望能夠模擬g,h,i和j的函數。我可以清楚地將4個函數作爲參數傳遞給a-f,但是將4個參數添加到所有函數中有點痛苦。另外,如果我需要改變任何a-f的實現來調用另一個方法,我需要改變它的簽名,這可能會產生一個令人討厭的重構練習。

任何使這種情況輕鬆工作的技巧?例如,在Java中,我可以構建一個包含所有外部服務的對象。構造函數會將這些服務存儲在成員變量中。然後,任何方法都可以通過成員變量訪問這些服務。因此,隨着方法被添加到服務中,方法簽名都不會改變。如果需要新的服務,只有構造函數方法簽名發生變化。

+3

爲什麼要嘲諷純粹的功能? – yairchu 2009-06-13 16:16:59

+0

好點,yairchu。你可能只會嘲笑行動。 – 2009-06-13 18:32:20

+3

@yairchu我會這樣做是出於效率的原因。無論測試需要x或100倍的時間對生產力都非常重要。所以我想說一個因子100000000 = <一些大數>用於測試目的(因此作爲輸入/模擬到另一個測試)。但是當測試運行器運行在階乘模塊上時,該階乘100000000 = <一些大數>本身可能是一個測試。 – user239558 2010-10-28 22:04:52

回答

20

單元測試是針對碎塊的,當您可以有基於規範的自動測試。您可以使用QuickCheck(你正在尋找的概念是coarbitrary)提供的Arbitrary型類產生任意(模擬)功能,並具有快速檢查,只要你喜歡用盡可能多的「模擬」功能測試功能。

「依賴注入」是一種隱式參數傳遞的退化形式。在Haskell中,您可以使用ReaderFree來實現同樣的事情,而不用大驚小怪。

1

一個簡單的解決辦法是改變你的

f x = ... 

f2 g x = ... 

然後

f = f2 g 
ftest = f2 gtest 
+1

當g被期望能夠遞歸地調用時,它變得更「有趣」,但這在這個和基於類的解決方案中都是可以解決的。 – ephemient 2009-06-12 00:09:04

+0

這很有道理。我想如果我只是使用一個服務中的函數(某些函數的邏輯分組),那麼將函數作爲參數傳遞是最簡單的。但是如果我使用了幾個函數,那麼構建一個類將會減少參數的數量。 – 2009-06-12 00:14:05

3

你不能只是通過一個名爲gf功能?只要g滿足接口typeOfSomeParms -> gReturnType,那麼你應該能夠傳遞真實函數或模擬函數。

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

我沒有在Java中使用依賴注入自己,但我已閱讀文本使它聽起來很像傳遞高階函數,所以也許這會做你想要什麼。


響應編輯:ephemient的答案是更好,如果您需要解決在enterprisey方式的問題,因爲你定義一個包含多種功能的類型。我提出的原型方法只是傳遞一個函數元組而不定義一個包含類型。但是,我幾乎沒有寫過類型註釋,所以重構並不是很難。

0

你可以只是你的兩個功能實現不同的名字,並g將是要麼定義爲一個或因爲你需要其他的變量。

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 
15

另一種選擇:

{-# 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 

這實際上是衆多方法中哈斯克爾創建OO風格的代碼之一。

5

要跟進編輯詢問多重功能,一個選擇是僅把它們放在一個記錄類型,並通過在記錄中。然後,你可以通過更新記錄類型添加新的。例如:

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

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

在某些情況下可能存在的另一種選擇是使用類型類。例如:

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

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

如果你能找到一種(或多種類型,如果您使用的多參數類型的類),該功能有相同之處,但在情況下,它是適當的,它會給你這隻作品好習慣Haskell。

1

如果你依賴於功能在另一個模塊,那麼你可以玩,以使這些真正的模塊或模擬模塊導入可見模塊配置的遊戲。

不過我想問一下爲什麼你覺得無論如何都需要使用模擬函數進行單元測試。你只是想證明你正在開發的模塊能夠完成它的工作。所以首先證明你的低級模塊(你想模擬的那個)工作,然後在它上面建立你的新模塊,並證明它也可以工作。

當然,這是假定你沒有使用monadic值,所以調用什麼或使用什麼參數並不重要。在這種情況下,您可能需要證明在正確的時間正確調用了正確的副作用,因此需要監視何時被調用。

或者你是否正在努力達到一個企業標準,要求單元測試只對整個系統的其餘部分進行模擬時使用單個模塊?這是一種非常糟糕的測試方式。最好從底層開始構建模塊,在每個級別上展示模塊在進入下一個級別之前符合規格。 Quickcheck是你的朋友。