假设我有一个简单的基于文件的数据库 Monad
。我将其定义如下所示。
newtype MyDbFileBased a = MyDbFileBased {
unMyDbDbFileBased :: ExceptT MyDbFileBasedError (ReaderT MyDbFileBasedEnv IO) a
} deriving (
Functor
, Applicative
, Monad
, MonadError MyDbFileBasedError
, MonadReader MyDbFileBasedEnv
, MonadIO
)
我读到不建议使用上述模式(当我找到它时会发布博客链接),我应该替换 IO
具有通用Monad
就像这样。
import qualified Data.ByteString as B
newtype MyDbFileBased m a = MyDbFileBased {
unMyDbFileBased :: ExceptT MyDbFileBasedError (ReaderT MyDbFileBasedEnv m) a
} deriving (
Functor
, Applicative
, Monad
, MonadError MyDbFileBasedError
, MonadReader MyDbFileBasedEnv
)
class Monad m => MonadFileBasedIO m where
readBytes :: FilePath -> m B.ByteString
writeBytes :: FilePath -> B.ByteString -> m ()
...
instance MonadFileBasedIO IO where
readBytes = B.readFile
writeBytes = B.writeFile
这应该会让单元测试变得更容易。建议在测试中模拟 IO,如下所示。
data MockFS = EmptyDir
| SingleFile FilePath String
deriving (Show)
newtype MockFileBasedIO a = MockFileBasedIO {
unMockFileBasedIO :: State MockFS a
} deriving (
Functor
, Applicative
, Monad
, MonadState MockFS
)
instance MonadFileBasedIO MockFileBasedIO where
readBytes pathReq = do
dir <- get
case dir of
EmptyDir -> fail "file not found"
SingleFile path contents -> if pathReq == path
then pure (BU.fromString contents)
else fail "file not found"
writeBytes path = put . SingleFile path . BU.toString
到目前为止,这一切对我来说看起来都很好。但后来我想添加类似 catch
的内容和liftIO
到MyDbFileBased
内的功能类型。我想添加 catchMonadFileBasedIO
和liftMonadFileBasedIO
功能为MonadFileBasedIO
类型类和设置 catchMonadFileBasedIO = catch
和liftMonadFileBasedIO = liftIO
对于 IO
单子(monad)。但随后它带来了对 MonadIO
的依赖和Exception
类型类,编译器告诉我将这些类型类添加到 catchMonadFileBasedIO
的函数签名中和liftMonadFileBasedIO
。我还需要导出 MonadIO
来自MyDbFileBased m
。那么替换IO
有什么意义呢?具有通用Monad
首先?
我不明白我是否应该 mock IO
无论是否存在此类情况。如何使用liftIO
和catch
如果我们 mock 它?我不应该捕获该模块中的异常并将它们级联到应用程序级别吗?
最佳答案
Then what's the point of replacing
IO
with a genericMonad
in the first place?
作为一般性建议,这可能使您能够替换 IO
测试时用一些纯粹的东西。
Unit tests ought to be deterministic ,这是 pure functions 的两个特征之一。因此,能够用纯函数来描述任何问题使得 intrinsically testable .
为了使一组交互变得纯粹且可测试,您可以例如替换 m
与 State
,并在 State
中运行单元测试单子(monad)。 Here's an example 。这是an example with Writer
.
一般来说,我建议尽可能避免“ mock ”。在面向对象编程中,这可能是启用测试所必需的,但它通常会导致代码难以维护。在函数式编程中,单元测试要容易得多,但它通常要求您以函数式风格设计应用程序的模块。
引入类型类作为与面向对象的接口(interface)或基类等效的东西不太可能导致函数式设计。这将把您拉向一种编程模型,其中(不纯粹的)交互是应用程序架构的中心。这正是面向对象编程如此困难的原因。
在函数式编程中,你的情况会好得多 pushing the impure interactions to the edge of the system 。这将使您能够对(纯)域逻辑进行单元测试,同时 IO 保持具体。
关于unit-testing - 我是否应该模拟 IO?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57683593/