2 Helpers for creating database tests with hspec and squeal, inspired by Jonathan Fischoff's
3 [hspec-pg-transact](http://hackage.haskell.org/package/hspec-pg-transact).
5 This uses @tmp-postgres@ to automatically and connect to a temporary instance of postgres on a random port.
7 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.
9 The libary also provides a few other functions for more fine grained control over running transactions in tests.
11 {-# LANGUAGE FlexibleContexts #-}
12 {-# LANGUAGE MonoLocalBinds #-}
13 {-# LANGUAGE RankNTypes #-}
14 {-# LANGUAGE RecordWildCards #-}
15 {-# LANGUAGE TupleSections #-}
16 {-# LANGUAGE TypeInType #-}
17 module Squeal.PostgreSQL.Hspec
20 import Control.Exception
22 import Control.Monad.Base (liftBase)
23 import Data.ByteString (ByteString)
24 import qualified Data.ByteString.Char8 as BSC
25 import Database.Postgres.Temp as Temp
26 import Squeal.PostgreSQL
27 import System.Environment (lookupEnv)
30 data TestDB a = TestDB
31 { tempDB :: Maybe Temp.DB
32 -- ^ Handle for temporary @postgres@ process
33 , pool :: Pool (K Connection a)
34 -- ^ Pool of 50 connections to the temporary @postgres@
35 , connectionString :: ByteString
38 type Fixtures db a = (Pool (K Connection db) -> IO a)
39 type Actions db a = PQ db db IO a
40 type FixtureContext db fix = (TestDB db, fix)
41 type Migrations def from to = Path (Migration def) from to
44 testDBEnv = "TEST_DB_CONNECTION_STRING"
46 getOrCreateConnectionString :: IO (ByteString, Maybe Temp.DB)
47 getOrCreateConnectionString = do
48 hasConnectionString <- lookupEnv testDBEnv
49 maybe createTempDB (pure . (, Nothing) . BSC.pack) hasConnectionString
51 createTempDB :: IO (ByteString, Maybe Temp.DB)
53 tempDB <- either throwIO return =<< Temp.start
54 let connectionString = Temp.toConnectionString tempDB
55 pure (connectionString, Just tempDB)
57 -- | Start a temporary @postgres@ process and create a pool of connections to it
59 :: Migratory def (IsoQ (Indexed PQ IO ()))
60 => Migrations def schema0 schema
61 -> Fixtures schema fix
62 -> IO (FixtureContext schema fix)
63 setupDB migration fixtures = do
64 (connectionString, tempDB) <- getOrCreateConnectionString
65 BSC.putStrLn connectionString
67 keepConnectionForOneHour = 3600
69 pool <- createConnectionPool
72 keepConnectionForOneHour
74 withConnection connectionString (migrateUp migration)
76 pure (TestDB {..}, res)
78 -- | Drop all the connections and shutdown the @postgres@ process
80 :: Migratory def (IsoQ (Indexed PQ IO ()))
81 => Migrations def schema0 schema
84 teardownDB migration TestDB {..} = do
85 withConnection connectionString (migrateDown migration)
86 destroyConnectionPool pool
87 maybe (pure ()) (void . Temp.stop) tempDB
89 -- | Run an 'IO' action with a connection from the pool
90 withPool :: TestDB db -> Actions db a -> IO a
91 withPool testDB = liftBase . usingConnectionPool (pool testDB)
93 -- | Run an 'DB' transaction, using 'transactionally_'
94 withDB :: Actions db a -> TestDB db -> IO a
95 withDB action testDB =
96 usingConnectionPool (pool testDB) (transactionally_ action)
98 -- | Flipped version of 'withDB'
99 runDB :: TestDB db -> Actions db a -> IO a
102 withFixture :: (fix -> Actions db a) -> FixtureContext db fix -> IO a
103 withFixture action (db, fix) =
104 usingConnectionPool (pool db) (transactionally_ $ action fix)
106 withoutFixture :: Actions db a -> FixtureContext db fix -> IO a
107 withoutFixture action (db, _) =
108 usingConnectionPool (pool db) (transactionally_ action)
110 -- | Helper for writing tests. Wrapper around 'it' that uses the passed
111 -- in 'TestDB' to run a db transaction automatically for the test.
112 itDB :: String -> Actions db a -> SpecWith (FixtureContext db ())
113 itDB msg action = it msg $ void . withoutFixture action
115 -- | Helper for writing tests. Wrapper around 'it' that uses the passed
116 -- in 'TestDB' to run a db transaction automatically for the test,
117 -- plus the result of the fixtures.
118 itDBF :: String -> (fix -> Actions db a) -> SpecWith (FixtureContext db fix)
119 itDBF msg action = it msg $ void . withFixture action
121 itDBF_ :: String -> Actions db a -> SpecWith (FixtureContext db fix)
122 itDBF_ msg action = it msg $ void . withoutFixture action
124 -- | Wraps 'describe' with a
127 -- 'beforeAll' ('setupDB' migrate)
130 -- hook for creating a db and a
133 -- 'afterAll' 'teardownDB'
136 -- hook for stopping a db.
138 :: Migratory def (IsoQ (Indexed PQ IO ()))
139 => Migrations def db0 db
142 -> SpecWith (FixtureContext db ())
144 describeDB migrate fixture str =
145 beforeAll (setupDB migrate fixture) . afterAll (teardownDB migrate . fst) . describe str
147 -- | Like `decribeDB`, but allow fixtures to pass
148 -- | a result to all specs
150 :: Migratory def (IsoQ (Indexed PQ IO ()))
151 => Migrations def db0 db
154 -> SpecWith (FixtureContext db fix)
156 describeFixtures migrate fixture str =
157 beforeAll (setupDB migrate fixture) . afterAll (teardownDB migrate . fst) . describe str