Add more documentation to yesod-test
This adds high-level documentation to yesod-test, plus some function documentation
This commit is contained in:
parent
074865bca9
commit
c3fa2adddd
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
{-|
|
{-|
|
||||||
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.
|
||||||
|
|
||||||
By pragmatic I may also mean 'dirty'. Its 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/.
|
||||||
@ -24,8 +24,86 @@ This is very useful for testing web applications built in yesod, for example,
|
|||||||
where 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 CSRF token input.
|
generated CSRF token input.
|
||||||
|
|
||||||
Your database is also directly available so you can use 'runDB' to set up
|
=== Example project
|
||||||
|
|
||||||
|
The best way to see an example project using yesod-test is to create a scaffolded Yesod project:
|
||||||
|
|
||||||
|
@stack new projectname yesod-sqlite@
|
||||||
|
|
||||||
|
(See https://github.com/commercialhaskell/stack-templates/wiki#yesod for the full list of Yesod templates)
|
||||||
|
|
||||||
|
The scaffolded project makes your database directly available in tests, 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.
|
||||||
|
It also handles wiping your database between each test.
|
||||||
|
|
||||||
|
=== Example code
|
||||||
|
|
||||||
|
The code below should give you a high-level idea of yesod-test's capabilities.
|
||||||
|
Note that it uses helper functions like @withApp@ and @runDB@ from the scaffolded project; these aren't provided by yesod-test.
|
||||||
|
|
||||||
|
@
|
||||||
|
spec :: Spec
|
||||||
|
spec = withApp $ do
|
||||||
|
describe \"Homepage\" $ do
|
||||||
|
it "loads the homepage with a valid status code" $ do
|
||||||
|
'get' HomeR
|
||||||
|
'statusIs' 200
|
||||||
|
describe \"Login Form\" $ do
|
||||||
|
it "Only allows dashboard access after logging in" $ do
|
||||||
|
'get' DashboardR
|
||||||
|
'statusIs' 401
|
||||||
|
|
||||||
|
'get' HomeR
|
||||||
|
-- Assert a \<p\> tag exists on the page
|
||||||
|
'htmlAnyContain' \"p\" \"Login\"
|
||||||
|
|
||||||
|
-- yesod-test provides a 'RequestBuilder' monad for building up HTTP requests
|
||||||
|
'request' $ do
|
||||||
|
-- Lookup the HTML \<label\> with the text Username, and set a POST parameter for that field with the value Felipe
|
||||||
|
'byLabelExact' \"Username\" \"Felipe\"
|
||||||
|
'byLabelExact' \"Password\" "pass\"
|
||||||
|
'setMethod' \"POST\"
|
||||||
|
'setUrl' SignupR
|
||||||
|
'statusIs' 200
|
||||||
|
|
||||||
|
-- The previous request will have stored a session cookie, so we can access the dashboard now
|
||||||
|
'get' DashboardR
|
||||||
|
'statusIs' 200
|
||||||
|
|
||||||
|
-- Assert a user with the name Felipe was added to the database
|
||||||
|
[Entity userId user] <- runDB $ selectList [] []
|
||||||
|
'assertEq' "A single user named Felipe is created" (userUsername user) \"Felipe\"
|
||||||
|
describe \"JSON\" $ do
|
||||||
|
it "Can make requests using JSON, and parse JSON responses" $ do
|
||||||
|
-- Precondition: Create a user with the name \"George\"
|
||||||
|
runDB $ insert_ $ User \"George\" "pass"
|
||||||
|
|
||||||
|
'request' $ do
|
||||||
|
-- Use the Aeson library to send JSON to the server
|
||||||
|
'setRequestBody' ('Data.Aeson.encode' $ LoginRequest \"George\" "pass")
|
||||||
|
'addRequestHeader' (\"Accept\", "application/json")
|
||||||
|
'addRequestHeader' ("Content-Type", "application/json")
|
||||||
|
'setUrl' LoginR
|
||||||
|
'statusIs' 200
|
||||||
|
|
||||||
|
-- Parse the request's response as JSON
|
||||||
|
(signupResponse :: SignupResponse) <- 'requireJSONResponse'
|
||||||
|
@
|
||||||
|
|
||||||
|
=== HUnit / HSpec integration
|
||||||
|
|
||||||
|
yesod-test is built on top of hspec, which is itself built on top of HUnit.
|
||||||
|
You can use existing assertion functions from those libraries, but you'll need to use `liftIO` with them:
|
||||||
|
|
||||||
|
@
|
||||||
|
liftIO $ actualTimesCalled `'Test.Hspec.Expectations.shouldBe'` expectedTimesCalled -- hspec assertion
|
||||||
|
@
|
||||||
|
|
||||||
|
@
|
||||||
|
liftIO $ 'Test.HUnit.Base.assertBool' "a is greater than b" (a > b) -- HUnit assertion
|
||||||
|
@
|
||||||
|
|
||||||
|
yesod-test provides a handful of assertion functions that are already lifted, such as 'assertEq', as well.
|
||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
@ -459,7 +537,7 @@ htmlQuery = htmlQuery' yedResponse []
|
|||||||
|
|
||||||
-- | Asserts that the two given values are equal.
|
-- | Asserts that the two given values are equal.
|
||||||
--
|
--
|
||||||
-- In case they are not equal, error message includes the two values.
|
-- In case they are not equal, the error message includes the two values.
|
||||||
--
|
--
|
||||||
-- @since 1.5.2
|
-- @since 1.5.2
|
||||||
assertEq :: (HasCallStack, Eq a, Show a) => String -> a -> a -> YesodExample site ()
|
assertEq :: (HasCallStack, Eq a, Show a) => String -> a -> a -> YesodExample site ()
|
||||||
@ -471,7 +549,7 @@ assertEq m a b =
|
|||||||
|
|
||||||
-- | Asserts that the two given values are not equal.
|
-- | Asserts that the two given values are not equal.
|
||||||
--
|
--
|
||||||
-- In case they are equal, error mesasge includes the values.
|
-- In case they are equal, the error message includes the values.
|
||||||
--
|
--
|
||||||
-- @since 1.5.6
|
-- @since 1.5.6
|
||||||
assertNotEq :: (HasCallStack, Eq a, Show a) => String -> a -> a -> YesodExample site ()
|
assertNotEq :: (HasCallStack, Eq a, Show a) => String -> a -> a -> YesodExample site ()
|
||||||
@ -491,6 +569,11 @@ assertEqualNoShow :: (HasCallStack, Eq a) => String -> a -> a -> YesodExample si
|
|||||||
assertEqualNoShow msg a b = liftIO $ HUnit.assertBool msg (a == b)
|
assertEqualNoShow msg a b = liftIO $ HUnit.assertBool msg (a == b)
|
||||||
|
|
||||||
-- | Assert the last response status is as expected.
|
-- | Assert the last response status is as expected.
|
||||||
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > get HomeR
|
||||||
|
-- > statusIs 200
|
||||||
statusIs :: HasCallStack => Int -> YesodExample site ()
|
statusIs :: HasCallStack => Int -> YesodExample site ()
|
||||||
statusIs number = withResponse $ \ SResponse { simpleStatus = s } ->
|
statusIs number = withResponse $ \ SResponse { simpleStatus = s } ->
|
||||||
liftIO $ flip HUnit.assertBool (H.statusCode s == number) $ concat
|
liftIO $ flip HUnit.assertBool (H.statusCode s == number) $ concat
|
||||||
@ -499,6 +582,17 @@ statusIs number = withResponse $ \ SResponse { simpleStatus = s } ->
|
|||||||
]
|
]
|
||||||
|
|
||||||
-- | Assert the given header key/value pair was returned.
|
-- | Assert the given header key/value pair was returned.
|
||||||
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > {-# LANGUAGE OverloadedStrings #-}
|
||||||
|
-- > get HomeR
|
||||||
|
-- > assertHeader "key" "value"
|
||||||
|
--
|
||||||
|
-- > import qualified Data.CaseInsensitive as CI
|
||||||
|
-- > import qualified Data.ByteString.Char8 as BS8
|
||||||
|
-- > getHomeR
|
||||||
|
-- > assertHeader (CI.mk (BS8.pack "key")) (BS8.pack "value")
|
||||||
assertHeader :: HasCallStack => CI BS8.ByteString -> BS8.ByteString -> YesodExample site ()
|
assertHeader :: HasCallStack => CI BS8.ByteString -> BS8.ByteString -> YesodExample site ()
|
||||||
assertHeader header value = withResponse $ \ SResponse { simpleHeaders = h } ->
|
assertHeader header value = withResponse $ \ SResponse { simpleHeaders = h } ->
|
||||||
case lookup header h of
|
case lookup header h of
|
||||||
@ -519,6 +613,17 @@ assertHeader header value = withResponse $ \ SResponse { simpleHeaders = h } ->
|
|||||||
]
|
]
|
||||||
|
|
||||||
-- | Assert the given header was not included in the response.
|
-- | Assert the given header was not included in the response.
|
||||||
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > {-# LANGUAGE OverloadedStrings #-}
|
||||||
|
-- > get HomeR
|
||||||
|
-- > assertNoHeader "key"
|
||||||
|
--
|
||||||
|
-- > import qualified Data.CaseInsensitive as CI
|
||||||
|
-- > import qualified Data.ByteString.Char8 as BS8
|
||||||
|
-- > getHomeR
|
||||||
|
-- > assertNoHeader (CI.mk (BS8.pack "key"))
|
||||||
assertNoHeader :: HasCallStack => CI BS8.ByteString -> YesodExample site ()
|
assertNoHeader :: HasCallStack => CI BS8.ByteString -> YesodExample site ()
|
||||||
assertNoHeader header = withResponse $ \ SResponse { simpleHeaders = h } ->
|
assertNoHeader header = withResponse $ \ SResponse { simpleHeaders = h } ->
|
||||||
case lookup header h of
|
case lookup header h of
|
||||||
@ -532,6 +637,11 @@ assertNoHeader header = withResponse $ \ SResponse { simpleHeaders = h } ->
|
|||||||
|
|
||||||
-- | Assert the last response is exactly equal to the given text. This is
|
-- | Assert the last response is exactly equal to the given text. This is
|
||||||
-- useful for testing API responses.
|
-- useful for testing API responses.
|
||||||
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > get HomeR
|
||||||
|
-- > bodyEquals "<html><body><h1>Hello, World</h1></body></html>"
|
||||||
bodyEquals :: HasCallStack => String -> YesodExample site ()
|
bodyEquals :: HasCallStack => String -> YesodExample site ()
|
||||||
bodyEquals text = withResponse $ \ res -> do
|
bodyEquals text = withResponse $ \ res -> do
|
||||||
let actual = simpleBody res
|
let actual = simpleBody res
|
||||||
@ -544,6 +654,11 @@ bodyEquals text = withResponse $ \ res -> do
|
|||||||
|
|
||||||
-- | Assert the last response has the given text. The check is performed using the response
|
-- | Assert the last response has the given text. The check is performed using the response
|
||||||
-- body in full text form.
|
-- body in full text form.
|
||||||
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > get HomeR
|
||||||
|
-- > bodyContains "<h1>Foo</h1>"
|
||||||
bodyContains :: HasCallStack => String -> YesodExample site ()
|
bodyContains :: HasCallStack => String -> YesodExample site ()
|
||||||
bodyContains text = withResponse $ \ res ->
|
bodyContains text = withResponse $ \ res ->
|
||||||
liftIO $ HUnit.assertBool ("Expected body to contain " ++ text) $
|
liftIO $ HUnit.assertBool ("Expected body to contain " ++ text) $
|
||||||
@ -551,6 +666,12 @@ bodyContains text = withResponse $ \ res ->
|
|||||||
|
|
||||||
-- | Assert the last response doesn't have the given text. The check is performed using the response
|
-- | Assert the last response doesn't have the given text. The check is performed using the response
|
||||||
-- body in full text form.
|
-- body in full text form.
|
||||||
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > get HomeR
|
||||||
|
-- > bodyNotContains "<h1>Foo</h1>
|
||||||
|
--
|
||||||
-- @since 1.5.3
|
-- @since 1.5.3
|
||||||
bodyNotContains :: HasCallStack => String -> YesodExample site ()
|
bodyNotContains :: HasCallStack => String -> YesodExample site ()
|
||||||
bodyNotContains text = withResponse $ \ res ->
|
bodyNotContains text = withResponse $ \ res ->
|
||||||
@ -562,6 +683,16 @@ 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.
|
||||||
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > {-# LANGUAGE OverloadedStrings #-}
|
||||||
|
-- > get HomeR
|
||||||
|
-- > htmlAllContain "p" "Hello" -- Every <p> tag contains the string "Hello"
|
||||||
|
--
|
||||||
|
-- > import qualified Data.Text as T
|
||||||
|
-- > get HomeR
|
||||||
|
-- > htmlAllContain (T.pack "h1#mainTitle") "Sign Up Now!" -- All <h1> tags with the ID mainTitle contain the string "Sign Up Now!"
|
||||||
htmlAllContain :: HasCallStack => Query -> String -> YesodExample site ()
|
htmlAllContain :: HasCallStack => Query -> String -> YesodExample site ()
|
||||||
htmlAllContain query search = do
|
htmlAllContain query search = do
|
||||||
matches <- htmlQuery query
|
matches <- htmlQuery query
|
||||||
@ -573,6 +704,12 @@ htmlAllContain query search = do
|
|||||||
-- | 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.
|
||||||
--
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > {-# LANGUAGE OverloadedStrings #-}
|
||||||
|
-- > get HomeR
|
||||||
|
-- > htmlAnyContain "p" "Hello" -- At least one <p> tag contains the string "Hello"
|
||||||
|
--
|
||||||
-- Since 0.3.5
|
-- Since 0.3.5
|
||||||
htmlAnyContain :: HasCallStack => Query -> String -> YesodExample site ()
|
htmlAnyContain :: HasCallStack => Query -> String -> YesodExample site ()
|
||||||
htmlAnyContain query search = do
|
htmlAnyContain query search = do
|
||||||
@ -584,7 +721,13 @@ htmlAnyContain query search = do
|
|||||||
|
|
||||||
-- | 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 htmlAnyContain).
|
||||||
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > {-# LANGUAGE OverloadedStrings #-}
|
||||||
|
-- > get HomeR
|
||||||
|
-- > htmlNoneContain ".my-class" "Hello" -- No tags with the class "my-class" contain the string "Hello"
|
||||||
--
|
--
|
||||||
-- Since 1.2.2
|
-- Since 1.2.2
|
||||||
htmlNoneContain :: HasCallStack => Query -> String -> YesodExample site ()
|
htmlNoneContain :: HasCallStack => Query -> String -> YesodExample site ()
|
||||||
@ -597,6 +740,12 @@ htmlNoneContain query search = do
|
|||||||
|
|
||||||
-- | 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.
|
||||||
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > {-# LANGUAGE OverloadedStrings #-}
|
||||||
|
-- > get HomeR
|
||||||
|
-- > htmlNoneContain "p" 3 -- There are exactly 3 <p> tags in the response
|
||||||
htmlCount :: HasCallStack => Query -> Int -> YesodExample site ()
|
htmlCount :: HasCallStack => Query -> Int -> YesodExample site ()
|
||||||
htmlCount query count = do
|
htmlCount query count = do
|
||||||
matches <- fmap DL.length $ htmlQuery query
|
matches <- fmap DL.length $ htmlQuery query
|
||||||
@ -634,18 +783,42 @@ requireJSONResponse = do
|
|||||||
failure $ T.concat ["Failed to parse JSON response; error: ", T.pack err, "JSON: ", bodyPreview]
|
failure $ T.concat ["Failed to parse JSON response; error: ", T.pack err, "JSON: ", bodyPreview]
|
||||||
Right v -> return v
|
Right v -> return v
|
||||||
|
|
||||||
-- | Outputs the last response body to stderr (So it doesn't get captured by HSpec)
|
-- | Outputs the last response body to stderr (So it doesn't get captured by HSpec). Useful for debugging.
|
||||||
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > get HomeR
|
||||||
|
-- > printBody
|
||||||
printBody :: YesodExample site ()
|
printBody :: YesodExample site ()
|
||||||
printBody = withResponse $ \ SResponse { simpleBody = b } ->
|
printBody = withResponse $ \ SResponse { simpleBody = b } ->
|
||||||
liftIO $ BSL8.hPutStrLn stderr b
|
liftIO $ BSL8.hPutStrLn stderr b
|
||||||
|
|
||||||
-- | Performs a CSS query and print the matches to stderr.
|
-- | Performs a CSS query and print the matches to stderr.
|
||||||
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > {-# LANGUAGE OverloadedStrings #-}
|
||||||
|
-- > get HomeR
|
||||||
|
-- > printMatches "h1" -- Prints all h1 tags
|
||||||
printMatches :: Query -> YesodExample site ()
|
printMatches :: Query -> YesodExample site ()
|
||||||
printMatches query = do
|
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 to the request body.
|
-- | Add a parameter with the given name and value to the request body.
|
||||||
|
-- This function can be called multiple times to add multiple parameters, and be mixed with calls to 'addFile'.
|
||||||
|
--
|
||||||
|
-- "Post parameter" is an informal description of what is submitted by making an HTTP POST with an HTML @\<form\>@.
|
||||||
|
-- Like HTML @\<form\>@s, yesod-test will default to a @Content-Type@ of @application/x-www-form-urlencoded@ if no files are added,
|
||||||
|
-- and switch to @multipart/form-data@ if files are added.
|
||||||
|
--
|
||||||
|
-- Calling this function after using 'setRequestBody' will raise an error.
|
||||||
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > {-# LANGUAGE OverloadedStrings #-}
|
||||||
|
-- > post $ do
|
||||||
|
-- > addPostParam "key" "value"
|
||||||
addPostParam :: T.Text -> T.Text -> RequestBuilder site ()
|
addPostParam :: T.Text -> T.Text -> RequestBuilder site ()
|
||||||
addPostParam name value =
|
addPostParam name value =
|
||||||
modifySIO $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd)) }
|
modifySIO $ \rbd -> rbd { rbdPostData = (addPostData (rbdPostData rbd)) }
|
||||||
@ -654,6 +827,12 @@ addPostParam name value =
|
|||||||
MultipleItemsPostData $ ReqKvPart name value : posts
|
MultipleItemsPostData $ ReqKvPart name value : posts
|
||||||
|
|
||||||
-- | Add a parameter with the given name and value to the query string.
|
-- | Add a parameter with the given name and value to the query string.
|
||||||
|
--
|
||||||
|
-- ==== __Examples__
|
||||||
|
--
|
||||||
|
-- > {-# LANGUAGE OverloadedStrings #-}
|
||||||
|
-- > request $ do
|
||||||
|
-- > addGetParam "key" "value" -- Adds ?key=value to the URL
|
||||||
addGetParam :: T.Text -> T.Text -> RequestBuilder site ()
|
addGetParam :: T.Text -> T.Text -> RequestBuilder site ()
|
||||||
addGetParam name value = modifySIO $ \rbd -> rbd
|
addGetParam name value = modifySIO $ \rbd -> rbd
|
||||||
{ rbdGets = (TE.encodeUtf8 name, Just $ TE.encodeUtf8 value)
|
{ rbdGets = (TE.encodeUtf8 name, Just $ TE.encodeUtf8 value)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user