2016-07-28 2 views
5

J'ai besoin de tester beaucoup de fonctions qui accèdent à la base de données (via Persistent). Alors que je peux le faire en utilisant monadicIO et withSqlitePool il se traduira par des tests inefficaces. Chaque test, pas la propriété, mais le test, va créer et détruire le pool de DB. Comment puis-je empêcher cela?Comment utiliser QuickCheck pour tester les fonctions liées à la base de données?

Important: Oubliez l'efficacité ou l'élégance. Je n'ai pas pu faire les types QuickCheck et Persistent pour composer même.

instance (Monad a) => MonadThrow (PropertyM a) 

instance (MonadThrow a) => MonadCatch (PropertyM a) 

type NwApp = SqlPersistT IO 

prop_childCreation :: PropertyM NwApp Bool 
prop_childCreation = do 
    uid <- pick $ UserKey <$> arbitrary 
    lid <- pick $ LogKey <$> arbitrary 
    gid <- pick $ Aria2Gid <$> arbitrary 
    let createDownload_ = createDownload gid lid uid [] 
    (Entity pid _) <- run $ createDownload_ Nothing 
    dstatus <- pick arbitrary 
    parent <- run $ updateGet pid [DownloadStatus =. dstatus] 

    let test = do 
     (Entity cid child) <- run $ createDownload_ (Just pid) 
     case (parent ^. status, child ^. status) of 
      (DownloadComplete ChildrenComplete, DownloadComplete ChildrenNone) -> return True 
      (DownloadComplete ChildrenIncomplete, DownloadIncomplete) -> return True 
      _ -> return False 

    test `catches` [ 
    Handler (\ (e :: SanityException) -> return True), 
    Handler (\ (e :: SomeException) -> return False) 
    ] 

-- How do I write this function? 
runTests = monadicIO $ runSqlite ":memory:" $ do 
-- whatever I do, this function fails to typecheck 
+0

Pouvez-vous donner un exemple de l'une des propriétés de votre QuickCheck? – ErikR

+1

Ne voulez-vous pas simplement utiliser 'withSqlitePool' en dehors de l'appel de' monadicIO'? Par exemple, 'tests = withSqlitePool $ \ pool -> monadicIO (pool test1); monadicIO (pool test2) '. –

+0

Nous utilisons une connexion SQLite à ': memory:' (je pense que c'est plus ou moins juste une base de données SQLite en mémoire). Cela semble fonctionner assez bien, certainement assez pour ne jamais être un goulot d'étranglement, mais peut-être que vous déplacez plus de données que nous ne le sommes. La tâche lente et ardue que vous pouvez faire est de créer votre propre instance de 'PersistStore' et de l'implémenter avec (par exemple) un tas de' Data.Map's. Mais cela vous empêche absolument d'utiliser quoi que ce soit dans 'Database.Persist.Sql', auquel cas vous auriez besoin de dépenser un bras et une jambe pour construire une valeur' SqlBackend'. – hao

Répondre

3

Pour éviter de créer et de détruire la piscine DB et seulement mis en place la base de données une fois, vous devez utiliser withSqliteConn dans votre fonction main à l'extérieur, puis transformer chaque propriété à utiliser cette connexion, comme dans ce code:

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| 
Person 
    name String 
    age Int Maybe 
    deriving Show Eq 
|] 

type SqlT m = SqlPersistT (NoLoggingT (ResourceT m)) 

prop_insert_person :: PropertyM (SqlT IO)() 
prop_insert_person = do 
    personName <- pick arbitrary 
    personAge <- pick arbitrary 
    let person = Person personName personAge 

    -- This assertion will fail right now on the second iteration 
    -- since I have not implemented the cleanup code 
    numEntries <- run $ count ([] :: [Filter Person]) 
    assert (numEntries == 0) 

    personId <- run $ insert person 
    result <- run $ get personId 
    assert (result == Just person) 

