Document much of yesod-test, especially the "Making Requests" section

This commit is contained in:
Maximilian Tagher 2015-01-25 15:20:25 -08:00
parent 3f20c759dc
commit cb4785cf28
2 changed files with 171 additions and 36 deletions

View File

@ -10,7 +10,7 @@ using css selectors.
You can also easily build requests using forms present in the current page. You can also easily build requests using forms present in the current page.
This is very useful for testing web applications built in yesod for example, This is very useful for testing web applications built in yesod for example,
were your forms may have field names generated by the framework or a randomly where your forms may have field names generated by the framework or a randomly
generated "\_token" field. generated "\_token" field.
Your database is also directly available so you can use runDB to set up Your database is also directly available so you can use runDB to set up

View File

@ -8,19 +8,19 @@
Yesod.Test is a pragmatic framework for testing web applications built Yesod.Test is a pragmatic framework for testing web applications built
using wai and persistent. using wai and persistent.
By pragmatic I may also mean 'dirty'. It's main goal is to encourage integration By pragmatic I may also mean 'dirty'. Its main goal is to encourage integration
and system testing of web applications by making everything /easy to test/. and system testing of web applications by making everything /easy to test/.
Your tests are like browser sessions that keep track of cookies and the last Your tests are like browser sessions that keep track of cookies and the last
visited page. You can perform assertions on the content of HTML responses, visited page. You can perform assertions on the content of HTML responses,
using css selectors to explore the document more easily. using CSS selectors to explore the document more easily.
You can also easily build requests using forms present in the current page. You can also easily build requests using forms present in the current page.
This is very useful for testing web applications built in yesod for example, This is very useful for testing web applications built in yesod, for example,
were your forms may have field names generated by the framework or a randomly where your forms may have field names generated by the framework or a randomly
generated '_nonce' field. generated nonce value.
Your database is also directly available so you can use runDBRunner to set up Your database is also directly available so you can use 'runDB' to set up
backend pre-conditions, or to assert that your session is having the desired effect. backend pre-conditions, or to assert that your session is having the desired effect.
-} -}
@ -38,12 +38,12 @@ module Yesod.Test
, yit , yit
-- * Making requests -- * Making requests
-- | To make a request you need to point to an url and pass in some parameters. -- | You can construct requests with the 'RequestBuilder' monad, which lets you
-- -- set the URL and add parameters, headers, and files. Helper functions are provided to
-- To build your parameters you will use the RequestBuilder monad that lets you -- lookup fields by label and to add the current nonce value from your forms.
-- add values, add files, lookup fields by label and find the current -- Once built, the request can be executed with the 'request' method.
-- nonce value and add it to your request too.
-- --
-- Convenience functions like 'get' and 'post' build and execute common requests.
, get , get
, post , post
, postBody , postBody
@ -57,16 +57,22 @@ module Yesod.Test
, RequestBuilder , RequestBuilder
, setUrl , setUrl
-- | Yesod can auto generate field ids, so you are never sure what -- *** Adding fields by label
-- the argument name should be for each one of your args when constructing -- | Yesod can auto generate field names, so you are never sure what
-- the argument name should be for each one of your inputs when constructing
-- your requests. What you do know is the /label/ of the field. -- your requests. What you do know is the /label/ of the field.
-- These functions let you add parameters to your request based -- These functions let you add parameters to your request based
-- on currently displayed label names. -- on currently displayed label names.
, byLabel , byLabel
, fileByLabel , fileByLabel
-- | Does the current form have a _nonce? Use any of these to add it to your -- *** Nonces
-- request parameters. -- | In order to prevent CSRF exploits, yesod-form adds a hidden input
-- to your forms with the name "_token". This token is a randomly generated,
-- per-session value called a /nonce/.
--
-- In order to prevent your forms from being rejected in tests, use one of
-- these functions to add the nonce to your request.
, addNonce , addNonce
, addNonce_ , addNonce_
@ -188,7 +194,7 @@ data RequestPart
= ReqKvPart T.Text T.Text = ReqKvPart T.Text T.Text
| ReqFilePart T.Text FilePath BSL8.ByteString T.Text | ReqFilePart T.Text FilePath BSL8.ByteString T.Text
-- | The RequestBuilder state monad constructs an url encoded string of arguments -- | The 'RequestBuilder' state monad constructs a URL encoded string of arguments
-- to send with your requests. Some of the functions that run on it use the current -- to send with your requests. Some of the functions that run on it use the current
-- response to analyze the forms that the server is expecting to receive. -- response to analyze the forms that the server is expecting to receive.
type RequestBuilder site = ST.StateT (RequestBuilderData site) IO type RequestBuilder site = ST.StateT (RequestBuilderData site) IO
@ -274,12 +280,12 @@ withResponse' getter f = maybe err f . getter =<< ST.get
withResponse :: (SResponse -> YesodExample site a) -> YesodExample site a withResponse :: (SResponse -> YesodExample site a) -> YesodExample site a
withResponse = withResponse' yedResponse withResponse = withResponse' yedResponse
-- | Use HXT to parse a value from an html tag. -- | Use HXT to parse a value from an HTML tag.
-- Check for usage examples in this module's source. -- Check for usage examples in this module's source.
parseHTML :: HtmlLBS -> Cursor parseHTML :: HtmlLBS -> Cursor
parseHTML html = fromDocument $ HD.parseLBS html parseHTML html = fromDocument $ HD.parseLBS html
-- | Query the last response using css selectors, returns a list of matched fragments -- | Query the last response using CSS selectors, returns a list of matched fragments
htmlQuery' :: MonadIO m htmlQuery' :: MonadIO m
=> (state -> Maybe SResponse) => (state -> Maybe SResponse)
-> Query -> Query
@ -289,7 +295,7 @@ htmlQuery' getter query = withResponse' getter $ \ res ->
Left err -> failure $ query <> " did not parse: " <> T.pack (show err) Left err -> failure $ query <> " did not parse: " <> T.pack (show err)
Right matches -> return $ map (encodeUtf8 . TL.pack) matches Right matches -> return $ map (encodeUtf8 . TL.pack) matches
-- | Query the last response using css selectors, returns a list of matched fragments -- | Query the last response using CSS selectors, returns a list of matched fragments
htmlQuery :: Query -> YesodExample site [HtmlLBS] htmlQuery :: Query -> YesodExample site [HtmlLBS]
htmlQuery = htmlQuery' yedResponse htmlQuery = htmlQuery' yedResponse
@ -354,7 +360,7 @@ bodyContains text = withResponse $ \ res ->
contains :: BSL8.ByteString -> String -> Bool contains :: BSL8.ByteString -> String -> Bool
contains a b = DL.isInfixOf b (TL.unpack $ decodeUtf8 a) contains a b = DL.isInfixOf b (TL.unpack $ decodeUtf8 a)
-- | Queries the html using a css selector, and all matched elements must contain -- | Queries the HTML using a CSS selector, and all matched elements must contain
-- the given string. -- the given string.
htmlAllContain :: Query -> String -> YesodExample site () htmlAllContain :: Query -> String -> YesodExample site ()
htmlAllContain query search = do htmlAllContain query search = do
@ -364,7 +370,7 @@ htmlAllContain query search = do
_ -> liftIO $ HUnit.assertBool ("Not all "++T.unpack query++" contain "++search) $ _ -> liftIO $ HUnit.assertBool ("Not all "++T.unpack query++" contain "++search) $
DL.all (DL.isInfixOf search) (map (TL.unpack . decodeUtf8) matches) DL.all (DL.isInfixOf search) (map (TL.unpack . decodeUtf8) matches)
-- | Queries the html using a css selector, and passes if any matched -- | Queries the HTML using a CSS selector, and passes if any matched
-- element contains the given string. -- element contains the given string.
-- --
-- Since 0.3.5 -- Since 0.3.5
@ -376,7 +382,7 @@ htmlAnyContain query search = do
_ -> liftIO $ HUnit.assertBool ("None of "++T.unpack query++" contain "++search) $ _ -> liftIO $ HUnit.assertBool ("None of "++T.unpack query++" contain "++search) $
DL.any (DL.isInfixOf search) (map (TL.unpack . decodeUtf8) matches) DL.any (DL.isInfixOf search) (map (TL.unpack . decodeUtf8) matches)
-- | Queries the html using a css selector, and fails if any matched -- | Queries the HTML using a CSS selector, and fails if any matched
-- element contains the given string (in other words, it is the logical -- element contains the given string (in other words, it is the logical
-- inverse of htmlAnyContains). -- inverse of htmlAnyContains).
-- --
@ -389,7 +395,7 @@ htmlNoneContain query search = do
found -> failure $ "Found " <> T.pack (show $ length found) <> found -> failure $ "Found " <> T.pack (show $ length found) <>
" instances of " <> T.pack search <> " in " <> query <> " elements" " instances of " <> T.pack search <> " in " <> query <> " elements"
-- | Performs a css query on the last response and asserts the matched elements -- | Performs a CSS query on the last response and asserts the matched elements
-- are as many as expected. -- are as many as expected.
htmlCount :: Query -> Int -> YesodExample site () htmlCount :: Query -> Int -> YesodExample site ()
htmlCount query count = do htmlCount query count = do
@ -408,7 +414,7 @@ printMatches query = do
matches <- htmlQuery query matches <- htmlQuery query
liftIO $ hPutStrLn stderr $ show matches liftIO $ hPutStrLn stderr $ show matches
-- | Add a parameter with the given name and value. -- | Add a parameter with the given name and value to the request body.
addPostParam :: T.Text -> T.Text -> RequestBuilder site () addPostParam :: T.Text -> T.Text -> RequestBuilder site ()
addPostParam name value = addPostParam name value =
ST.modify $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd)) } ST.modify $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd)) }
@ -416,16 +422,25 @@ addPostParam name value =
addPostData (MultipleItemsPostData posts) = addPostData (MultipleItemsPostData posts) =
MultipleItemsPostData $ ReqKvPart name value : posts MultipleItemsPostData $ ReqKvPart name value : posts
-- | Add a parameter with the given name and value to the query string.
addGetParam :: T.Text -> T.Text -> RequestBuilder site () addGetParam :: T.Text -> T.Text -> RequestBuilder site ()
addGetParam name value = ST.modify $ \rbd -> rbd addGetParam name value = ST.modify $ \rbd -> rbd
{ rbdGets = (TE.encodeUtf8 name, Just $ TE.encodeUtf8 value) { rbdGets = (TE.encodeUtf8 name, Just $ TE.encodeUtf8 value)
: rbdGets rbd : rbdGets rbd
} }
-- | Add a file to be posted with the current request -- | Add a file to be posted with the current request.
-- --
-- Adding a file will automatically change your request content-type to be multipart/form-data -- Adding a file will automatically change your request content-type to be multipart/form-data.
addFile :: T.Text -> FilePath -> T.Text -> RequestBuilder site () --
-- ==== __Examples__
--
-- > request $ do
-- > addFile "profile_picture" "static/img/picture.png" "img/png"
addFile :: T.Text -- ^ The parameter name for the file.
-> FilePath -- ^ The path to the file.
-> T.Text -- ^ The MIME type of the file, e.g. "image/png".
-> RequestBuilder site ()
addFile name path mimetype = do addFile name path mimetype = do
contents <- liftIO $ BSL8.readFile path contents <- liftIO $ BSL8.readFile path
ST.modify $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd) contents) } ST.modify $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd) contents) }
@ -476,18 +491,75 @@ nameFromLabel label = do
(<>) :: T.Text -> T.Text -> T.Text (<>) :: T.Text -> T.Text -> T.Text
(<>) = T.append (<>) = T.append
byLabel :: T.Text -> T.Text -> RequestBuilder site () -- How does this work for the alternate <label><input></label> syntax?
-- | Finds the @\<label>@ with the given value, finds its corresponding @\<input>@, then adds a parameter
-- for that input to the request body.
--
-- ==== __Examples__
--
-- Given this HTML, we want to submit @f1=Michael@ to the server:
--
-- > <form method="POST">
-- > <label for="user">Username</label>
-- > <input id="user" name="f1" />
-- > </form>
--
-- You can set this parameter like so:
--
-- > request $ do
-- > byLabel "Username" "Michael"
--
-- This function also supports the implicit label syntax, in which
-- the @\<input>@ is nested inside the @\<label>@ rather than specified with @for@:
--
-- > <form method="POST">
-- > <label>Username <input name="f1"> </label>
-- > </form>
byLabel :: T.Text -- ^ The text contained in the @\<label>@.
-> T.Text -- ^ The value to set the parameter to.
-> RequestBuilder site ()
byLabel label value = do byLabel label value = do
name <- nameFromLabel label name <- nameFromLabel label
addPostParam name value addPostParam name value
fileByLabel :: T.Text -> FilePath -> T.Text -> RequestBuilder site () -- | Finds the @\<label>@ with the given value, finds its corresponding @\<input>@, then adds a file for that input to the request body.
--
-- ==== __Examples__
--
-- Given this HTML, we want to submit a file with the parameter name @f1@ to the server:
--
-- > <form method="POST">
-- > <label for="imageInput">Please submit an image</label>
-- > <input id="imageInput" type="file" name="f1" accept="image/*">
-- > </form>
--
-- You can set this parameter like so:
--
-- > request $ do
-- > fileByLabel "Please submit an image" "static/img/picture.png" "img/png"
--
-- This function also supports the implicit label syntax, in which
-- the @\<input>@ is nested inside the @\<label>@ rather than specified with @for@:
--
-- > <form method="POST">
-- > <label>Please submit an image <input type="file" name="f1"> </label>
-- > </form>
fileByLabel :: T.Text -- ^ The text contained in the @\<label>@.
-> FilePath -- ^ The path to the file.
-> T.Text -- ^ The MIME type of the file, e.g. "image/png".
-> RequestBuilder site ()
fileByLabel label path mime = do fileByLabel label path mime = do
name <- nameFromLabel label name <- nameFromLabel label
addFile name path mime addFile name path mime
-- | Lookup a _nonce form field and add it's value to the params. -- | Lookup a _token form field and add its value to the params.
-- Receives a CSS selector that should resolve to the form element containing the nonce. -- Receives a CSS selector that should resolve to the form element containing the nonce.
--
-- ==== __Examples__
--
-- > request $ do
-- > addNonce_ "#formID"
addNonce_ :: Query -> RequestBuilder site () addNonce_ :: Query -> RequestBuilder site ()
addNonce_ scope = do addNonce_ scope = do
matches <- htmlQuery' rbdResponse $ scope <> "input[name=_token][type=hidden][value]" matches <- htmlQuery' rbdResponse $ scope <> "input[name=_token][type=hidden][value]"
@ -500,7 +572,11 @@ addNonce_ scope = do
addNonce :: RequestBuilder site () addNonce :: RequestBuilder site ()
addNonce = addNonce_ "" addNonce = addNonce_ ""
-- | Perform a POST request to url -- | Perform a POST request to @url@.
--
-- ==== __Examples__
--
-- > post HomeR
post :: (Yesod site, RedirectUrl site url) post :: (Yesod site, RedirectUrl site url)
=> url => url
-> YesodExample site () -> YesodExample site ()
@ -508,7 +584,14 @@ post url = request $ do
setMethod "POST" setMethod "POST"
setUrl url setUrl url
-- | Perform a POST request to url with sending a body into it. -- | Perform a POST request to @url@ with the given body.
--
-- ==== __Examples__
--
-- > postBody HomeR "foobar"
--
-- > import Data.Aeson
-- > postBody HomeR (encode $ object ["age" .= (1 :: Integer)])
postBody :: (Yesod site, RedirectUrl site url) postBody :: (Yesod site, RedirectUrl site url)
=> url => url
-> BSL8.ByteString -> BSL8.ByteString
@ -518,7 +601,13 @@ postBody url body = request $ do
setUrl url setUrl url
setRequestBody body setRequestBody body
-- | Perform a GET request to url, using params -- | Perform a GET request to @url@.
--
-- ==== __Examples__
--
-- > get HomeR
--
-- > get ("http://google.com" :: Text)
get :: (Yesod site, RedirectUrl site url) get :: (Yesod site, RedirectUrl site url)
=> url => url
-> YesodExample site () -> YesodExample site ()
@ -526,9 +615,28 @@ get url = request $ do
setMethod "GET" setMethod "GET"
setUrl url setUrl url
-- | Sets the HTTP method used by the request.
--
-- ==== __Examples__
--
-- > request $ do
-- > setMethod "POST"
--
-- > import Network.HTTP.Types.Method
-- > request $ do
-- > setMethod methodPut
setMethod :: H.Method -> RequestBuilder site () setMethod :: H.Method -> RequestBuilder site ()
setMethod m = ST.modify $ \rbd -> rbd { rbdMethod = m } setMethod m = ST.modify $ \rbd -> rbd { rbdMethod = m }
-- | Sets the URL used by the request.
--
-- ==== __Examples__
--
-- > request $ do
-- > setUrl HomeR
--
-- > request $ do
-- > setUrl ("http://google.com/" :: Text)
setUrl :: (Yesod site, RedirectUrl site url) setUrl :: (Yesod site, RedirectUrl site url)
=> url => url
-> RequestBuilder site () -> RequestBuilder site ()
@ -551,18 +659,45 @@ setUrl url' = do
} }
-- | Simple way to set HTTP request body -- | Simple way to set HTTP request body
--
-- ==== __ Examples__
--
-- > request $ do
-- > setRequestBody "foobar"
--
-- > import Data.Aeson
-- > request $ do
-- > setRequestBody $ encode $ object ["age" .= (1 :: Integer)]
setRequestBody :: (Yesod site) setRequestBody :: (Yesod site)
=> BSL8.ByteString => BSL8.ByteString
-> RequestBuilder site () -> RequestBuilder site ()
setRequestBody body = ST.modify $ \rbd -> rbd { rbdPostData = BinaryPostData body } setRequestBody body = ST.modify $ \rbd -> rbd { rbdPostData = BinaryPostData body }
-- | Adds the given header to the request; see "Network.HTTP.Types.Header" for creating 'Header's.
--
-- ==== __Examples__
--
-- > import Network.HTTP.Types.Header
-- > request $ do
-- > addRequestHeader (hUserAgent, "Chrome/41.0.2228.0")
addRequestHeader :: H.Header -> RequestBuilder site () addRequestHeader :: H.Header -> RequestBuilder site ()
addRequestHeader header = ST.modify $ \rbd -> rbd addRequestHeader header = ST.modify $ \rbd -> rbd
{ rbdHeaders = header : rbdHeaders rbd { rbdHeaders = header : rbdHeaders rbd
} }
-- | General interface to performing requests, allowing you to add extra -- | The general interface for performing requests. 'request' takes a 'RequestBuilder',
-- headers as well as letting you specify the request method. -- constructs a request, and executes it.
--
-- The 'RequestBuilder' allows you to build up attributes of the request, like the
-- headers, parameters, and URL of the request.
--
-- ==== __Examples__
--
-- > request $ do
-- > addNonce
-- > byLabel "First Name" "Felipe"
-- > setMethod "PUT"
-- > setUrl NameR
request :: Yesod site request :: Yesod site
=> RequestBuilder site () => RequestBuilder site ()
-> YesodExample site () -> YesodExample site ()