如何在Haskell中模拟测试?

Cli*_*ler 37 testing haskell mocking

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

f = ...
    g someParms
    ...
Run Code Online (Sandbox Code Playgroud)

如何用模拟版本替换函数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
Run Code Online (Sandbox Code Playgroud)

然后,重新定义f如下:

f someService ... = ...
                    g someService someParms
                    ...
Run Code Online (Sandbox Code Playgroud)

看起来这样可行,但我只是在学习Haskell并想知道这是否是最好的方法呢?更一般地说,我喜欢依赖注入的想法,不仅仅是为了模拟,而且还使代码更具可定制性和可重用性.通常,我喜欢不被锁定到一段代码使用的任何服务的单个实现中的想法.在代码中广泛使用上述技巧以获得依赖注入的好处是否被认为是一个好主意?

编辑:

让我们更进一步.假设我在一个模块中有一系列函数a,b,c,d,e和f,它们都需要能够从不同的模块引用函数g,h,i和j.并且假设我想能够模拟函数g,h,i和j.我可以清楚地将4个函数作为参数传递给af,但是将4个参数添加到所有函数中会有点痛苦.另外,如果我需要更改任何af的实现以调用另一种方法,我需要更改其签名,这可能会产生令人讨厌的重构练习.

使这种情况变得容易的任何技巧?例如,在Java中,我可以使用其所有外部服务构造一个对象.构造函数会将服务存储在成员变量中.然后,任何方法都可以通过成员变量访问这些服务.因此,随着方法被添加到服务中,方法签名都不会改变.如果需要新服务,则只会更改构造函数方法签名.

Apo*_*isp 22

当您可以进行基于规格的自动化测试时,单元测试适用于笨蛋.您可以使用任意生成(模拟)功能Arbitrary提供类型级快速检查(你正在寻找的概念是coarbitrary),并具有快速检查,只要你喜欢用尽可能多的"模拟"功能测试功能.

"依赖注入"是隐式参数传递的简并形式.在Haskell中,您可以使用Reader,或者Free以更少的大惊小怪来实现相同的功能.

  • 与问题不同的是,这个答案缺乏任何代码示例,并且有点“monad 只是 endofunctors 类别中的幺半群”的味道,即答案对 Haskell 内部人员来说是可读的,而问题显然来自具有 Java 背景的人。这个答案一言以蔽之:“你的方法不好,去学习好方法,我不再多说了”。 (10认同)
  • `"依赖注入"是隐式参数传递的简并形式 (9认同)

eph*_*ent 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
Run Code Online (Sandbox Code Playgroud)

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


GS *_*ica 5

要跟进编辑询问多个函数,一个选项是将它们放入记录类型并传入记录.然后您可以通过更新记录类型来添加新的.例如:

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

a grp ... = ... g grp someThing ... h grp someThingElse ...
Run Code Online (Sandbox Code Playgroud)

在某些情况下可能可行的另一种选择是使用类型类.例如:

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

a :: HasFunctionGroup t => <some type involving t>
a ... = ... g someThing ... h someThingElse
Run Code Online (Sandbox Code Playgroud)

这只有在你能找到函数共有的类型(或多种类型,如果你使用多参数类型类)时才有效,但是在适当的情况下它会给你很好的惯用Haskell.