packages feed

hspec-snap 0.2.0.0 → 0.3.0.0

raw patch · 4 files changed

+283/−56 lines, 4 filesdep ~hspec-snapPVP ok

version bump matches the API change (PVP)

Dependency ranges changed: hspec-snap

API changes (from Hackage documentation)

- Test.Hspec.Snap: afterAll :: IO () -> SpecWith a -> SpecWith a
+ Test.Hspec.Snap: class Factory b a d | a -> b, a -> d, d -> a where create transform = save $ transform fields reload = return
+ Test.Hspec.Snap: class HasSession b
+ Test.Hspec.Snap: create :: Factory b a d => (d -> d) -> SnapHspecM b a
+ Test.Hspec.Snap: fields :: Factory b a d => d
+ Test.Hspec.Snap: getSessionLens :: HasSession b => SnapletLens b SessionManager
+ Test.Hspec.Snap: recordSession :: HasSession b => SnapHspecM b a -> SnapHspecM b a
+ Test.Hspec.Snap: reload :: Factory b a d => a -> SnapHspecM b a
+ Test.Hspec.Snap: save :: Factory b a d => d -> SnapHspecM b a
+ Test.Hspec.Snap: sessionShouldContain :: Text -> SnapHspecM b ()
+ Test.Hspec.Snap: sessionShouldNotContain :: Text -> SnapHspecM b ()
- Test.Hspec.Snap: SnapHspecState :: Result -> (Handler b b ()) -> (Snaplet b) -> (InitializerState b) -> SnapHspecState b
+ Test.Hspec.Snap: SnapHspecState :: Result -> (Handler b b ()) -> (Snaplet b) -> (InitializerState b) -> (MVar [(Text, Text)]) -> (Handler b b ()) -> (Handler b b ()) -> SnapHspecState b

Files

