繁体   English   中英

如何在Haskell中进行模拟测试?

[英]How to mock for testing in Haskell?

假设我正在定义一个Haskell函数f(无论是纯函数还是动作),并且在f内的某个地方调用函数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个函数作为参数传递给af,但是要将4个参数添加到所有函数中有点麻烦。 另外,如果我曾经需要更改af的实现来调用另一种方法,则需要更改其签名,这可能会造成麻烦的重构。

有什么技巧可以使这种情况轻松实现? 例如,在Java中,我可以使用其所有外部服务构造一个对象。 构造函数会将服务存储在成员变量中。 然后,任何方法都可以通过成员变量访问那些服务。 因此,将方法添加到服务后,方法签名都不会更改。 而且,如果需要新服务,则仅构造函数方法签名会更改。

可以进行基于规范的自动化测试时,为什么还要使用单元测试 QuickCheck库为您完成此任务。 它可以使用Arbitrary类型类生成任意(模拟)函数和数据。

“依赖注入”是隐式参数传递的简并形式。 在Haskell中,您可以使用ReaderFree以更Haskelly的方式实现相同的目的。

另一种选择:

{-# 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样式的代码的众多方法之一。

要跟进有关多种功能的编辑,一种选择是将它们置于记录类型中并传递记录。然后,只需更新记录类型即可添加新功能。 例如:

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。

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

例如

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

我自己没有在Java中使用依赖项注入,但是我读过的文字听起来很像传递高阶函数,所以也许这可以满足您的要求。


对编辑的响应:如果您需要以一种企业的方式解决问题,那么短暂的答案会更好,因为您定义了一个包含多个功能的类型。 我提议的原型制作方法只是传递一个函数元组,而无需定义包含类型。 但是后来我几乎没有写过类型注释,因此重构并不是很难。

一个简单的解决方案是更改您的

f x = ...

f2 g x = ... 

然后

f = f2 g
ftest = f2 gtest

如果您依赖的功能在另一个模块中,那么您可以使用可见的模块配置来玩游戏,以便导入真实模块或模拟模块。

但是我想问一个问题,为什么您仍然需要使用模拟函数进行单元测试。 您只想证明您正在处理的模块已完成工作。 因此,首先要证明您的较低级别的模块(您要模拟的模块)可以正常工作,然后在该模块之上构建新模块并演示该模块也可以工作。

当然,这假定您未使用单子值,因此调用什么或使用什么参数都没有关系。 在那种情况下,您可能需要证明在正确的时间调用了正确的副作用,因此必须监视何时需要调用什么。

还是只是按照公司的标准工作,即要求单元测试仅使用单个模块,而要模拟整个系统的其余部分? 那是一种非常糟糕的测试方法。 最好从下至上构建模块,在进入下一级别之前,在每个级别上证明模块均符合其规格。 Quickcheck是您的朋友在这里。

您可以使用不同的名称来实现两个函数实现,而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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM