packages feed

hspec-pg-transact-0.1.0.3: src/Test/Hspec/DB.hs

{-|
Helpers for creating database tests with hspec and pg-transact

@hspec-pg-transact@ utilizes @tmp-postgres@ to automatically and connect to a temporary instance of @postgres@ on a random port.

 @
  'describeDB' migrate "Query” $
    itDB "work" $ do
      'execute_' [sql|
        INSERT INTO things
        VALUES (‘me’) |]
      'query_' [sql|
        SELECT name
         FROM things |]
        `shouldReturn` [Only "me"]
 @

In the example above 'describeDB' wraps 'describe' with a 'beforeAll' hook for creating a db and a 'afterAll' hook for stopping a db.
.
Tests can be written with 'itDB' which is wrapper around 'it' that uses the passed in 'TestDB' to run a db transaction automatically for the test.

The libary also provides a few other functions for more fine grained control over running transactions in tests.

-}

module Test.Hspec.DB where

import           Control.Monad
import           Data.Pool
import           Database.PostgreSQL.Simple
import           Database.PostgreSQL.Transact
import qualified Database.Postgres.Temp       as Temp
import           Test.Hspec

data TestDB = TestDB
  { tempDB :: Temp.DB
  -- ^ Handle for temporary @postgres@ process
  , pool   :: Pool Connection
  -- ^ Pool of 50 connections to the temporary @postgres@
  }

-- | Start a temporary @postgres@ process and create a pool of connections to it
setupDB :: (Connection -> IO ()) -> IO (Either Temp.StartError TestDB)
setupDB = setupDBWithConfig Temp.defaultConfig

-- | Start a temporary @postgres@ process using the provided configuration
setupDBWithConfig :: Temp.Config
  -> (Connection -> IO ())
  -> IO (Either Temp.StartError TestDB)
setupDBWithConfig c f =
  traverse (wrapCallback f) =<< Temp.startConfig c

wrapCallback :: (Connection -> IO ()) -> Temp.DB -> IO TestDB
wrapCallback f d = do
  p <- createPool
    (connectPostgreSQL $ Temp.toConnectionString d)
    close
    1
    100000000
    50
  withResource p f
  pure $ TestDB d p

-- | Drop all the connections and shutdown the @postgres@ process
teardownDB :: TestDB -> IO ()
teardownDB (TestDB d p) = do
  destroyAllResources p
  void $ Temp.stop d

-- | Run an 'IO' action with a connection from the pool
withPool :: TestDB -> (Connection -> IO a) -> IO a
withPool testDB = withResource (pool testDB)

-- | Run an 'DB' transaction. Uses 'runDBTSerializable'.
withDB :: DB a -> TestDB -> IO a
withDB action testDB =
  withResource (pool testDB) (runDBTSerializable action)

-- | Flipped version of 'withDB'
runDB :: TestDB -> DB a -> IO a
runDB = flip withDB

-- | Helper for writing tests. Wrapper around 'it' that uses the passed
--   in 'TestDB' to run a db transaction automatically for the test.
itDB :: String -> DB a -> SpecWith TestDB
itDB msg action = it msg $ void . withDB action

-- | Wraps 'describeDBWithConfig' using the default configuration
describeDB :: (Connection -> IO ()) -> String -> SpecWith TestDB -> Spec
describeDB = describeDBWithConfig Temp.defaultConfig

-- | Wraps 'describe' with a
--
-- @
--   'beforeAll' ('setupDB' migrate)
-- @
--
-- hook for creating a db and a
--
-- @
--   'afterAll' 'teardownDB'
-- @
--
-- hook for stopping a db.
describeDBWithConfig :: Temp.Config -> (Connection -> IO ()) -> String -> SpecWith TestDB -> Spec
describeDBWithConfig c f s =
  beforeAll (catch =<< setupDBWithConfig c f) . afterAll teardownDB . describe s
  where
    catch :: Either Temp.StartError TestDB -> IO TestDB
    catch r = case r of
      Left x   -> error (show x)
      Right db -> pure db