CHANGELOG view
@@ -1,1 +1,10 @@+0.3.0.0 - 2014-10-18 - Add support for factories, and no longer export `afterAll`.+	A more correct implementation of `afterAll` is currently in hspec2+	HEAD, and will be released soon (and we don't want to have to do+	another major version when it does).++0.2.0.0 - 2014-9-24 - Change order of parameters in predicates that take two arguments+	to make pattern of `get req >>= shouldHaveProp foo` work (vs needing to call `flip`).+	And add more predicates.+ 0.1.0.0 - 2014-9-3 - Initial release.
hspec-snap.cabal view
@@ -1,15 +1,18 @@ name:                hspec-snap-version:             0.2.0.0+version:             0.3.0.0 synopsis:            A library for testing with Hspec and the Snap Web Framework homepage:            https://github.com/dbp/hspec-snap license:             BSD3 license-file:        LICENSE author:              Daniel Patterson maintainer:          dbp@dbpmail.net-extra-source-files: CHANGELOG+extra-source-files:  CHANGELOG category:            Web, Snap build-type:          Simple cabal-version:       >=1.8+source-repository head+  type:     git+  location: https://github.com/dbp/hspec-snap  library   exposed-modules:@@ -47,4 +50,4 @@                      , snap-core >= 0.9 && < 0.10                      , text >= 0.11 && < 1.2                      , transformers >= 0.3 && < 0.4-    build-depends: hspec-snap == 0.2.0.0+    build-depends: hspec-snap == 0.3.0.0
spec/Main.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE GADTs                     #-}+{-# LANGUAGE MultiParamTypeClasses     #-} {-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE OverloadedStrings         #-} {-# LANGUAGE TemplateHaskell           #-}@@ -9,32 +10,70 @@ ---------------------------------------------------------- -- Section 0: Imports.                                  -- -----------------------------------------------------------import           Control.Applicative     ((<$>), (<*>))+import           Control.Applicative                         ((<$>), (<*>))+import           Control.Concurrent.MVar                     (MVar, isEmptyMVar,+                                                              newEmptyMVar,+                                                              newMVar, putMVar,+                                                              takeMVar,+                                                              tryPutMVar,+                                                              tryTakeMVar) import           Control.Lens-import           Data.ByteString         (ByteString)-import qualified Data.Map                as M-import           Data.Maybe              (fromMaybe)-import           Data.Text               (Text)-import qualified Data.Text               as T-import           Snap                    (Handler, Method (..), SnapletInit,-                                          addRoutes, getParam, liftIO,-                                          makeSnaplet, method, route, void,-                                          writeBS, writeText)+import           Control.Monad                               (when)+import           Data.ByteString                             (ByteString)+import           Data.Map                                    (Map)+import qualified Data.Map                                    as M+import           Data.Maybe                                  (fromMaybe)+import           Data.Text                                   (Text)+import qualified Data.Text                                   as T+import qualified Data.Text.Encoding                          as T+import           Snap                                        (Handler,+                                                              Method (..),+                                                              Snaplet,+                                                              SnapletInit,+                                                              addRoutes,+                                                              getParam, liftIO,+                                                              makeSnaplet,+                                                              method,+                                                              nestSnaplet,+                                                              route, void, with,+                                                              writeBS,+                                                              writeText) import qualified Snap+import           Snap.Snaplet.Session+import           Snap.Snaplet.Session.Backends.CookieSession+import           System.Directory                            (doesFileExist,+                                                              removeFile)+import           System.IO import           Text.Digestive -import           Control.Concurrent.MVar (MVar, isEmptyMVar, newEmptyMVar,-                                          tryPutMVar, tryTakeMVar) import           Test.Hspec import           Test.Hspec.Snap  ---------------------------------------------------------- -- Section 1: Example application used for testing.     -- -----------------------------------------------------------data App = App { _mv :: MVar () }+data Foo = Foo Int String String+data App = App { _mv :: MVar (), _store :: MVar (Map Int Foo), _sess :: Snaplet SessionManager }  makeLenses ''App +newFoo :: String -> String -> Handler App App Foo+newFoo s1 s2 = do smvar <- use store+                  mp <- liftIO $ takeMVar smvar+                  let i = 1 + M.size mp+                  let foo = Foo i s1 s2+                  liftIO $ putMVar smvar (M.insert i foo mp)+                  return foo++lookupFoo :: Int -> Handler App App (Maybe Foo)+lookupFoo i = do smvar <- use store+                 mp <- liftIO $ takeMVar smvar+                 liftIO $ putMVar smvar mp+                 return (M.lookup i mp)++instance HasSession App where+  getSessionLens = sess+ html :: Text html = "<table><tr><td>One</td><td>Two</td></tr></table>" @@ -52,21 +91,37 @@          ,("/setmv", do m <- use mv                         void $ liftIO $ tryPutMVar m ()                         return ())+         ,("/setsess/:k", do Just k <- fmap T.decodeUtf8 <$> getParam "k"+                             with sess $ setInSession k "bar" >> commitSession+                             writeText "")+         ,("/getsess/:k", do Just k <- fmap T.decodeUtf8 <$> getParam "k"+                             Just r <- with sess $ getFromSession k+                             writeText r)          ] -app :: MVar () -> SnapletInit App App-app mvar = makeSnaplet "app" "An snaplet example application." Nothing $ do-         addRoutes routes-         return (App mvar)+app :: MVar (Map Int Foo) -> MVar () -> SnapletInit App App+app state mvar = makeSnaplet "app" "An snaplet example application." Nothing $ do+   addRoutes routes+   s <- nestSnaplet "sess" sess $ initCookieSessionManager "site_key.txt" "sess" (Just 3600)+   Snap.onUnload (do e <- doesFileExist "site_key.txt"+                     when e $ removeFile "site_key.txt")+   return (App mvar state s)   ---------------------------------------------------------- -- Section 2: Test suite against application.           -- ---------------------------------------------------------- -tests :: MVar () -> Spec-tests mvar =-  snap (route routes) (app mvar) $ do+newtype FooFields = FooFields (IO String)++instance Factory App Foo FooFields where+  fields = FooFields (return "default")+  save (FooFields as) = do s <- liftIO as+                           eval (newFoo s "const")++tests :: MVar (Map Int Foo) -> MVar () -> Spec+tests store mvar =+  snap (route routes) (app store mvar) $ do     describe "requests" $ do       it "should match selector from a GET request" $ do         p <- get "/test"@@ -116,6 +171,42 @@         form (ErrorPaths ["a"]) testForm (M.fromList [])       it "should call predicates on valid data" $ do         form (Predicate (("oo" `T.isInfixOf`) . fst)) testForm (M.fromList [("a", "foobar")])+    describe "sessions" $ do+      it "should be able to modify session in handlers" $+        recordSession $ do get "/setsess/4"+                           sessionShouldContain "4"+                           sessionShouldContain "bar"+      it "should be able to modify session with eval" $+        recordSession $ do eval (with sess $ setInSession "foozlo" "bar" >> commitSession)+                           sessionShouldContain "foozlo"+                           sessionShouldContain "bar"+      it "should be able to persist sessions between requests" $+        recordSession $ do get "/setsess/3"+                           get "/getsess/3" >>= shouldHaveText "bar"+      it "should be able to persist sessions between eval and requests" $+        recordSession $ do eval (with sess $ setInSession "2" "bar" >> commitSession)+                           get "/getsess/2" >>= shouldHaveText "bar"+      it "should be able to persist sessions between requests and eval" $+        recordSession $ do get "/setsess/1"+                           eval (with sess $ getFromSession "1" ) >>= shouldEqual (Just "bar")+      it "should be able to persist sessions between eval and eval" $+        recordSession $ do eval (with sess $ setInSession "foofoo" "bar" >> commitSession)+                           eval (with sess $ getFromSession "foofoo" ) >>= shouldEqual (Just "bar")+      it "should be able to remove stuff from session" $+        recordSession $ do eval (with sess $ setInSession "foobar" "baz" >> commitSession)+                           sessionShouldContain "foobar"+                           eval (with sess $ deleteFromSession "foobar" >> commitSession)+                           sessionShouldNotContain "foobar"+    describe "factories" $ do+      it "should be able to generate a foo" $+        do (Foo i _ _) <- create id+           Just (Foo _ _ s) <- eval (lookupFoo i)+           s `shouldEqual` "const"+      it "should be able to modify defaulted values" $+        do (Foo _ s _) <- create (\_ -> FooFields (return "Hi!"))+           s `shouldEqual` "Hi!"+           (Foo _ s _) <- create id+           s `shouldNotEqual` "Hi!"   ----------------------------------------------------------@@ -124,4 +215,5 @@ main :: IO () main = do   mvar <- newEmptyMVar-  hspec (tests mvar)+  store <- newMVar M.empty+  hspec (tests store mvar)
src/Test/Hspec/Snap.hs view
@@ -1,9 +1,13 @@-{-# LANGUAGE FlexibleInstances    #-}-{-# LANGUAGE OverloadedStrings    #-}-{-# LANGUAGE ScopedTypeVariables  #-}-{-# LANGUAGE TupleSections        #-}-{-# LANGUAGE TypeFamilies         #-}-{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE DataKinds              #-}+{-# LANGUAGE FlexibleContexts       #-}+{-# LANGUAGE FlexibleInstances      #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE MultiParamTypeClasses  #-}+{-# LANGUAGE OverloadedStrings      #-}+{-# LANGUAGE ScopedTypeVariables    #-}+{-# LANGUAGE TupleSections          #-}+{-# LANGUAGE TypeFamilies           #-}+{-# LANGUAGE TypeSynonymInstances   #-}  module Test.Hspec.Snap (   -- * Running blocks of hspec-snap tests@@ -17,8 +21,8 @@   , TestResponse(..)   , SnapHspecM -  -- * General Hspec helpers-  , afterAll+  -- * Factory style test data generation+  , Factory(..)    -- * Requests   , get@@ -29,6 +33,12 @@   -- * Helpers for dealing with TestResponses   , restrictResponse +  -- * Dealing with session state (EXPERIMENTAL)+  , recordSession+  , HasSession(..)+  , sessionShouldContain+  , sessionShouldNotContain+   -- * Evaluating application code   , eval @@ -66,9 +76,10 @@   , evalHandlerSafe   ) where -import           Control.Applicative     ((<$>))-import           Control.Concurrent.MVar (modifyMVar, newEmptyMVar, newMVar,-                                          putMVar, takeMVar)+import           Control.Applicative     ((<$>), (<|>))+import           Control.Concurrent.MVar (MVar, modifyMVar, newEmptyMVar,+                                          newMVar, putMVar, readMVar, takeMVar,+                                          tryTakeMVar) import           Control.Exception       (SomeException, catch) import           Control.Monad           (void) import           Control.Monad.State     (StateT (..), runStateT)@@ -82,7 +93,11 @@ import qualified Data.Text.Encoding      as T import           Snap.Core               (Response (..), getHeader) import qualified Snap.Core               as Snap-import           Snap.Snaplet            (Handler, Snaplet, SnapletInit)+import           Snap.Snaplet            (Handler, Snaplet, SnapletInit,+                                          SnapletLens, with)+import           Snap.Snaplet.Session    (SessionManager, commitSession,+                                          sessionToList, setInSession,+                                          withSession) import           Snap.Snaplet.Test       (InitializerState, closeSnaplet,                                           evalHandler', getSnaplet, runHandler') import           Snap.Test               (RequestBuilder, getResponseBody)@@ -104,17 +119,59 @@ type SnapHspecM b = StateT (SnapHspecState b) IO  -- | Internal state used to share site initialization across tests, and to propogate failures.-data SnapHspecState b = SnapHspecState Result (Handler b b ()) (Snaplet b) (InitializerState b)+-- Understanding it is completely unnecessary to use the library.+--+-- The fields it contains, in order, are:+--+-- > Result+-- > Main handler+-- > Startup state+-- > Startup state+-- > Session state+-- > Before handler (runs before each eval)+-- > After handler (runs after each eval).+data SnapHspecState b = SnapHspecState Result+                                       (Handler b b ())+                                       (Snaplet b)+                                       (InitializerState b)+                                       (MVar [(Text, Text)])+                                       (Handler b b ())+                                       (Handler b b ())   instance Example (SnapHspecM b ()) where   type Arg (SnapHspecM b ()) = SnapHspecState b   evaluateExample s _ cb _ =     do mv <- newEmptyMVar-       cb $ \st@(SnapHspecState _ _ _ _) -> do ((),(SnapHspecState r' _ _ _)) <- runStateT s st-                                               putMVar mv r'+       cb $ \st@(SnapHspecState _ _ _ _ _ _ _) ->+              do ((),(SnapHspecState r' _ _ _ _ _ _)) <- runStateT s st+                 putMVar mv r'        takeMVar mv +-- | Factory instances allow you to easily generate test data.+--+-- Essentially, you specify a default way of constructing a+-- data type, and allow certain parts of it to be modified (via+-- the 'fields' data structure).+--+-- An example follows:+--+-- > data Foo = Foo Int+-- > newtype FooFields = FooFields (IO Int)+-- > instance Factory App Foo FooFields where+-- >   fields = randomIO+-- >   save f = liftIO f >>= saveFoo . Foo1+-- >+-- > main = do create id :: SnapHspecM App Foo+-- >           create (const $ FooFields (return 1)) :: SnapHspecM App Foo+class Factory b a d | a -> b, a -> d, d -> a where+  fields :: d+  save :: d -> SnapHspecM b a+  create :: (d -> d) -> SnapHspecM b a+  create transform = save $ transform fields+  reload :: a -> SnapHspecM b a+  reload = return+ -- | Runs a given action once after all the tests in the given block have run. -- -- __Warning__: Due to current limitations to how Hspec works, this only works@@ -169,11 +226,12 @@ snap :: Handler b b () -> SnapletInit b b -> SpecWith (SnapHspecState b) -> Spec snap site app spec = do   snapinit <- runIO $ getSnaplet (Just "test") app+  mv <- runIO (newMVar [])   case snapinit of     Left err -> error $ show err     Right (snaplet, initstate) -> do       afterAll (closeSnaplet initstate) $-        before (return (SnapHspecState Success site snaplet initstate)) spec+        before (return (SnapHspecState Success site snaplet initstate mv (return ()) (return ()))) spec  -- | This allows you to change the default handler you are running -- requests against within a block. This is most likely useful for@@ -181,8 +239,8 @@ modifySite :: (Handler b b () -> Handler b b ())            -> SpecWith (SnapHspecState b)            -> SpecWith (SnapHspecState b)-modifySite f = beforeWith (\(SnapHspecState r site snaplet initst) ->-                             return (SnapHspecState r (f site) snaplet initst))+modifySite f = beforeWith (\(SnapHspecState r site snaplet initst sess bef aft) ->+                             return (SnapHspecState r (f site) snaplet initst sess bef aft))  -- | This performs a similar operation to `modifySite` but in the context -- of `SnapHspecM` (which is needed if you need to `eval`, produce values, and@@ -190,19 +248,63 @@ modifySite' :: (Handler b b () -> Handler b b ())             -> SnapHspecM b a             -> SnapHspecM b a-modifySite' f a = do (SnapHspecState r site s i) <- S.get-                     S.put (SnapHspecState r (f site) s i)+modifySite' f a = do (SnapHspecState r site s i sess bef aft) <- S.get+                     S.put (SnapHspecState r (f site) s i sess bef aft)                      a  -- | Evaluate a Handler action after each test. afterEval :: Handler b b () -> SpecWith (SnapHspecState b) -> SpecWith (SnapHspecState b)-afterEval h = after (\(SnapHspecState r site s i) -> void $ evalHandlerSafe h s i)+afterEval h = after (\(SnapHspecState r site s i _ _ _) ->+                       do res <- evalHandlerSafe h s i+                          case res of+                            Right _ -> return ()+                            Left msg -> liftIO $ print msg)  -- | Evaluate a Handler action before each test. beforeEval :: Handler b b () -> SpecWith (SnapHspecState b) -> SpecWith (SnapHspecState b)-beforeEval h = beforeWith (\state@(SnapHspecState r site s i) -> do void $ evalHandlerSafe h s i-                                                                    return state)+beforeEval h = beforeWith (\state@(SnapHspecState r site s i _ _ _) -> do void $ evalHandlerSafe h s i+                                                                          return state) +class HasSession b where+  getSessionLens :: SnapletLens b SessionManager++recordSession :: HasSession b => SnapHspecM b a -> SnapHspecM b a+recordSession a =+  do st@(SnapHspecState r site s i mv bef aft) <- S.get+     S.put (SnapHspecState r site s i mv+                             (do ps <- liftIO $ readMVar mv+                                 with getSessionLens $ mapM_ (uncurry setInSession) ps+                                 with getSessionLens commitSession)+                             (do ps' <- with getSessionLens sessionToList+                                 liftIO $ takeMVar mv+                                 liftIO $ putMVar mv ps'))+     res <- a+     (SnapHspecState r' _ _ _ _ _ _) <- S.get+     liftIO $ takeMVar mv+     liftIO $ putMVar mv []+     S.put (SnapHspecState r' site s i mv bef aft)+     return res++sessionShouldContain :: Text -> SnapHspecM b ()+sessionShouldContain t =+  do (SnapHspecState _ _ _ _ mv _ _) <- S.get+     ps <- liftIO $ readMVar mv+     let contents = T.concat (map (uncurry T.append) ps)+     if t `T.isInfixOf` contents+       then setResult Success+       else setResult (Fail $ "Session did not contain: " ++ (T.unpack t)+                            ++ "\n\nSession was:\n" ++ (T.unpack contents))++sessionShouldNotContain :: Text -> SnapHspecM b ()+sessionShouldNotContain t =+  do (SnapHspecState _ _ _ _ mv _ _) <- S.get+     ps <- liftIO $ readMVar mv+     let contents = T.concat (map (uncurry T.append) ps)+     if t `T.isInfixOf` contents+       then setResult (Fail $ "Session should not have contained: " ++ (T.unpack t)+                            ++ "\n\nSession was:\n" ++ (T.unpack contents))+       else setResult Success+ -- | Runs a GET request. get :: Text -> SnapHspecM b TestResponse get path = get' path M.empty@@ -231,16 +333,19 @@  -- | Runs an arbitrary stateful action from your application. eval :: Handler b b a -> SnapHspecM b a-eval act = do (SnapHspecState _ _ app is) <- S.get-              liftIO $ fmap (either (error . T.unpack) id) $ evalHandlerSafe act app is+eval act = do (SnapHspecState _ site app is mv bef aft) <- S.get+              liftIO $ fmap (either (error . T.unpack) id) $ evalHandlerSafe (do bef+                                                                                 r <- act+                                                                                 aft+                                                                                 return r) app is   -- | Records a test Success or Fail. Only the first Fail will be -- recorded (and will cause the whole block to Fail). setResult :: Result -> SnapHspecM b ()-setResult r = do (SnapHspecState r' s a i) <- S.get+setResult r = do (SnapHspecState r' s a i sess bef aft) <- S.get                  case r' of-                   Success -> S.put (SnapHspecState r s a i)+                   Success -> S.put (SnapHspecState r s a i sess bef aft)                    _ -> return ()  -- | Asserts that a given stateful action will produce a specific different result after@@ -389,11 +494,29 @@   do r <- eval $ DF.postForm "form" theForm (const $ return lookupParam)      case expected of        Value a -> shouldEqual (snd r) (Just a)-       Predicate f -> shouldBeTrue (isJust (snd r) && f (fromJust (snd r)))+       Predicate f ->+         case snd r of+           Nothing -> setResult (Fail $ T.unpack $+                                 T.append "Expected form to validate. Resulted in errors: "+                                          (T.pack (show $ DF.viewErrors $ fst r)))+           Just v -> case f v of+                      True -> setResult Success+                      False -> setResult (Fail $ T.unpack $+                                          T.append "Expected predicate to pass on value: "+                                                   (T.pack (show v)))        ErrorPaths expectedPaths ->          do let viewErrorPaths = map (DF.fromPath . fst) $ DF.viewErrors $ fst r-            shouldBeTrue (all (`elem` viewErrorPaths) expectedPaths-                          && (length viewErrorPaths == length expectedPaths))+            if all (`elem` viewErrorPaths) expectedPaths+               then if length viewErrorPaths == length expectedPaths+                       then setResult Success+                       else setResult (Fail $ "Number of errors did not match test. Got:\n\n "+                                            ++ (show viewErrorPaths)+                                            ++ "\n\nBut expected:\n\n"+                                            ++ (show expectedPaths))+               else setResult (Fail $ "Did not have all errors specified. Got:\n\n"+                                    ++ (show viewErrorPaths)+                                    ++ "\n\nBut expected:\n\n"+                                    ++ (show expectedPaths))   where lookupParam pth = case M.lookup (DF.fromPath pth) fixedParams of                             Nothing -> return []                             Just v -> return [DF.TextInput v]@@ -402,8 +525,8 @@ -- | Runs a request (built with helpers from Snap.Test), resulting in a response. runRequest :: RequestBuilder IO () -> SnapHspecM b TestResponse runRequest req = do-  (SnapHspecState _ site app is) <- S.get-  res <- liftIO $ runHandlerSafe req site app is+  (SnapHspecState _ site app is _ bef aft) <- S.get+  res <- liftIO $ runHandlerSafe req (bef >> site >> aft) app is   case res of     Left err -> do       error $ T.unpack err