如何使用QuickCheck测试数据库相关的功能?

时间:2016-07-28 19:05:21

标签: haskell persistent quickcheck

我需要测试很多访问数据库的函数(通过Persistent)。虽然我可以使用monadicIOwithSqlitePool执行此操作,但这会导致测试效率低下。每个测试,而不是属性,但测试,将创建和销毁数据库池。我该如何防止这种情况?

重要:忘记效率或优雅。我还没有能够使QuickCheckPersistent类型甚至构成。

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

3 个答案:

答案 0 :(得分:3)

为避免创建和销毁数据库池并仅设置数据库一次,您需要在外部的withSqliteConn函数中使用main,然后转换每个属性以使用该连接,例如在这段代码中:

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

可以找到包含导入和扩展名的完整代码in this gist

请注意,我没有实现在测试之间清理数据库的功能,因为我不知道如何通常使用持久性,你必须自己实现(替换只打印消息的占位符清理操作)马上)。

对于MonadCatch,您也不需要MonadThrow / PropertyM的实例。相反,你应该赶上NwApp monad。所以不要这样:

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

您应该使用以下代码:

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

答案 1 :(得分:1)

(.lhs见:http://lpaste.net/173182

使用的套餐:

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

首先,一些进口:

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

以下是我们要测试的查询:

 aQuery :: SqlPersistM Int
 aQuery = undefined

当然,aQuery可能会有争议。重要的是 它返回SqlPersistM动作。

以下是运行SqlPersistM操作的方法:

 runQuery = runSqlite ":memory:" $ do aQuery

即使PropertyM是monad变换器,它似乎是唯一的 使用它的有用方法是使用PropertyM IO

为了从SqlPersistM-action中获取IO动作,我们需要 后端。

考虑到这些,这是一个示例数据库测试:

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

此处runlift相同。

要运行具有特定后端的SqlPersistM操作,我们需要 执行一些提升:

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

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

说明:

  • runSqlPersistM aQuery backend是一个IO动作
  • withSqliteConn ...需要记录
  • 的monadic操作
  • 所以我们使用liftNoLogging函数将IO动作提升为NoLoggingT IO动作

最后,通过quickCheck运行prop_test:

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

答案 2 :(得分:0)

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

这些不会构成。其中一个不属于。

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

这看起来比monadicIO更好:我们可以将这个和我们的要求结合起来,将prop_childCreation用于生成需求(m Property - &gt; Property)。

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

重写NwApp以便于查找:

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

我只相信最后T的所有内容都是MonadTrans,这意味着我们有lift :: Monad m => m a -> T m a。然后我们可以看到这是我们摆脱SqlPersistT的机会:

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

我们需要再次摆脱IO,所以monadicIO可能会帮助我们:

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

升降机闪耀的时间!除了在f中我们显然将Property扔到IO Property之外,在右边我们需要以某种方式“fmap”到SqlPersistT的monad参数部分。好吧,我们可以忽略第一个问题,并将另一个问题推迟到下一步:

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

看起来就像Control.Monad.Morph MFunctor提供的那样。我只是假装SqlPersistT有一个实例:

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

Tada!祝你好运,也许这会有所帮助。

exference项目尝试自动完成我刚刚完成的过程。我听说过,只要我把f和g之类的参数放在一边,就会让ghc告诉你应该去哪种类型。