Рекомендации Haskell QuickCheck (особенно при тестировании классов типов)

Я только что начал использовать QuickCheck с кучей кода Haskell. Я знаю, что знаю. Этот вопрос является двухпартовым:

Во-первых, каковы общие рекомендации по быстрой проверке? До сих пор я взял следующее:

  • Назовите свои тесты prop_ * (раздражает, потому что все остальное - camelCase)
  • Проверьте экспортированный код (если вы тестируете внутренние элементы, вы, вероятно, ошибаетесь)
  • Свойства теста, а не примеры
    • Не говори X is out of range, Y is in range
    • Вместо этого скажем if x is out of range, normalize x ≠ x (или какое-либо другое такое свойство)

Но я все еще хватаюсь за другие лучшие практики. В частности:

  • Где хранятся свойства?
    • Тот же файл?
    • в каталоге test/? (Если да, то как вы импортируете материал в src/?)
    • в каталоге Properties/ в разделе src?

Самое главное, как мы склонны тестировать свойства классов типов? Например, рассмотрим следующий (упрощенный) тип:

class Gen a where
    next :: a -> a
    prev :: a -> a

Я хотел бы проверить свойство ∀ x: prev (next x) == x. Конечно, это требует написания тестов для каждого экземпляра. Очень сложно написать одно и то же свойство для каждого экземпляра, особенно когда тест более сложный. Каков стандартный способ обобщения таких тестов?

Ответы

Ответ 1

Я считаю, что соглашение prop_ поступило из QC, следующего с script, в котором выполнялись все функции, которые начинались с prop_ в качестве тестов. Поэтому нет никакой реальной причины для этого, но он визуально выделяется (поэтому свойство для функции foo равно prop_foo).

И нет ничего плохого в тестировании внутренних компонентов. Есть два способа сделать это:

  • Поместите свойства в том же модуле, что и внутренние. Это делает модуль более значительным и требует безусловной зависимости от QC для проекта (если вы не используете хакерство CPP).

  • Имейте внутренности в неэкспортированном модуле, с функциями, которые фактически будут экспортированы, реэкспортироваться из другого модуля. Затем вы можете импортировать внутренний модуль в тот, который определяет свойства QC, и этот модуль только построен (и имеет зависимость QC), если используется флаг, указанный в файле .cabal.

Если ваш проект большой, то могут быть полезны отдельные каталоги src/ и test/ (хотя различение может помешать тестированию внутренних компонентов). Но если ваш проект не такой большой (и в любом случае он находится под одной общей иерархией модулей), тогда нет никакой реальной необходимости разделить его таким образом.

Как сказал в своем ответе Норман Рэмси, для классов классов вы можете просто определить свойство как находящееся на классной строке и использовать соответственно.

Ответ 2

Нужно записывать одно и то же свойство для каждого экземпляра

Вы этого не делаете. Вы пишете свойство один раз для класса:

class Gen a where
    next :: a -> a
    prev :: a -> a

np_prop :: (Eq a, Gen a) => a -> Bool
np_prop a = prev (next a) == a

Затем, чтобы протестировать его, вы добавили к определенному типу:

quickCheck (np_prop :: Int -> Bool)
quickCheck (np_prop :: String -> Bool)

Другие вопросы, с которыми я не могу помочь.

Ответ 3

Try

{-# LANGUAGE GADTs, ScopedTypeVariables #-}
import Test.QuickCheck hiding (Gen)

class Gen a where
  next :: a -> a
  prev :: a -> a

np_prop :: SomeGen -> Bool
np_prop (SomeGen a) = prev (next a) == a

main :: IO ()
main = quickCheck np_prop

instance Gen Bool where
  next True = False
  next False = True
  prev True = False
  prev False = True

instance Gen Int where
  next = (+ 1)
  prev = subtract 1

data SomeGen where
  SomeGen :: (Show a, Eq a, Arbitrary a, Gen a) => a -> SomeGen

instance Show SomeGen where
  showsPrec p (SomeGen a) = showsPrec p a
  show (SomeGen a) = show a

instance Arbitrary SomeGen where
  arbitrary = do
    GenDict (Proxy :: Proxy a) <- arbitrary
    a :: a <- arbitrary
    return $ SomeGen a
  shrink (SomeGen a) =
    map SomeGen $ shrink a

data GenDict where
  GenDict :: (Show a, Eq a, Arbitrary a, Gen a) => Proxy a -> GenDict

instance Arbitrary GenDict where
  arbitrary =
    elements
    [ GenDict (Proxy :: Proxy Bool)
    , GenDict (Proxy :: Proxy Int)
    ]

data Proxy a = Proxy

Класс типа инициализируется в словарь с экзистенциальным количественным выражением, на котором определен экземпляр Arbitrary. Этот экземпляр словаря Arbitrary затем используется для определения экземпляра Arbitrary для экзистенциально квантованных значений.

Другой пример приведен в https://github.com/sonyandy/var/blob/4e0b12c390eb503616d53281b0fd66c0e1d0594d/tests/properties.hs#L217.

Это может быть дополнительно обобщено (и сокращен шаблон), если вы хотите использовать ConstraintKinds. Следующее определяется только один раз.

data Some c where
  Some :: (Show a, Arbitrary a, c a) => a -> Some c

instance Show (Some c) where
  showsPrec p (Some a) = showsPrec p a
  show (Some a) = show a

instance Arbitrary (Dict c) => Arbitrary (Some c) where
  arbitrary = do
    Dict (Proxy :: Proxy a) :: Dict c <- arbitrary
    a :: a <- arbitrary
    return $ Some a
  shrink (Some a) =
    map Some $ shrink a

data Dict c where
  Dict :: (Show a, Arbitrary a, c a) => Proxy a -> Dict c

data Proxy a = Proxy

class (c a, d a) => (c &&# d) a
instance (c a, d a) => (c &&# d) a

Для каждого типа, который вы хотите протестировать, требуется Arbitrary экземпляр Dict.

instance Arbitrary (Dict (Eq &&# Gen)) where
  arbitrary =
    elements
    [ Dict (Proxy :: Proxy Bool)
    , Dict (Proxy :: Proxy Int)
    ]

np_prop :: Some (Eq &&# Gen) -> Bool
np_prop (Some a) = prev (next a) == a