Тестирование функций в Haskell, которые делают IO - PullRequest
33 голосов
/ 10 сентября 2011

Работаем в реальном мире на Haskell прямо сейчас Вот решение для очень раннего упражнения в книге:

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

Мой вопрос: как бы вы протестировали эту функцию? Есть ли способ сделать «фиктивный» ввод вместо того, чтобы фактически взаимодействовать с файловой системой, чтобы проверить его? Haskell делает такой акцент на чистых функциях, что я должен представить, что это легко сделать.

Ответы [ 5 ]

44 голосов
/ 11 сентября 2011

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

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)

Позже вы можете запустить его в IO:

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

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

Полный код можно найти здесь: http://hpaste.org/51210

19 голосов
/ 10 сентября 2011

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

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


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

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

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

9 голосов
/ 10 сентября 2011

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

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

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

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

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

8 голосов
/ 10 сентября 2011

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

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

Так что в этом примере нечего высмеивать и ничего проверять модулем.

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


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

5 голосов
/ 04 февраля 2013

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

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

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

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

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

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

Теперь вы можете проверитьthat numCharactersInFile 'возвращает ожидаемые результаты без ввода-вывода:

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

Наконец, экспортируйте версию вашей исходной сигнатуры функции для использования с IO

numCharactersInFile :: IO Int
numCharactersInFile = NumCharactersInFile' readFile

Итак, в концедня, numCharactersInFile 'можно тестировать с помощью макетов.numCharactersInFile - это всего лишь разновидность numCharactersInFile '.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...