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

Как насмехаться над тестированием в Haskell?

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

f = ...
    g someParms
    ...

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

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

Это лучший способ сделать это, чтобы ввести класс типа 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
                    ...

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

EDIT:

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

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

4b9b3361

Ответ 1

Зачем использовать модульное тестирование, если у вас есть возможность автоматического тестирования на основе спецификаций? Библиотека QuickCheck сделает это за вас. Он может генерировать произвольные (фиктивные) функции и данные, используя класс типов Arbitrary.

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

Ответ 2

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

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

Ответ 3

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

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.

Ответ 4

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

например,

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

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


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

Ответ 5

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

f x = ...

к

f2 g x = ... 

а затем

f = f2 g
ftest = f2 gtest

Ответ 6

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

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

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

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

Ответ 7

У вас могут быть только две реализации функций с разными именами, а 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