unit-testing - 我是否应该模拟 IO?

标签 unit-testing haskell exception

假设我有一个简单的基于文件的数据库 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 的内容和liftIOMyDbFileBased内的功能类型。我想添加 catchMonadFileBasedIOliftMonadFileBasedIO功能为MonadFileBasedIO类型类和设置 catchMonadFileBasedIO = catchliftMonadFileBasedIO = liftIO对于 IO单子(monad)。但随后它带来了对 MonadIO 的依赖和Exception类型类,编译器告诉我将这些类型类添加到 catchMonadFileBasedIO 的函数签名中和liftMonadFileBasedIO 。我还需要导出 MonadIO来自MyDbFileBased m 。那么替换IO有什么意义呢?具有通用Monad首先?

我不明白我是否应该 mock IO无论是否存在此类情况。如何使用liftIOcatch如果我们 mock 它?我不应该捕获该模块中的异常并将它们级联到应用程序级别吗?

最佳答案

Then what's the point of replacing IO with a generic Monad in the first place?

作为一般性建议,这可能使您能够替换 IO测试时用一些纯粹的东西。

Unit tests ought to be deterministic ,这是 pure functions 的两个特征之一。因此,能够用纯函数来描述任何问题使得 intrinsically testable .

为了使一组交互变得纯粹且可测试,您可以例如替换 mState ,并在 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/

相关文章:

java - 在 Play Framework 2 上进行单元测试时出现错误

php - 对网站进行单元测试

python:如何测试 @unittest.expectedFailure 的特定类型的错误?

java数组泛型初始化

Haskell 关系错误 - 声明中的语法错误(意外的 `;' ,可能是由于布局错误)

haskell - Yesod 条件子网站

java - 从 Java 执行 Rscript

java - 如何在 TestNG 上的 @BeforeMethod 中添加分组和依赖关系

ruby-on-rails - 使用 Ruby 临时设置系统时间以进行单元测试

algorithm - 可证明正确的排列小于 O(n^2)