main :: IO() 
main = runNoLoggingT $ withSqliteConn ":memory:" $ \connection -> lift $ do 
    let 
    -- Run a SqlT action using our connection 
    runSql :: SqlT IO a -> IO a 
    runSql = flip runSqlPersistM connection 

    runSqlProperty :: SqlT IO Property -> Property 
    runSqlProperty action = ioProperty . runSql $ do 
     prop <- action 
     liftIO $ putStrLn "\nDB reset code (per test) goes here\n" 
     return prop 

    quickCheckSql :: PropertyM (SqlT IO)() -> IO() 
    quickCheckSql = quickCheck . monadic runSqlProperty 

    -- Initial DB setup code 
    runSql $ runMigration migrateAll 

    -- Test as many quickcheck properties as you like 
    quickCheckSql prop_insert_person 

Le code complet, y compris les importations et les extensions peuvent être trouvées in this gist. Notez que je n'ai pas implémenté la fonctionnalité pour nettoyer la base de données entre les tests, car je ne sais pas comment faire en général avec persistant, vous devrez l'implémenter vous-même (remplacez l'action de nettoyage d'espace réservé qui imprime juste un message maintenant).


Vous ne devriez pas besoin d'instances pour MonadCatch/MonadThrow pour PropertyM. Au lieu de cela, vous devriez attraper dans la monade NwApp. Ainsi, au lieu de cela:

let test = do 
    run a 
    ... 
    run b 
test `catch` \exc -> ... 

vous devez utiliser le code suivant à la place:

let test = do 
    a 
    b 
    return ...whether or not the test was successfull... 
let testCaught = test `catch` \exc -> ..handler code... 
ok <- test 
assert ok 
+0

qu'est ce que 'ioProperty' dans le code? –

+0

http://hackage.haskell.org/package/QuickCheck-2.9.1/docs/Test-QuickCheck-Property.html#v:ioProperty – bennofs

0
monadicIO :: PropertyM IO a -> Property 
runSqlite ":memory:" :: SqlPersistT (NoLoggingT (ResourceT m)) a -> m a 
prop_childCreation :: PropertyM NwApp Bool 

Ils ne seront pas composer. L'un d'eux n'appartient pas.

monadic :: Monad m => (m Property -> Property) -> PropertyM m a -> Property 

Cela semble mieux que monadicIO: Nous pouvons combiner et notre obligation d'utiliser prop_childCreation dans l'obligation de produire (m Propriété -> Propriété).

runSqlite ":memory:" :: SqlPersistT (NoLoggingT (ResourceT m)) a -> m a 
\f -> monadic f prop_childCreation :: (NwApp Property -> Property) -> Property 

Rewrite NwApp pour faciliter la regardant:

runSqlite ":memory:" :: SqlPersistT (NoLoggingT (ResourceT m)) a -> m a 
\f -> monadic f prop_childCreation :: (SqlPersistT IO Property -> Property) -> Property 

Je vais juste confiance que tout avec T à la fin est un MonadTrans, ce qui signifie que nous avons lift :: Monad m => m a -> T m a. Ensuite, nous pouvons voir que c'est notre chance de se débarrasser de SqlPersistT:

\f g -> monadic (f . runSqlite ":memory:" . g) prop_childCreation :: (IO Property -> Property) -> (SqlPersistT IO Property -> SqlPersistT (NoLoggingT (ResourceT m)) Property) -> Property 

Nous aurons besoin de se débarrasser de l'IO quelque part à nouveau, si monadicIO pourrait nous aider:

\f g -> monadic (monadicIO . f . runSqlite ":memory:" . g) prop_childCreation :: (IO Property -> PropertyT IO a) -> (SqlPersistT IO Property -> SqlPersistT (NoLoggingT (ResourceT m)) Property) -> Property 

temps pour soulevez pour briller! Sauf que dans f, nous jetons apparemment le Property dans IO Property, et sur la droite, nous devons "fmap" dans la partie de l'argument monad de SqlPersistT en quelque sorte.Eh bien, nous pouvons ignorer le premier problème et remettre l'autre à l'étape suivante:

