Подтвердить что ты не робот

Функции тестирования в Haskell, которые выполняют 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 уделяет особое внимание чистым функциям, которые я должен себе представить, что это легко сделать.

4b9b3361

Ответ 1

Как уже указывал Александр Полуэктов, код, который вы пытаетесь проверить, можно легко разделить на чистую и нечистую часть. Тем не менее, я думаю, что хорошо знать, как тестировать такие нечистые функции в 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.

Конечно, вы также можете проверить некоторые другие свойства в зависимости от вашего использования.


* Обратите внимание на мое использование термина макет поведения:
Термин "макет в объектно-ориентированном смысле", возможно, не самый лучший здесь. Но каково намерение заманить насчет? Это позволяет вам проверять код, который нуждается в доступе к ресурсу, обычно

  • либо не доступно во время тестирования
  • или не легко контролируется и, следовательно, нелегко проверить.

Перемещая ответственность за предоставление такого ресурса для быстрой проверки, неожиданно становится возможным обеспечить среду для тестируемого кода, которая может быть проверена после пробного запуска.
Мартин Фаулер прекрасно описывает это в статье о mocks:
"Mocks - это... объекты, предварительно запрограммированные ожиданиями, которые формируют спецификацию вызовов, которые они должны получать".
Для установки быстрой проверки я бы сказал, что файлы, созданные в качестве входных данных, "запрограммированы заранее", так что мы знаем об их размере (== ожидание). И тогда они проверяются против нашей спецификации (== свойство).

Ответ 2

Вы можете сделать свой код тестируемым, используя переменную типа с ограниченным классом типов вместо ввода-вывода.

Во-первых, давайте уберем импорт.

{-# 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

Таким образом, ваш тестируемый код не требует особых изменений.

Ответ 3

Для этого вам нужно будет изменить функцию так, чтобы она была:

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

Теперь вы можете передать любую функцию mock, которая принимает путь к файлу и возвращает строку ввода-вывода, такую ​​как:

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

и передать эту функцию в numCharactersInFile.

Ответ 4

Функция состоит из двух частей: нечистая (чтение части содержимого как String) и чистая (вычисление длины строки).

Нечистая часть не может быть "единицей" - проверена по определению. Чистая часть - это просто вызов функции библиотеки (и, конечно, вы можете проверить ее, если хотите:)).

Итак, в этом примере нечего насмехаться и ничего не делать.

Положите это по-другому. Учтите, что у вас есть эквивалентная реализация на С++ или Java (*): чтение содержимого, а затем вычисление его длины. Что бы вы действительно хотели высмеять и что осталось после тестирования?


(*), что, конечно же, не так, как вы будете делать на С++ или Java, но это оффтоп.

Ответ 5

Основываясь на моем неспециалистском понимании Хаскелла, я пришел к следующим выводам:

  • Если функция использует монаду IO, макетирование будет невозможно. Избегайте жесткого кодирования монады IO в вашей функции.

  • Сделайте вспомогательную версию своей функции, которая принимает другие функции, которые могут выполнять IO. Результат будет выглядеть следующим образом:

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

numCharactersInFile' теперь можно проверить с помощью mocks!

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 'тестируется с помощью mocks. numCharactersInFile - это просто вариант numCharactersInFile '.