Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation for using wai-hspec with other resources #36

Open
jml opened this issue Jul 15, 2016 · 8 comments
Open

Documentation for using wai-hspec with other resources #36

jml opened this issue Jul 15, 2016 · 8 comments

Comments

@jml
Copy link

jml commented Jul 15, 2016

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
Copy link
Author

jml commented Jul 16, 2016

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
Copy link
Author

jml commented Jul 16, 2016

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

@sol
Copy link
Member

sol commented Jul 17, 2016

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
Copy link
Member

sol commented Jul 17, 2016

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
Copy link
Member

sol commented Jul 17, 2016

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
Copy link
Member

sol commented Jul 17, 2016

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.

@jml
Copy link
Author

jml commented Jul 17, 2016

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

@jml
Copy link
Author

jml commented Jul 24, 2016

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 added a commit to jml/holborn that referenced this issue Sep 22, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants