18

Вопрос по haskell – Лучшие практики Haskell QuickCheck (особенно при тестировании классов типов)

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

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

  • Name your tests prop_* (annoying, because everything else is camelCase)
  • Test exported code (if you're testing internals you're likely doing it wrong)
  • Test properties, not examples
    • Don't say X is out of range, Y is in range
    • Instead, say if x is out of range, normalize x ≠ x (or some other such property)

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

  • Where are properties kept?
    • The same file?
    • in a test/ directory? (If so, then how do you import the stuff in src/?)
    • in a Properties/ directory under src?

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

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

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

  • Error: User Rate Limit Exceededtest-suiteError: User Rate Limit Exceeded

    от
  • Они, конечно, могут, просто сложнее получить к ним тесты и (по моему опыту) гораздо полезнее тестировать экспортированное поведение, а не детали реализации.

    от So8res
  • Для импорта при использовании параллельногоsrc/ а такжеtest/ каталоги, которые вы хотите установитьHs-Source-Dirs: src, test в вашем.cabal файл, так что оба каталога находятся в пути поиска модуля.

    от hammar
  • Почему внутренние компоненты не могут иметь свойства?

    от alternative
  • 18

    It's tedious to write the same property for each instance

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

    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

    Пытаться

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

  • 10

    Я верю

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

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

    Put the properties in the same module as the internals. This makes the module bigger, and requires an unconditional dependency on QC for the project (unless you use CPP hackery).

    Have internals in a non-exported module, with the functions to actually be exported re-exported from another module. Then you can import the internal module into one that defines the QC properties, and that module is only built (and has a QC dependency) if a flag specified in the .cabal file is used.

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

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