\f -> monadic (monadicIO . lift . runSqlite ":memory:" . f (lift . lift)) prop_childCreation :: ((m a -> n a) -> SqlPersistT m a -> SqlPersist n a) -> Property 

Transforme ce que ressemble MFunctor de Control.Monad.Morph fournit. Je vais juste faire semblant SqlPersistT avait une instance de cette:

monadic (monadicIO . lift . runSqlite ":memory:" . mmorph (lift . lift)) prop_childCreation :: Property 

Tada! Bonne chance dans votre quête, peut-être cela va aider un peu.

Le projet exference tente d'automatiser le processus que je viens de parcourir. J'ai entendu dire que mettre n'importe où des arguments comme f et g fera dire à ghc quel type devrait aller là-bas.

+0

Je ne pense pas que cette réponse soit correcte, il y a plusieurs erreurs ici: 1) 'runSqlite' sera exécuté sur chaque test (peut-être même chaque' run'?), Donc l'exigence "configurer le DB une seule fois" de la question n'est pas satisfaite en jetant le 'Property' dans 'Propriété IO', vous jetez essentiellement le cas de test lui-même – bennofs

+0

" Important: Oubliez l'efficacité ou l'élégance, je n'ai pas été en mesure de faire les types QuickCheck et Persistent pour même composer. " fait semblant de le faire typecheck est correct comme un premier pas. Mais oui, après avoir vu et inséré des commentaires sur les insuffisances, je pensais que je courrais juste avec elle et voir si c'est instructif. Mais alors j'ai pensé que cela resterait la seule réponse pendant un moment. – Gurkenglas

0

(.lhs disponibles à: http://lpaste.net/173182)

paquets utilisés:

build-depends: base >= 4.7 && < 5, QuickCheck, persistent, persistent-sqlite, monad-logger, transformers 

Premièrement, certaines importations:

{-# LANGUAGE OverloadedStrings #-} 

module Lib2 where 

import Database.Persist.Sql 
import Database.Persist.Sqlite 
import Test.QuickCheck 
import Test.QuickCheck.Monadic 
import Control.Monad.Logger 
import Control.Monad.Trans.Class 

Voici la requête que nous voulons tester:

aQuery :: SqlPersistM Int 
aQuery = undefined 

Bien sûr, aQuery peut prendre des arguments. L'important est que il renvoie une action SqlPersistM.

Voici comment vous pouvez exécuter une action SqlPersistM:

runQuery = runSqlite ":memory:" $ do aQuery 

Même si PropertyM est un transformateur de monade, il semble que le seul moyen utile de l'utiliser est avec PropertyM IO.

Afin d'obtenir une action d'E/S d'une action SqlPersistM, nous avons besoin du backend .

Avec cela à l'esprit, voici un test de base de données exemple:

prop_test :: SqlBackend -> PropertyM IO Bool 
prop_test backend = do 
    a <- run $ runSqlPersistM aQuery backend 
    b <- run $ runSqlPersistM aQuery backend 
    return (a == b) 

Ici run est le même que lift.

Pour exécuter une action SqlPersistM avec une base spécifique, nous avons besoin d'effectuer une levée:

runQuery2 = withSqliteConn ":memory:" $ \backend -> do 
       liftNoLogging (runSqlPersistM aQuery backend) 

liftNoLogging :: Monad m => m a -> NoLoggingT m a 
liftNoLogging = lift 

Explication:

  • runSqlPersistM aQuery backend est un IO action
  • mais withSqliteConn ... nécessite une l'action monadique qui a logging
  • ainsi nous élevons l'action d'E/S à une action d'E/S NoLoggingT avec le liftNoLogging fonction

Enfin, pour exécuter prop_test via quickcheck:

runTest = withSqliteConn ":memory:" $ \backend -> do 
      liftNoLogging $ quickCheck (monadicIO (prop_test backend)) 
+0

Vous pouvez utiliser 'PropertyM m' même si' m' n'est pas 'IO', en fournissant une fonction' m a -> IO a'. Voir ma réponse – bennofs

+0

Oui - 'ioProperty' était la fonction qui me manquait. – ErikR