简体   繁体   English

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

[英]How to use QuickCheck to test database related functions?

I need to test a lot of functions that access the database (via Persistent). 我需要测试许多访问数据库的函数(通过Persistent)。 While I can do this using monadicIO and withSqlitePool it will result in inefficient tests. 虽然我可以使用monadicIOwithSqlitePool来执行此操作, withSqlitePool会导致测试效率低下。 Each test, not property, but test, will create and destroy the DB pool. 每个测试,而不是属性,但测试,将创建和销毁数据库池。 How do I prevent this? 我该如何防止这种情况?

Important: Forget about efficiency or elegance. 重要提示:忘记效率或优雅。 I haven't been able to make the QuickCheck and Persistent types to even compose. 我甚至无法使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

To avoid creating and destroying the DB pool and only set up the DB once, you need to use withSqliteConn in your main function on the outside and then transform each property to use that connection, like in this code: 为了避免创建和销毁数据库池并仅设置数据库一次,您需要在外部的main函数中使用withSqliteConn ,然后转换每个属性以使用该连接,如下面的代码所示:

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

The full code including imports and extensions can be found in this gist . 完整的代码包括导入和扩展可以在这个要点中找到。

Note that I did not implement the functionality to clean the database between tests, as I do not know how to do that in general with persistent, you will have to implement that yourself (replace the placeholder cleanup action that just prints a message right now). 请注意,我没有实现在测试之间清理数据库的功能,因为我不知道如何通常使用持久性,你必须自己实现(替换刚刚打印消息的占位符清理操作) 。


You should also not need instances for MonadCatch / MonadThrow for PropertyM . 对于PropertyM您也不需要MonadCatch / MonadThrow实例。 Instead, you should catch in the NwApp monad. 相反,你应该赶上NwApp monad。 So instead of this: 所以不是这样的:

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

you should use the following code instead: 您应该使用以下代码:

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

( .lhs available at: http://lpaste.net/173182 ) (.lhs位于: http ://lpaste.net/173182)

Packages used: 使用的包:

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

First, some imports: 首先,一些进口:

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

Here is the query we want to test: 这是我们要测试的查询:

 aQuery :: SqlPersistM Int
 aQuery = undefined

Of course, aQuery may take arguments. 当然, aQuery可能需要参数。 The important thing is that it returns a SqlPersistM action. 重要的是它返回一个SqlPersistM动作。

Here is how you can run a SqlPersistM action: 以下是如何运行SqlPersistM操作:

 runQuery = runSqlite ":memory:" $ do aQuery

Even though PropertyM is a monad transformer, it appears that the only useful way to use it is with PropertyM IO . 尽管PropertyM是monad变换器,但似乎唯一有用的方法是使用PropertyM IO

In order to get an IO-action out of a SqlPersistM-action, we need the backend. 为了从SqlPersistM-action中获取IO动作,我们需要后端。

With these in mind, here is an example database test: 考虑到这些,这是一个示例数据库测试:

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

Here run is the same as lift . 这里的runlift相同。

To run an SqlPersistM action with a specific backend, we need to perform some lifting: 要使用特定后端运行SqlPersistM操作,我们需要执行一些提升:

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

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

Explanation: 说明:

  • runSqlPersistM aQuery backend is an IO-action runSqlPersistM aQuery backend是一个IO动作
  • but withSqliteConn ... requires a monadic action that has logging 但是withSqliteConn ...需要一个具有日志记录的withSqliteConn ...动作
  • so we lift the IO-action to a NoLoggingT IO-action with the liftNoLogging function 所以我们使用liftNoLogging函数将IO动作提升到NoLoggingT IO动作

Finally, to run prop_test via quickCheck: 最后,通过quickCheck运行prop_test:

 runTest = withSqliteConn ":memory:" $ \backend -> do
             liftNoLogging $ quickCheck (monadicIO (prop_test backend))
monadicIO :: PropertyM IO a -> Property
runSqlite ":memory:" :: SqlPersistT (NoLoggingT (ResourceT m)) a -> m a
prop_childCreation :: PropertyM NwApp Bool

These won't compose. 这些不会构成。 One of these doesn't belong. 其中一个不属于。

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

This looks better than monadicIO : We can combine this and our requirement to use prop_childCreation into a requirement to produce (m Property -> Property). 这看起来比monadicIO :我们可以将这个和我们的要求结合起来,将prop_childCreation用于生成需求(m Property - > Property)。

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

Rewrite NwApp to ease the looking up: 重写NwApp以便于查找:

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

I'll just trust that everything with T at the end is a MonadTrans , meaning we have lift :: Monad m => ma -> T ma . 我只相信T末端的所有东西都是MonadTrans ,这意味着我们有lift :: Monad m => ma -> T ma Then we can see that this is our chance to get rid of SqlPersistT: 然后我们可以看到这是我们摆脱SqlPersistT的机会:

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

We'll need to get rid of the IO somewhere again, so monadicIO might help us: 我们需要再次摆脱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

Time for lift to shine! 电梯闪耀的时间! Except that in f we apparently throw the Property in IO Property away, and on the right we need to "fmap" into the monad argument part of SqlPersistT somehow. 除了在f中我们显然将IO PropertyProperty抛出,并且在右边我们需要以某种方式“fmap”到SqlPersistT的monad参数部分。 Well, we can ignore the first problem and defer the other to the next step: 好吧,我们可以忽略第一个问题,并将另一个问题推迟到下一步:

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

Turns out this looks just like what Control.Monad.Morph 's MFunctor provides. 结果看起来就像Control.Monad.MorphMFunctor提供的那样。 I'll just pretend SqlPersistT had an instance of that: 我只是假装SqlPersistT有一个实例:

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

Tada! 田田! Good luck in your quest, maybe this'll help a little. 祝你好运,也许这会有所帮助。

The exference project attempts to automate the process I just walked through. exference项目试图使我刚刚完成的过程自动化。 I've heard that putting _ whereever I put arguments like f and g will make ghc tell you what type should go there. 我听说过,只要我把f和g之类的参数放进去,就会让ghc告诉你应该去哪种类型。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM