簡體   English   中英

在Haskell中測試執行IO的功能

[英]Testing functions in Haskell that do IO

現在就通過Real World Haskell工作。 這是本書中非常早的練習的一種解決方案:

-- | 4) Counts the number of characters in a file
numCharactersInFile :: FilePath -> IO Int
numCharactersInFile fileName = do
    contents <- readFile fileName
    return (length contents)

我的問題是:您將如何測試此功能? 有沒有一種方法可以進行“模擬”輸入,而不需要實際與文件系統進行交互以進行測試? Haskell如此強調純函數,以至於我不得不想象這很容易做到。

您可以使用類型類約束的類型變量而不是IO來使代碼可測試。

首先,讓我們取消導入。

{-# LANGUAGE FlexibleInstances #-}
import qualified Prelude
import Prelude hiding(readFile)
import Control.Monad.State

我們要測試的代碼:

class Monad m => FSMonad m where
    readFile :: FilePath -> m String

-- | 4) Counts the number of characters in a file
numCharactersInFile :: FSMonad m => FilePath -> m Int
numCharactersInFile fileName = do
    contents <- readFile fileName
    return (length contents)

稍后,我們可以運行它:

instance FSMonad IO where
    readFile = Prelude.readFile

並對其進行測試:

data MockFS = SingleFile FilePath String

instance FSMonad (State MockFS) where 
               -- ^ Reader would be enough in this particular case though
    readFile pathRequested = do
        (SingleFile pathExisting contents) <- get
        if pathExisting == pathRequested
            then return contents
            else fail "file not found"


testNumCharactersInFile :: Bool
testNumCharactersInFile =
    evalState
        (numCharactersInFile "test.txt") 
        (SingleFile "test.txt" "hello world")
      == 11

這樣,您的受測代碼幾乎不需要修改。

正如Alexander Poluektov所指出的那樣,您要測試的代碼可以很容易地分為純部分和不純部分。 盡管如此,我認為知道如何在haskell中測試這種不純函數是一件很不錯的事情。
在haskell中進行測試的常用方法是使用quickcheck ,這也是我不純代碼傾向於使用的方法。

這是一個示例,說明如何實現自己想做的事情,從而給您一種模擬行為*

import Test.QuickCheck
import Test.QuickCheck.Monadic(monadicIO,run,assert)
import System.Directory(removeFile,getTemporaryDirectory)
import System.IO
import Control.Exception(finally,bracket)

numCharactersInFile :: FilePath -> IO Int
numCharactersInFile fileName = do
    contents <- readFile fileName
    return (length contents)

現在提供替代功能針對模型進行測試)

numAlternative ::  FilePath -> IO Integer
numAlternative p = bracket (openFile p ReadMode) hClose hFileSize

提供測試環境的任意實例:

data TestFile = TestFile String deriving (Eq,Ord,Show)
instance Arbitrary TestFile where
  arbitrary = do
    n <- choose (0,2000)
    testString <- vectorOf n $ elements ['a'..'z'] 
    return $ TestFile testString

針對模型進行屬性測試( 對單子代碼使用quickcheck ):

prop_charsInFile (TestFile string) = 
  length string > 0 ==> monadicIO $ do
    (res,alternative) <- run $ createTmpFile string $
      \p h -> do
          alternative <- numAlternative p
          testRes <- numCharactersInFile p
          return (testRes,alternative)
    assert $ res == fromInteger alternative

和一些輔助功能:

createTmpFile :: String -> (FilePath -> Handle -> IO a) -> IO a
createTmpFile content func = do
      tempdir <- catch getTemporaryDirectory (\_ -> return ".")
      (tempfile, temph) <- openTempFile tempdir ""
      hPutStr temph content
      hFlush temph
      hClose temph
      finally (func tempfile temph) 
              (removeFile tempfile)

這將使quickCheck為您創建一些隨機文件,並針對模型函數測試實現。

$ quickCheck prop_charsInFile 
+++ OK, passed 100 tests.

當然,您還可以根據用例測試其他一些屬性。


*注意我對術語模擬行為的用法:
面向對象意義上的模擬一詞在這里可能不是最好的。 但是模擬的目的是什么?
它讓您測試需要訪問通常需要訪問資源的代碼。

  • 在測試時都不可用
  • 或不容易控制,因此不容易驗證。

通過將提供這種資源的責任轉移到快速檢查中,突然變得可以為被測代碼提供一個可以在測試運行后進行驗證的環境。
Martin Fowler在有關模擬文章中對此進行了很好的描述:
“ Mo子是……用期望進行預編程的對象,這些對象構成了期望接收的呼叫的規范。”
對於快速檢查設置,我想說作為輸入生成的文件是“預編程的”,這樣我們就知道它們的大小(==期望值)。 然后根據我們的規范(==屬性)對它們進行驗證。

為此,您將需要修改函數,使其變為:

numCharactersInFile :: (FilePath -> IO String) -> FilePath -> IO Int
numCharactersInFile reader fileName = do
                         contents <- reader fileName
                         return (length contents)

現在,您可以傳遞任何采用文件路徑並返回IO字符串的模擬函數,例如:

fakeFile :: FilePath -> IO String
fakeFile fileName = return "Fake content"

並將此函數傳遞給numCharactersInFile

該函數由兩部分組成:不純的(以String形式讀取零件內容)和純的(計算String的長度)。

不純部分不能通過定義進行“單元”測試。 純粹的部分只是調用庫函數(當然,如果需要,您也可以測試它:))。

因此,在此示例中,無需模擬,也無需進行單元測試。

換句話說。 考慮您具有相等的C ++或Java實現(*):讀取內容,然后計算其長度。 您真正想要模擬的是什么,之后還需要進行測試?


(*)當然不是您將在C ++或Java中執行的方式,但這是題外話。

根據外行對Haskell的理解,我得出以下結論:

  1. 如果某個函數使用IO monad,則將無法進行模擬測試。 避免在函數中對IO monad進行硬編碼。

  2. 為您的函數創建一個幫助程序版本,其中包含可以執行IO的其他功能。 結果將如下所示:

 numCharactersInFile' :: Monad m => (FilePath -> m String) -> FilePath -> m Int numCharactersInFile' f filePath = do contents <- f filePath return (length contents) 

numCharactersInFile'現在可以通過numCharactersInFile'測試了!

mockFileSystem :: FilePath -> Identity String
mockFileSystem "fileName" = return "mock file contents"

現在,您可以驗證numCharactersInFile'是否返回沒有IO的預期結果:

18 == (runIdentity .  numCharactersInFile' mockFileSystem $ "fileName")

最后,導出原始功能簽名的版本以用於IO

numCharactersInFile :: IO Int
numCharactersInFile = NumCharactersInFile' readFile

因此,最終,numCharactersInFile'可以通過模擬進行測試。 numCharactersInFile只是numCharactersInFile'的變體。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM