2016-07-28 2 views
5

(Persistent를 통해) 데이터베이스에 액세스하는 많은 기능을 테스트해야합니다. monadicIOwithSqlitePool을 사용하여이 작업을 수행 할 수 있지만 비효율적 인 테스트가 발생합니다. 각 테스트 (속성이 아니라 테스트)는 DB 풀을 만들고 파괴합니다. 어떻게 이것을 막을 수 있습니까?QuickCheck를 사용하여 데이터베이스 관련 기능을 테스트하는 방법은 무엇입니까?

중요 : 효율성이나 우아함을 잊어 버리십시오. 나는 심지어 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 
+0

것은 당신이 당신의 quickcheck 호텔 중 한 곳의 예를 들어 주실 수 있습니까? – ErikR

+1

'monadicIO'를 호출하지 않고'withSqlitePool'을 사용하고 싶지 않으십니까? 예 :'tests = withSqlitePool $ \ pool -> do monadicIO (test1 pool); 모나드IO (test2 풀)'. –

+0

우리는': memory :'에 SQLite 연결을 사용한다. (나는 메모리 내 SQLite 데이터베이스라고 생각한다.) 병목 현상을 일으키지 않을 정도로 충분히 잘 작동하는 것 같지만, 아마도 우리는 우리보다 더 많은 데이터를 움직이고있을 것입니다. 여러분이 할 수있는 느린 힘든 일은'PersistStore'의 인스턴스를 만들고 그것을 (예를 들어)'Data.Map'의 묶음으로 구현하는 것입니다. 그러나 그것은 절대적으로'Database.Persist.Sql'에서 어떤 것을 사용하지 못하게합니다.이 경우'SqlBackend' 값을 생성하기 위해 팔과 다리를 사용해야합니다. – hao

답변

3

생성 및 DB 풀에만 DB 번,이 코드에서와 같이, 그 연결을 사용하도록 외부에 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 

수입 및 확장을 포함한 전체 코드는 in this gist입니다.

테스트간에 데이터베이스를 정리하는 기능을 구현하지 않았다는 것을 기억하십시오. 영구적 인 방법으로 일반화하는 방법을 모르기 때문에 직접 구현해야합니다 (자리 표시 자 정리 작업을 바꿔서 지금 메시지).


또한 PropertyM에 대한 MonadCatch/MonadThrow에 대한 인스턴스를 필요가 없습니다. 대신, NwApp 모나드를 잡아야합니다. 그래서이 대신 :

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 
+0

코드에서'ioProperty'는 무엇입니까? –

+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 

이러한 구성하지 않습니다. 이 중 하나는 속하지 않습니다.

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

이것은 monadicIO보다 더 나은 모습 : 우리는이 우리의 요구 사항은 생산 요구에 prop_childCreation를 사용하여 결합 할 수 있습니다 (m 속성 -> 속성).

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 

난 그냥 우리가 lift :: Monad m => m a -> T m a을 의미하는 MonadTrans입니다 끝에 T와 그 모든 것을 믿을 수 있습니다. 그리고 우리는이 SqlPersistT 제거하는 우리의 기회라고 볼 수

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

monadicIO이 우리에게 도움이 될 수 있도록 우리는 어딘가에 다시 IO를 제거해야합니다 :

\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을 던져 넣고, 오른쪽에서는 SqlPersistT의 모나드 인수 부분에 어떻게 든 "fmap"할 필요가 있습니다.음, 우리는 첫 번째 문제를 무시하고 다음 단계로 다른 사람을 연기 할 수 있습니다

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

이 그냥 Control.Monad.MorphMFunctor가 제공하는 무엇처럼 보인다 밝혀졌습니다. 당신의 탐구에서

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

Tada! 행운은, 어쩌면이 조금 도움이됩니다 : 난 그냥 SqlPersistT가의 인스턴스를했다 척 것이다.

exference 프로젝트는 방금 걸어온 프로세스를 자동화하려고 시도합니다. 나는 f와 g 같은 인자를 넣으면 ghc가 어떤 타입이 거기에 가야하는지 알려줄 것입니다. 사용

패키지 :

+0

이 대답이 정확하지 않다고 생각합니다. 여기에 여러 가지 실수가 있습니다. 1)'runSqlite'는 매 테스트마다 실행됩니다 (실행마다?). 따라서 질문에서 "DB를 한 번만 설정하십시오"라는 조건이 만족되지 않습니다. 2) 'Property'를 'IO Property'는 테스트 케이스 자체를 버리고 있습니다. – bennofs

+0

"중요 : 효율성이나 우아함을 잊어 버렸습니다. QuickCheck와 지속 형을 작성하지 못했습니다." 그것은 typecheck가 첫 번째 단계로 괜찮아지는 것처럼 들리도록 만들었습니다. 그러나 네, 부족한 부분에 대한 주석을보고 삽입 한 직후 나는 이드가 그걸 가지고 달려가 그 가르침을 보았다고 생각했습니다. 그러나 그때 나는 이것이 잠시 동안 유일한 대답으로 남을 것이라고 생각했다. – Gurkenglas

0

(http://lpaste.net/173182에서 사용할 수 .lhs) :

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 동작을 반환한다는 것입니다. 유일한 유용한 방법이 PropertyM IO 함께 사용하는 것으로 나타납니다 PropertyM는 모나드 변압기입니다

runQuery = runSqlite ":memory:" $ do aQuery 

비록 : 여기

당신이 SqlPersistM 조치를 실행할 수있는 방법이다.

SqlPersistM 작업에서 IO 작업을 수행하려면 백엔드가 필요합니다. 여기 run

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

lift과 동일하다 : 마음이와

, 여기에 예를 들어 데이터베이스 테스트입니다.

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

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

설명 :

  • runSqlPersistM aQuery backend는 IO-행동을
  • 하지만 withSqliteConn ... 인은을 필요로

    , 우리는 약간의 리프팅을 수행 할 필요 특정 백엔드과 SqlPersistM 작업을 실행하려면 로깅을 가진 모나 딕 액션
  • 따라서 우리는 IO 작업을 NoLoggingT IO 작업으로 옮깁니다. 35,124,기능

마지막으로, quickCheck를 통해 prop_test 실행 :

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

'm a -> IO a'함수를 사용하면 m이 'IO'가 아닌 경우에도 'PropertyM m'을 사용할 수 있습니다. 내 대답보기 – bennofs

+0

예 - 'ioProperty'는 제가 누락 된 기능이었습니다. – ErikR

관련 문제