hspec-wai
hspec-wai copied to clipboard
Documentation for using wai-hspec with other resources
I'd like to use hspec-wai in the following way:
- create a database before all my tests
- use that database to construct my wai application
- around each test, reset the database
- inside some tests, access the database directly to do some set up that's not supported by my public REST API
I think this is a pretty standard set of things to want to test. However, it's really hard to figure out how to do this with hspec & hspec-wai. I'm cobbling something together now (will post if I finish it), but it has taken a lot of time to figure it out, with a lot of searching down dead ends.
I would really appreciate concrete examples for doing the above. I think they'd make a good addition to the documentation.
To clarify, I've read the writing specs tutorial, but it doesn't answer questions like how to:
- do something before all tests (answer: hspec has
beforeAll
& friends) - pass the result of that to other test hooks, especially the
WaiSession
hook (answer: don't usebeforeAll
, because there's no way of passing it around short of performing unusually sophisticated type system invocations) - combine the
with app
hook with other hooks (e.g. to reset the db) without changing the type of the expression - query the DB in tests in addition to doing web requests. (I think this is just
liftIO
but I've not got there yet).
If all of this seems obvious, then that's exactly why publishing an example would help so much. What's obvious to one is often murky to another.
Doesn't reset the DB around each test yet.
waiTests :: IO TestTree
waiTests = do
dbVar <- newEmptyMVar
testSpec "wai-tests" $ beforeAll_ (startDB dbVar) $ afterAll_ (stopDB dbVar) $ (before (makeTestApp <$> getConfig dbVar)) $ do
describe "/my-endpoint" $ do
it "loves being posted to" $ do
config <- liftIO (getConfig dbVar)
user <- makeArbitraryUser config
post "/my-endpoint"
(fromValue $ object [ "name" .= (userName user), "message" .= "peace and love" ])
`shouldRespondWith`
(fromValue $ object [ "response" .= "mostly agree" ])
where
startDB var = do
db <- makeDatabase "path/to/schema.sql"
putMVar var db
stopDB var = do
db <- takeMVar var
stopPostgres db
getConfig var = do
db <- readMVar var
pure $ Config { dbConnection = connection db }
-- | Config is the configuration for the whole WAI app, specific to this app.
data Config = Config { dbConnection :: Connection }
-- | User is custom user object specific to this app.
data User = User { userName :: Text }
-- | makeTestApp constructs our application from its configuration.
makeTestApp :: Config -> Application
makeTestApp = serve api (server config)
-- | makeArbitrary user inserts a new user into the database and returns the newly-created user
makeArbitraryUser :: MonadIO => Config -> m User
makeArbitraryUser config = undefined -- use `config` to insert something into the database
Hi, sorry for the late reply. I don't have much time to put into this, but here are some pointers.
Before we start, a general disclaimer (you are probably already aware of this, but I leave it here for others who might read this in the future):
- Using
beforeAll
is considered bad style, as it may make your specs order dependent. - Using internals (e.g. a database connection) in acceptance tests is considered bad style. Acceptance tests should only test user visible features and not rely on implementation details.
Currently hspec
only allows you to carry around one value. If you want to initialize a Connection
with beforeAll
and later transform it to an Application
you can use aroundWith
for that. You can also use aroundWith
to use your Connection
without transforming it.
If you want to use the connection within an acceptance test things get more complicated. You can't use hspec-wai
's magic in that scenario. Instead you have to use Test.Hspec.Wai.Internal.withApplication
directly.
You can try to piece things together by following the types.
Example for using aroundWith
:
{-# LANGUAGE OverloadedStrings #-}
module AppSpec (spec) where
import Test.Hspec
import Test.Hspec.Wai
import Network.Wai
data Connection
mkApplication :: Connection -> Application
mkApplication = undefined
newConnection :: IO Connection
newConnection = undefined
closeConnection :: Connection -> IO ()
closeConnection = undefined
createUser :: Connection -> IO ()
createUser = undefined
withApplication :: ActionWith Application -> ActionWith Connection
withApplication action connection = do
action (mkApplication connection)
withConnection :: ActionWith Connection -> (Connection -> IO ()) -> ActionWith Connection
withConnection action f connection = do
f connection
action connection
spec :: Spec
spec = beforeAll newConnection $ afterAll closeConnection $ do
aroundWith (withConnection createUser) $ do
aroundWith withApplication $ do
describe "/" $ do
it "responds with 200" $ do
get "/" `shouldRespondWith` 200
Example for using withApplication
:
{-# LANGUAGE OverloadedStrings #-}
module AppSpec (spec) where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.Internal
import Network.Wai
data Connection
mkApplication :: Connection -> Application
mkApplication = undefined
newConnection :: IO Connection
newConnection = undefined
closeConnection :: Connection -> IO ()
closeConnection = undefined
createUser :: Connection -> IO ()
createUser = undefined
spec :: Spec
spec = beforeAll newConnection $ afterAll closeConnection $ do
describe "/" $ do
it "responds with 200" $ \connection -> do
createUser connection
withApplication (mkApplication connection) $ do
get "/" `shouldRespondWith` 200
One more note, a convenient way to reset the database into a pristine state is to create a transaction before each test and do a rollback after.
Thank you! It'll take me a little while to digest those, but they're very much appreciated.
Finally got back to this. It all pieces together now. I don't think I would ever have figured out aroundWith
without an example—it runs backwards to my initial intuitions.
Thank you.