haskellscottyhspec

Multiple before functions in HSpec?


I have an in-memory repository that I can create by calling this function:

newEmptyRepository :: IO InMemoryGameRepository

where InMemoryGameRepository is defined like this:

type State = (HashMap GameId Game)
type IORefState = IORef State
newtype InMemoryGameRepository = InMemoryGameRepository IORefState

When writing tests for my Scotty application I've seen examples of using this approach:

spec =
  before app $ do
    describe "GET /" $ do
      it "responds with 200" $ get "/" `shouldRespondWith` 200
      it "responds with 'hello'" $ get "/" `shouldRespondWith` "hello"
    ...

This is all fine but I need to somehow also initialize the InMemoryGameRepository (by calling newEmptyRepository) and use the created instance in my tests. Thus I've changed app to:

app :: InMemoryGameRepository -> IO Application
app repo = scottyApp $ routes repo

And I'm trying to create a test that uses the repository AND the IO Application, for example like this (which doesn't work):

spec = 
    before (do repo <- newEmptyRepository
               app repo) $ 
      -- API Tests
      describe "GET /api/games" $ 
        it "responds with " $ do
          liftIO $ startGame repo
          get "/api/games" `shouldRespondWith` singleGameResponse

where startGame is defined like this:

startGame :: InMemoryGameRepository -> IO Game

Here the compiler says (obviously) that repo is not in scope. But how can I achieve this? I.e. I want to share a single instance of newEmptyRepository both for the app and in the test?

Ps: you can see the full application on github.


Solution

  • You should use beforeWith which has the type

    beforeWith :: (b -> IO a) -> SpecWith a -> SpecWith b
    

    Use it as e.g. before newEmptyRepository . beforeWith app whose type is SpecWith Application -> Spec.

    If you want to access both the InMemoryGameRepository and the Application in your test cases, defined a helper function

    withArg f a = (,) a <$> f a
    withArg :: Functor f => (t -> f b) -> t -> f (t, b)
    

    then use

    before newEmptyRepository . beforeWith (withArg app)
        :: SpecWith (InMemoryGameRepository, Application) -> Spec
    

    Finally, you shouldn't use liftIO $ startGame repo in the definition of your tests - this runs startGame every time the test tree is built (although, this may actually be what you want, it doesn't seem to be the case). Instead, if you use the before family of functions, startGame will run once before the tests are actually run. You can even access the Game returned by startGame using the same technique as above:

      before newEmptyRepository 
    . beforeWith (withArg startGame) 
    . beforeWith (withArg $ app . fst)
    :: SpecWith ((InMemoryGameRepository, Game), Application) -> Spec