hspec-wai icon indicating copy to clipboard operation
hspec-wai copied to clipboard

Documentation for using wai-hspec with other resources

Open jml opened this issue 8 years ago • 8 comments

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.

jml avatar Jul 15 '16 21:07 jml

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 use beforeAll, 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.

jml avatar Jul 16 '16 08:07 jml

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

jml avatar Jul 16 '16 10:07 jml

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):

  1. Using beforeAll is considered bad style, as it may make your specs order dependent.
  2. 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.

sol avatar Jul 17 '16 06:07 sol

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                                  

sol avatar Jul 17 '16 07:07 sol

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

sol avatar Jul 17 '16 07:07 sol

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.

sol avatar Jul 17 '16 07:07 sol

Thank you! It'll take me a little while to digest those, but they're very much appreciated.

jml avatar Jul 17 '16 08:07 jml

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.

jml avatar Jul 24 '16 20:07 jml