[英]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. 虽然我可以使用
monadicIO
和withSqlitePool
来执行此操作, 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. 我甚至无法使
QuickCheck
和Persistent
类型构成。
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
. 这里的
run
与lift
相同。
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动作 withSqliteConn ...
requires a monadic action that has logging withSqliteConn ...
需要一个具有日志记录的withSqliteConn ...
动作 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 Property
的Property
抛出,并且在右边我们需要以某种方式“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.Morph
的MFunctor
提供的那样。 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.