如何在Haskell中模拟测试?

时间:2009-06-11 23:47:24

标签: testing haskell mocking

假设我正在定义一个Haskell函数f(纯函数或动作)并且在f I函数g中调用函数g。例如:

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中,我可以使用其所有外部服务构造一个对象。构造函数会将服务存储在成员变量中。然后,任何方法都可以通过成员变量访问这些服务。因此,随着方法被添加到服务中,方法签名都不会改变。如果需要新服务,则只会更改构造函数方法签名。

7 个答案:

答案 0 :(得分:22)

单元测试适用于chumps,当您可以进行基于规范的自动化测试时。您可以使用QuickCheck提供的Arbitrary类型类生成任意(模拟)函数(您正在寻找的概念是 coobbitrary ),并使用QuickCheck测试您的函数使用尽可能多的“模拟”功能。

“依赖注入”是隐式参数传递的简并形式。在Haskell中,您只需使用ReaderFree即可轻松实现相同的目标。

答案 1 :(得分:16)

另一种选择:

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

这实际上是在Haskell中创建OO样式代码的众多方法之一。

答案 2 :(得分: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。

答案 3 :(得分:3)

你难道不能将名为g的函数传递给f吗?只要g满足接口typeOfSomeParms -> gReturnType,那么您应该能够传入真实函数或模拟函数。

例如

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

我自己没有在Java中使用依赖注入,但是我读过的文本听起来很像传递高阶函数,所以也许这会做你想要的。


对编辑的回应:如果你需要以企业的方式解决问题,ephemient的答案会更好,因为你定义了一个包含多个函数的类型。我提出的原型方法只是传递一个函数元组而不定义包含类型。但后来我几乎没有写过类型注释,所以重构不是很难。

答案 4 :(得分:1)

一个简单的解决方案就是改变你的

f x = ...

f2 g x = ... 

然后

f = f2 g
ftest = f2 gtest

答案 5 :(得分:1)

如果您所依赖的功能位于另一个模块中,那么您可以玩具有可见模块配置的游戏,以便导入真实模块或模拟模块。

但是我想问一下为什么你觉得有必要使用模拟函数进行单元测试。您只想证明您正在处理的模块完成其工作。首先证明你的低级模块(你想要模拟的模块)工作,然后在它上面构建你的新模块,并证明它也有效。

当然,这假设您没有使用monadic值,因此调用什么或使用什么参数无关紧要。在这种情况下,您可能需要证明在正确的时间调用了正确的副作用,因此在必要时监视调用的内容。

或者您是否正在制定一项企业标准,该标准要求单元测试仅运行单个模块,而整个系统的其余部分都被模拟?这是一种非常糟糕的测试方式。从底层向上构建模块要好得多,在每个级别演示模块是否符合其规范,然后再进入下一个级别。 Quickcheck是你的朋友。

答案 6 :(得分: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