Как издеваться над тестированием в Haskell? - PullRequest
34 голосов
/ 12 июня 2009

Предположим, я определяю функцию Haskell f (чистую или как действие), и где-то внутри f я вызываю функцию g. Например:

f = ...
    g someParms
    ...

Как заменить функцию g фиктивной версией для модульного тестирования?

Если бы я работал на Java, g был бы методом класса SomeServiceImpl, который реализует интерфейс SomeService. Затем я использовал бы инъекцию зависимостей, чтобы сказать f, что нужно использовать SomeServiceImpl или MockSomeServiceImpl. Я не уверен, как это сделать в Хаскеле.

Лучший способ сделать это, представить класс типов SomeService:

class SomeService a where
    g :: a -> typeOfSomeParms -> gReturnType

data SomeServiceImpl = SomeServiceImpl
data MockSomeServiceImpl = MockSomeServiceImpl

instance SomeService SomeServiceImpl where
    g _ someParms = ... -- real implementation of g

instance SomeService MockSomeServiceImpl where
    g _ someParms = ... -- mock implementation of g

Затем переопределите f следующим образом:

f someService ... = ...
                    g someService someParms
                    ...

Кажется, это сработает, но я просто изучаю Хаскель и задаюсь вопросом, это лучший способ сделать это? В целом, мне нравится идея внедрения зависимостей не только для насмешек, но и для того, чтобы сделать код более настраиваемым и многократно используемым. Вообще, мне нравится идея не быть привязанным к одной реализации для каких-либо сервисов, которые использует фрагмент кода. Будет ли хорошей идеей широко использовать вышеописанный трюк в коде, чтобы получить преимущества внедрения зависимостей?

EDIT:

Давайте сделаем еще один шаг вперед. Предположим, у меня есть ряд функций a, b, c, d, e и f в модуле, которые все должны иметь возможность ссылаться на функции g, h, i и j из другого модуля. И предположим, я хочу иметь возможность макетировать функции g, h, i и j. Я мог бы четко передать 4 функции в качестве параметров a-f, но добавить 4 параметра ко всем функциям довольно сложно. Кроме того, если мне когда-нибудь понадобится изменить реализацию любого из a-f для вызова еще одного метода, мне нужно будет изменить его сигнатуру, что может создать неприятное упражнение по рефакторингу.

Какие-нибудь хитрости для того, чтобы заставить этот тип ситуации работать легко? Например, в Java я мог бы создать объект со всеми его внешними службами. Конструктор будет хранить сервисы в переменных-членах. Затем любой из методов может получить доступ к этим службам через переменные-члены. Таким образом, по мере добавления методов в службы ни одна из сигнатур методов не изменяется. А если нужны новые сервисы, меняется только сигнатура метода конструктора.

Ответы [ 7 ]

22 голосов
/ 12 июня 2009

Модульное тестирование для чурбанов, когда вы можете иметь Автоматизированное тестирование на основе спецификаций . Вы можете генерировать произвольные (фиктивные) функции, используя класс типов Arbitrary, предоставленный QuickCheck (концепция, которую вы ищете - coarbitrary ), и QuickCheck протестирует вашу функцию, используя столько «фиктивных» функций, сколько вам нужно.

«Внедрение зависимостей» - это вырожденная форма неявной передачи параметров. В Haskell вы можете просто использовать Reader или Free, чтобы достичь того же результата с гораздо меньшим количеством суеты.

16 голосов
/ 12 июня 2009

Другая альтернатива:

