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 DataKinds #-}
12 {-# LANGUAGE FlexibleContexts #-}
13 {-# LANGUAGE MonoLocalBinds #-}
14 {-# LANGUAGE RankNTypes #-}
15 {-# LANGUAGE RecordWildCards #-}
16 {-# LANGUAGE TupleSections #-}
17 {-# LANGUAGE TypeInType #-}
18 {-# LANGUAGE TypeOperators #-}
19 module Squeal.PostgreSQL.Hspec
22 import Control.Exception
24 import Control.Monad.Base (liftBase)
25 import Data.ByteString (ByteString)
26 import qualified Data.ByteString.Char8 as BSC
27 import qualified Database.Postgres.Temp as Temp
28 import Generics.SOP (K)
29 import Squeal.PostgreSQL
30 import Squeal.PostgreSQL.Pool
31 import System.Environment (lookupEnv)
34 data TestDB a = TestDB
35 { tempDB :: Maybe Temp.DB
36 -- ^ Handle for temporary @postgres@ process
38 -- ^ Pool of 50 connections to the temporary @postgres@
39 , connectionString :: ByteString
42 type Fixtures schema a = (Pool (K Connection schema) -> IO a)
43 type Actions schema a = PoolPQ schema IO a
44 type SquealContext schema = TestDB (K Connection schema)
45 type FixtureContext schema fix = (SquealContext schema, fix)
48 testDBEnv = "TEST_DB_CONNECTION_STRING"
50 getOrCreateConnectionString :: IO (ByteString, Maybe Temp.DB)
51 getOrCreateConnectionString = do
52 hasConnectionString <- lookupEnv testDBEnv
53 maybe createTempDB (pure . (, Nothing) . BSC.pack) hasConnectionString
55 createTempDB :: IO (ByteString, Maybe Temp.DB)
57 tempDB <- either throwIO return =<< Temp.startAndLogToTmp []
58 let connectionString = BSC.pack (Temp.connectionString tempDB)
59 pure (connectionString, Just tempDB)
61 -- | Start a temporary @postgres@ process and create a pool of connections to it
63 :: Migratory p => AlignedList (Migration p) schema0 schema
64 -> Fixtures schema fix
65 -> IO (FixtureContext schema fix)
66 setupDB migration fixtures = do
67 (connectionString, tempDB) <- getOrCreateConnectionString
68 BSC.putStrLn connectionString
70 keepConnectionForOneHour = 3600
72 pool <- createConnectionPool
75 keepConnectionForOneHour
77 withConnection connectionString (migrateUp migration)
79 pure (TestDB {..}, res)
81 -- | Drop all the connections and shutdown the @postgres@ process
83 :: Migratory p => AlignedList (Migration p) schema0 schema
86 teardownDB migration TestDB {..} = do
87 withConnection connectionString (migrateDown migration)
88 destroyAllResources pool
89 maybe (pure ()) (void . Temp.stop) tempDB
91 -- | Run an 'IO' action with a connection from the pool
92 withPool :: TestDB (K Connection schema) -> Actions schema a -> IO a
93 withPool testDB = liftBase . flip runPoolPQ (pool testDB)
95 -- | Run an 'DB' transaction, using 'transactionally_'
96 withDB :: Actions schema a -> TestDB (K Connection schema) -> IO a
97 withDB action testDB =
98 runPoolPQ (transactionally_ action) (pool testDB)
100 -- | Flipped version of 'withDB'
101 runDB :: TestDB (K Connection schema) -> Actions schema a -> IO a
104 withFixture :: (fix -> Actions schema a) -> FixtureContext schema fix -> IO a
105 withFixture action (db, fix) =
106 runPoolPQ (transactionally_ $ action fix) (pool db)
108 withoutFixture :: Actions schema a -> FixtureContext schema fix -> IO a
109 withoutFixture action (db, _) =
110 runPoolPQ (transactionally_ action) (pool db)
112 -- | Helper for writing tests. Wrapper around 'it' that uses the passed
113 -- in 'TestDB' to run a db transaction automatically for the test.
114 itDB :: String -> Actions schema a -> SpecWith (FixtureContext schema ())
115 itDB msg action = it msg $ void . withoutFixture action
117 -- | Helper for writing tests. Wrapper around 'it' that uses the passed
118 -- in 'TestDB' to run a db transaction automatically for the test,
119 -- plus the result of the fixtures.
120 itDBF :: String -> (fix -> Actions schema a) -> SpecWith (FixtureContext schema fix)
121 itDBF msg action = it msg $ void . withFixture action
123 itDBF_ :: String -> Actions schema a -> SpecWith (FixtureContext schema fix)
124 itDBF_ msg action = it msg $ void . withoutFixture action
126 -- | Wraps 'describe' with a
129 -- 'beforeAll' ('setupDB' migrate)
132 -- hook for creating a db and a
135 -- 'afterAll' 'teardownDB'
138 -- hook for stopping a db.
140 :: Migratory p => AlignedList (Migration p) schema0 schema
141 -> Fixtures schema ()
143 -> SpecWith (FixtureContext schema ())
145 describeDB migrate fixture str =
146 beforeAll (setupDB migrate fixture) . afterAll (teardownDB migrate . fst) . describe str
148 -- | Like `decribeDB`, but allow fixtures to pass
149 -- | a result to all specs
151 :: Migratory p => AlignedList (Migration p) schema0 schema
152 -> Fixtures schema fix
154 -> SpecWith (FixtureContext schema fix)
156 describeFixtures migrate fixture str =
157 beforeAll (setupDB migrate fixture) . afterAll (teardownDB migrate . fst) . describe str