Skip to content

Commit e75a2be

Browse files
Merge pull request #23 from brandonchinn178/test-persistent-api
Finish backfilling tests
2 parents 2b8c2c8 + 10ae6fa commit e75a2be

File tree

3 files changed

+361
-50
lines changed

3 files changed

+361
-50
lines changed

src/Database/Persist/Monad/TestUtils.hs

+126-13
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Defines 'MockSqlQueryT', which one can use in tests in order to mock out
77

88
{-# LANGUAGE GADTs #-}
99
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
10+
{-# LANGUAGE LambdaCase #-}
1011
{-# LANGUAGE RankNTypes #-}
1112
{-# LANGUAGE ScopedTypeVariables #-}
1213
{-# LANGUAGE TypeApplications #-}
@@ -17,16 +18,29 @@ module Database.Persist.Monad.TestUtils
1718
, withRecord
1819
, mockQuery
1920
, MockQuery
21+
-- * Specialized helpers
22+
, mockSelectSource
23+
, mockSelectKeys
24+
, mockWithRawQuery
25+
, mockRawQuery
26+
, mockRawSql
2027
) where
2128

29+
import Conduit ((.|))
30+
import qualified Conduit
2231
import Control.Monad (msum)
2332
import Control.Monad.IO.Class (MonadIO(..))
2433
import Control.Monad.Reader (ReaderT, ask, runReaderT)
2534
import Control.Monad.Trans.Resource (MonadResource)
35+
import qualified Data.Acquire as Acquire
36+
import Data.Text (Text)
37+
import qualified Data.Text as Text
2638
import Data.Typeable (Typeable, eqT, (:~:)(..))
39+
import Database.Persist.Sql
40+
(Entity, Filter, Key, PersistValue, SelectOpt, rawSqlProcessRow)
2741

2842
import Database.Persist.Monad.Class (MonadSqlQuery(..))
29-
import Database.Persist.Monad.SqlQueryRep (SqlQueryRep)
43+
import Database.Persist.Monad.SqlQueryRep (SqlQueryRep(..))
3044

3145
-- | A monad transformer for testing functions that use 'MonadSqlQuery'.
3246
newtype MockSqlQueryT m a = MockSqlQueryT
@@ -43,11 +57,10 @@ newtype MockSqlQueryT m a = MockSqlQueryT
4357
--
4458
-- When a database query is executed, the first mock that returns a 'Just' is
4559
-- returned. If no mocks match the query, an error is thrown. See 'SqlQueryRep'
46-
-- for the constructors available to match against. Use `withRecord` to only
47-
-- match queries against a specific @record@ type
48-
-- (e.g. 'Database.Persist.Monad.Shim.selectList'), or `mockQuery` to match
49-
-- queries that don't reference a specific @record@ type
50-
-- (e.g. 'Database.Persist.Monad.Shim.rawSql').
60+
-- for the constructors available to match against. Most of the time, you'll
61+
-- want to use 'withRecord' to only match queries against a specific @record@
62+
-- type (e.g. only match 'Database.Persist.Monad.Shim.selectList' calls for
63+
-- the @Person@ entity).
5164
--
5265
-- Usage:
5366
--
@@ -71,10 +84,10 @@ newtype MockSqlQueryT m a = MockSqlQueryT
7184
runMockSqlQueryT :: MockSqlQueryT m a -> [MockQuery] -> m a
7285
runMockSqlQueryT action mockQueries = (`runReaderT` mockQueries) . unMockSqlQueryT $ action
7386

74-
instance Monad m => MonadSqlQuery (MockSqlQueryT m) where
87+
instance MonadIO m => MonadSqlQuery (MockSqlQueryT m) where
7588
runQueryRep rep = do
7689
mockQueries <- MockSqlQueryT ask
77-
maybe (error $ "Could not find mock for query: " ++ show rep) return
90+
maybe (error $ "Could not find mock for query: " ++ show rep) liftIO
7891
$ msum $ map tryMockQuery mockQueries
7992
where
8093
tryMockQuery (MockQuery f) = f rep
@@ -83,8 +96,8 @@ instance Monad m => MonadSqlQuery (MockSqlQueryT m) where
8396

8497
-- | A mocked query to use in 'runMockSqlQueryT'.
8598
--
86-
-- Use 'withRecord' or 'mockQuery' to create a 'MockQuery'.
87-
data MockQuery = MockQuery (forall record a. Typeable record => SqlQueryRep record a -> Maybe a)
99+
-- Use 'withRecord' or another helper to create a 'MockQuery'.
100+
data MockQuery = MockQuery (forall record a. Typeable record => SqlQueryRep record a -> Maybe (IO a))
88101

89102
-- | A helper for defining a mocked database query against a specific @record@
90103
-- type. Designed to be used with TypeApplications.
@@ -110,13 +123,113 @@ data MockQuery = MockQuery (forall record a. Typeable record => SqlQueryRep reco
110123
withRecord :: forall record. Typeable record => (forall a. SqlQueryRep record a -> Maybe a) -> MockQuery
111124
withRecord f = MockQuery $ \(rep :: SqlQueryRep someRecord result) ->
112125
case eqT @record @someRecord of
113-
Just Refl -> f rep
126+
Just Refl -> pure <$> f rep
114127
Nothing -> Nothing
115128

116129
-- | A helper for defining a mocked database query.
117130
--
118131
-- This does not do any matching on the @record@ type, so it is mostly useful
119132
-- for queries that don't use the @record@ type, like
120-
-- 'Database.Persist.Monad.Shim.rawSql'.
133+
-- 'Database.Persist.Monad.Shim.rawExecute'.
121134
mockQuery :: (forall record a. Typeable record => SqlQueryRep record a -> Maybe a) -> MockQuery
122-
mockQuery = MockQuery
135+
mockQuery f = MockQuery (fmap pure . f)
136+
137+
-- | A helper for mocking a 'Database.Persist.Monad.Shim.selectSource' or
138+
-- 'Database.Persist.Monad.Shim.selectSourceRes' call.
139+
--
140+
-- Usage:
141+
--
142+
-- @
143+
-- mockSelectSource $ \\filters opts ->
144+
-- if null filters && null opts
145+
-- then
146+
-- let person1 = [Entity (toSqlKey 1) $ Person \"Alice\"]
147+
-- person2 = [Entity (toSqlKey 2) $ Person \"Bob\"]
148+
-- in Just [person1, person2]
149+
-- else Nothing
150+
-- @
151+
mockSelectSource :: forall record. Typeable record => ([Filter record] -> [SelectOpt record] -> Maybe [Entity record]) -> MockQuery
152+
mockSelectSource f = withRecord @record $ \case
153+
SelectSourceRes filters opts ->
154+
let toAcquire entities = Acquire.mkAcquire (pure $ Conduit.yieldMany entities) (\_ -> pure ())
155+
in toAcquire <$> f filters opts
156+
_ -> Nothing
157+
158+
-- | A helper for mocking a 'Database.Persist.Monad.Shim.selectKeys' or
159+
-- 'Database.Persist.Monad.Shim.selectKeysRes' call.
160+
--
161+
-- Usage:
162+
--
163+
-- @
164+
-- mockSelectKeys $ \\filters opts ->
165+
-- if null filters && null opts
166+
-- then Just $ map toSqlKey [1, 2]
167+
-- else Nothing
168+
-- @
169+
mockSelectKeys :: forall record. Typeable record => ([Filter record] -> [SelectOpt record] -> Maybe [Key record]) -> MockQuery
170+
mockSelectKeys f = withRecord @record $ \case
171+
SelectKeysRes filters opts ->
172+
let toAcquire keys = Acquire.mkAcquire (pure $ Conduit.yieldMany keys) (\_ -> pure ())
173+
in toAcquire <$> f filters opts
174+
_ -> Nothing
175+
176+
-- | A helper for mocking a 'Database.Persist.Monad.Shim.withRawQuery' call.
177+
--
178+
-- Usage:
179+
--
180+
-- @
181+
-- mockWithRawQuery $ \\sql vals ->
182+
-- if sql == "SELECT id, name FROM person"
183+
-- then
184+
-- let row1 = [toPersistValue 1, toPersistValue \"Alice\"]
185+
-- row2 = [toPersistValue 2, toPersistValue \"Bob\"]
186+
-- in Just [row1, row2]
187+
-- else Nothing
188+
-- @
189+
mockWithRawQuery :: (Text -> [PersistValue] -> Maybe [[PersistValue]]) -> MockQuery
190+
mockWithRawQuery f = MockQuery $ \case
191+
WithRawQuery sql vals conduit ->
192+
let outputRows rows = Conduit.runConduit $ Conduit.yieldMany rows .| conduit
193+
in outputRows <$> f sql vals
194+
_ -> Nothing
195+
196+
-- | A helper for mocking a 'Database.Persist.Monad.Shim.rawQuery' or
197+
-- 'Database.Persist.Monad.Shim.rawQueryRes' call.
198+
--
199+
-- Usage:
200+
--
201+
-- @
202+
-- mockRawQuery $ \\sql vals ->
203+
-- if sql == "SELECT id, name FROM person"
204+
-- then
205+
-- let row1 = [toPersistValue 1, toPersistValue \"Alice\"]
206+
-- row2 = [toPersistValue 2, toPersistValue \"Bob\"]
207+
-- in Just [row1, row2]
208+
-- else Nothing
209+
-- @
210+
mockRawQuery :: (Text -> [PersistValue] -> Maybe [[PersistValue]]) -> MockQuery
211+
mockRawQuery f = MockQuery $ \case
212+
RawQueryRes sql vals ->
213+
let toAcquire rows = Acquire.mkAcquire (pure $ Conduit.yieldMany rows) (\_ -> pure ())
214+
in pure . toAcquire <$> f sql vals
215+
_ -> Nothing
216+
217+
-- | A helper for mocking a 'Database.Persist.Monad.Shim.rawSql' call.
218+
--
219+
-- Usage:
220+
--
221+
-- @
222+
-- mockRawSql $ \\sql vals ->
223+
-- if sql == "SELECT id, name FROM person"
224+
-- then
225+
-- let row1 = [toPersistValue 1, toPersistValue \"Alice\"]
226+
-- row2 = [toPersistValue 2, toPersistValue \"Bob\"]
227+
-- in Just [row1, row2]
228+
-- else Nothing
229+
-- @
230+
mockRawSql :: (Text -> [PersistValue] -> Maybe [[PersistValue]]) -> MockQuery
231+
mockRawSql f = MockQuery $ \case
232+
RawSql sql vals ->
233+
let fromRow = either (error . Text.unpack) id . rawSqlProcessRow
234+
in pure . map fromRow <$> f sql vals
235+
_ -> Nothing

0 commit comments

Comments
 (0)