{-# LANGUAGE FlexibleContexts, RankNTypes #-}

import Control.Monad.RWS

data (Monad m) => ServiceImplementation m = ServiceImplementation
  { serviceHello :: m ()
  , serviceGetLine :: m String
  , servicePutLine :: String -> m ()
  }

serviceHelloBase :: (Monad m) => ServiceImplementation m -> m ()
serviceHelloBase impl = do
    name <- serviceGetLine impl
    servicePutLine impl $ "Hello, " ++ name

realImpl :: ServiceImplementation IO
realImpl = ServiceImplementation
  { serviceHello = serviceHelloBase realImpl
  , serviceGetLine = getLine
  , servicePutLine = putStrLn
  }

mockImpl :: (Monad m, MonadReader String m, MonadWriter String m) =>
    ServiceImplementation m
mockImpl = ServiceImplementation
  { serviceHello = serviceHelloBase mockImpl
  , serviceGetLine = ask
  , servicePutLine = tell
  }

main = serviceHello realImpl
test = case runRWS (serviceHello mockImpl) "Dave" () of
    (_, _, "Hello, Dave") -> True; _ -> False

На самом деле это один из многих способов создания кода в стиле OO в Haskell.

5 голосов
/ 12 июня 2009

Чтобы продолжить редактирование с запросом о нескольких функциях, можно просто поместить их в тип записи и передать запись. Затем вы можете добавить новые, просто обновив тип записи. Например:

data FunctionGroup t = FunctionGroup { g :: Int -> Int, h :: t -> Int }

a grp ... = ... g grp someThing ... h grp someThingElse ...

Другим вариантом, который может быть жизнеспособным в некоторых случаях, является использование классов типов. Например:

class HasFunctionGroup t where
    g :: Int -> t
    h :: t -> Int

a :: HasFunctionGroup t => <some type involving t>
a ... = ... g someThing ... h someThingElse

Это работает только в том случае, если вы можете найти тип (или несколько типов, если вы используете классы многопараметрических типов), которые имеют общие функции, но в тех случаях, когда это уместно, вы получите хороший идиоматический Haskell.

3 голосов
/ 12 июня 2009

Не могли бы вы просто передать функцию с именем g в f? Пока g удовлетворяет интерфейсу typeOfSomeParms -> gReturnType, вы сможете передавать реальную функцию или фиктивную функцию.

например

f g = do
  ...
  g someParams
  ...

Я сам не использовал внедрение зависимостей в Java, но прочитанные мною тексты напоминают прохождение функций более высокого порядка, поэтому, возможно, это будет делать то, что вы хотите.


Ответ на редактирование: ответ ephemient лучше, если вам нужно решить проблему корпоративным способом, потому что вы определяете тип, содержащий несколько функций. Предлагаемый мной способ создания прототипа будет просто передавать набор функций без определения содержащего типа. Но тогда я почти никогда не пишу аннотации типов, поэтому рефакторинг не очень сложен.

1 голос
/ 13 июня 2009

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

Однако я хотел бы спросить, почему вы все равно чувствуете необходимость использовать фиктивные функции для модульного тестирования. Вы просто хотите продемонстрировать, что модуль, над которым вы работаете, выполняет свою работу. Итак, сначала докажите, что ваш модуль нижнего уровня (тот, который вы хотите смоделировать) работает, а затем создайте новый модуль поверх него и продемонстрируйте, что он тоже работает.

Конечно, это предполагает, что вы не работаете с монадическими значениями, поэтому не имеет значения, что вызывается или с какими параметрами. В этом случае вам, вероятно, нужно продемонстрировать, что правильные побочные эффекты вызываются в нужное время, поэтому необходимо следить за тем, что вызывается, когда это необходимо.

Или вы просто работаете над корпоративным стандартом, который требует, чтобы модульные тесты выполняли только один модуль, а вся остальная часть системы была смоделирована? Это очень плохой способ тестирования. Намного лучше строить свои модули снизу вверх, демонстрируя на каждом уровне, что модули соответствуют их спецификациям, прежде чем переходить на следующий уровень. Quickcheck твой друг здесь.

1 голос
/ 12 июня 2009

Простым решением было бы изменить ваш

f x = ...

до

f2 g x = ... 

, а затем

f = f2 g
ftest = f2 gtest
0 голосов
/ 12 июня 2009

Вы можете просто иметь две реализации функций с разными именами, и g будет переменной, которая определена как одна или другая, как вам нужно.

g :: typeOfSomeParms -> gReturnType
g = g_mock -- change this to "g_real" when you need to

g_mock someParms = ... -- mock implementation of g

g_real someParms = ... -- real implementation of g
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...