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)
- No digas que
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 ensrc/
?) - en un directorio de
Properties/
bajosrc
?
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 sí 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