testing haskell

testing - Mejores prácticas de Haskell QuickCheck(especialmente al probar clases de tipos)



(3)

Es tedioso escribir la misma propiedad para cada instancia

Tú no haces esto. Usted escribe la propiedad una vez para la clase:

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

Luego para probarlo, lanzas a un tipo particular:

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

Sus otras preguntas no puedo ayudar.

Acabo de empezar a usar QuickCheck con un montón de código Haskell. Estoy atrasado de los tiempos, lo sé. Esta pregunta es de dos partes:

En primer lugar, ¿cuáles son las mejores prácticas generales para Quick Check? Hasta ahora, he recogido lo siguiente:

  • Nombra tus pruebas prop_ * (molesto, porque todo lo demás es camelCase)
  • Pruebe el código exportado (si está probando internamente, es probable que lo esté haciendo mal)
  • Propiedades de prueba, no ejemplos.
    • No digas que X is out of range, Y is in range
    • En cambio, diga if x is out of range, normalize x ≠ x (o alguna otra propiedad similar)

Pero todavía estoy tomando otras mejores prácticas. Particularmente:

  • ¿Dónde se guardan las propiedades?
    • ¿El mismo archivo?
    • en una test/ directorio? (Si es así, entonces, ¿cómo importar las cosas en src/ ?)
    • en un directorio de Properties/ bajo src ?

Más importante aún, ¿cómo tendemos a probar las propiedades en las clases de tipos? Por ejemplo, considere la siguiente clase de tipo (simplificada):

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

Me gustaría probar la propiedad ∀ x: prev (next x) == x . Por supuesto, esto implica escribir pruebas para cada instancia. Es tedioso escribir la misma propiedad para cada instancia, especialmente cuando la prueba es más complicada. ¿Cuál es la forma estándar de generalizar tales pruebas?


Creo que la convención prop_ provenía de QC que venía con un script que ejecutaba todas las funciones que comenzaron con prop_ como pruebas. Entonces, no hay una razón real para hacerlo, pero se destaca visualmente (por lo que la propiedad de una función foo es prop_foo ).

Y no hay nada malo con las pruebas internas. Hay dos maneras de hacerlo:

  • Ponga las propiedades en el mismo módulo que las internas. Esto hace que el módulo sea más grande y requiere una dependencia incondicional de QC para el proyecto (a menos que use una piratería de CPP).

  • Tener elementos internos en un módulo no exportado, con las funciones que se exportarán en realidad reexportadas desde otro módulo. Luego, puede importar el módulo interno a uno que defina las propiedades de QC, y ese módulo solo se construye (y tiene una dependencia de QC) si se usa una marca especificada en el archivo .cabal.

Si su proyecto es grande, entonces tener src/ y test/ directory separados puede ser útil (aunque tener una distinción puede impedirle realizar pruebas internas). Pero si su proyecto no es tan grande (y reside de todos modos bajo una jerarquía general de módulos), entonces no hay necesidad real de dividirlo así.

Como dijo Norman Ramsey en su respuesta, para las clases de tipos, puede definir la propiedad como parte de la clase y usarla en consecuencia.


Tratar

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

La clase de tipo se reifica en un diccionario cuantificado existencialmente, en el que se define una instancia Arbitrary . Esta instancia de diccionario Arbitrary se utiliza para definir una instancia de Arbitrary para valores cuantificados existencialmente.

Otro ejemplo se da en https://github.com/sonyandy/var/blob/4e0b12c390eb503616d53281b0fd66c0e1d0594d/tests/properties.hs#L217 .

Esto se puede generalizar aún más (y reducir la placa de preparación) si está dispuesto a usar ConstraintKinds . Lo siguiente se define solo una vez.

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

Para cada clase de tipo que desee probar, se requiere una instancia Arbitrary de 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