Moduleyesod-test-1.6.19Haskell2010
Yesod.Test
Yesod.Test is a pragmatic framework for testing web applications built using wai.
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.
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, using CSS selectors to explore the document more easily.
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, where your forms may have field names generated by the framework or a randomly generated CSRF token input.
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.
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 (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 $ 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.
- 8 types
- 70 values
- Packageyesod-test-1.6.19
- Exports78
- LanguageHaskell2010
- LicenceMIT
- SourceTest.hs
Declaring and running your test suite
13 declarationsCorresponds to hspec's Spec.
Since 1.2.0
Same as yesodSpec, but instead of taking already built site it takes an action which produces site for each test.
Same as yesodSpecWithSiteGenerator, but also takes an argument to build the site and makes that argument available to the tests.
Same as yesodSpec, but instead of taking a site it takes an action which produces the Application for each test. This lets you use your middleware from makeApplication
A single test case, to be run with yit.
Since 1.2.0
The state used in a single test case defined using yit
Since 1.2.4
Constructors
YesodExampleDatayedApp :: !ApplicationyedSite :: !siteyedCookies :: !CookiesyedResponse :: !Maybe SResponse
Instances2Example, Arg
YesodDispatch site => Example (SIO (YesodExampleData site) a)Defined in yesod-test-1.6.19 · Yesod.Testtype Arg (SIO (YesodExampleData site) a) = TestApp siteDefined in yesod-test-1.6.19 · Yesod.Test
Internal data structure, corresponding to hspec's SpecTree.
Since 1.2.0
Constructors
YesodSpecGroup String [YesodSpecTree site]YesodSpecItem String (YesodExample site ())
Start describing a Tests suite keeping cookies and a reference to the tested Application
and ConnectionPool
Describe a single test that keeps cookies, and a reference to the last response.
Modify test site
1 declarationtestModifySite :: YesodDispatch site=> (site -> IO (site, Middleware))A function from the existing site, to a new site and middleware for a WAI app.
-> YesodExample site ()
Modifies the site (yedSite) of the test, and creates a new WAI app (yedApp) for it.
yesod-test allows sending requests to your application to test that it handles them correctly. In rare cases, you may wish to modify that application in the middle of a test. This may be useful if you wish to, for example, test your application under a certain configuration, then change that configuration to see if your app responds differently.
Examples
post SendEmailR
-- Assert email not created in database
testModifySite (\site -> pure (site { siteSettingsStoreEmail = True }, id))
post SendEmailR
-- Assert email created in databasetestModifySite (\site -> do
middleware <- makeLogware site
pure (site { appRedisConnection = Nothing }, middleware)
)Modify test state
4 declarationsSets a cookie
Examples
import qualified Web.Cookie as Cookie
:set -XOverloadedStrings
testSetCookie Cookie.defaultSetCookie { Cookie.setCookieName = "name" }Deletes the cookie of the given name
Examples
:set -XOverloadedStrings
testDeleteCookie "name"Modify the current cookies with the given mapping function
Clears the current cookies
Making requests
19 declarationsYou can construct requests with the RequestBuilder monad, which lets you set the URL and add parameters, headers, and files. Helper functions are provided to lookup fields by label and to add the current CSRF token from your forms. Once built, the request can be executed with the request method.
Convenience functions like get and post build and execute common requests.
Perform a GET request to url.
Examples
get HomeRget ("http://google.com" :: Text)Perform a POST request to url.
Examples
post HomeRPerform a POST request to url with the given body.
Examples
postBody HomeR "foobar"import Data.Aeson
postBody HomeR (encode $ object ["age" .= (1 :: Integer)])Perform a request using a given method to url.
Examples
performMethod "DELETE" HomeRFollow a redirect, if the last response was a redirect. (We consider a request a redirect if the status is 301, 302, 303, 307 or 308, and the Location header is set.)
Examples
get HomeR
followRedirectParse the Location header of the last response.
Examples
post ResourcesR
(Right (ResourceR resourceId)) <- getLocationThe general interface for performing requests. request takes a RequestBuilder, 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
addToken
byLabel "First Name" "Felipe"
setMethod "PUT"
setUrl NameRAdds the given header to the request; see Network.HTTP.Types.Header for creating Headers.
Examples
import Network.HTTP.Types.Header
request $ do
addRequestHeader (hUserAgent, "Chrome/41.0.2228.0")Adds a header for HTTP Basic Authentication to the request
Examples
request $ do
addBasicAuthHeader "Aladdin" "OpenSesame"Sets the HTTP method used by the request.
Examples
request $ do
setMethod "POST"import Network.HTTP.Types.Method
request $ do
setMethod methodPutAdd 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"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 URLAdd a bare parameter with the given name and no value to the query
string. The parameter is added without an = sign.
You can specify the entire query string literally by adding a single bare parameter and no other parameters.
Examples
{-# LANGUAGE OverloadedStrings #-}
request $ do
addBareGetParam "key" -- Adds ?key to the URLaddFile :: TextThe parameter name for the file.
-> FilePathThe path to the file.
-> TextThe MIME type of the file, e.g. "image/png".
-> RequestBuilder site ()
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.
Examples
request $ do
addFile "profile_picture" "static/img/picture.png" "img/png"Simple way to set HTTP request body
Examples
request $ do
setRequestBody "foobar"import Data.Aeson
request $ do
setRequestBody $ encode $ object ["age" .= (1 :: Integer)]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 response to analyze the forms that the server is expecting to receive.
State + IO
Instances9MonadState, Monad, Functor, Applicative, MonadIO, MonadThrow, …
MonadState s (SIO s)Defined in yesod-test-1.6.19 · Yesod.Test.Internal.SIOMonad (SIO s)Defined in yesod-test-1.6.19 · Yesod.Test.Internal.SIOFunctor (SIO s)Defined in yesod-test-1.6.19 · Yesod.Test.Internal.SIOApplicative (SIO s)Defined in yesod-test-1.6.19 · Yesod.Test.Internal.SIOMonadIO (SIO s)Defined in yesod-test-1.6.19 · Yesod.Test.Internal.SIOMonadThrow (SIO s)Defined in yesod-test-1.6.19 · Yesod.Test.Internal.SIOMonadUnliftIO (SIO s)Defined in yesod-test-1.6.19 · Yesod.Test.Internal.SIOYesodDispatch site => Example (SIO (YesodExampleData site) a)Defined in yesod-test-1.6.19 · Yesod.Testtype Arg (SIO (YesodExampleData site) a) = TestApp siteDefined in yesod-test-1.6.19 · Yesod.Test
Sets the URL used by the request.
Examples
request $ do
setUrl HomeRrequest $ do
setUrl ("http://google.com/" :: Text)Click on a link defined by a CSS query
Examples
get "/foobar"
clickOn "a#idofthelink"Adding fields by label
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. These functions let you add parameters to your request based on currently displayed label names.
byLabel :: TextThe text contained in the
<label>.-> TextThe value to set the parameter to.
-> RequestBuilder site ()
Deprecated. This function seems to have multiple bugs (ref: https://github.com/yesodweb/yesod/pull/1459). Use byLabelExact, byLabelContain, byLabelPrefix or byLabelSuffix instead
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>Warning: This function looks for any label that contains the provided text. If multiple labels contain that text, this function will throw an error, as in the example below:
<form method="POST">
<label for="nickname">Nickname</label>
<input id="nickname" name="f1" /> <label for="nickname2">Nickname2</label>
<input id="nickname2" name="f2" />
</form>request $ do
byLabel "Nickname" "Snoyberger"Then, it throws "More than one label contained" error.
Therefore, this function is deprecated. Please consider using byLabelExact, which performs the exact match over the provided text.
byLabelExact :: TextThe text in the
<label>.-> TextThe value to set the parameter to.
-> RequestBuilder site ()
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>byLabelContain :: TextThe text in the
<label>.-> TextThe value to set the parameter to.
-> RequestBuilder site ()
Contain version of byLabelExact
Note: Just like byLabel, this function throws an error if it finds multiple labels
byLabelPrefix :: TextThe text in the
<label>.-> TextThe value to set the parameter to.
-> RequestBuilder site ()
Prefix version of byLabelExact
Note: Just like byLabel, this function throws an error if it finds multiple labels
byLabelSuffix :: TextThe text in the
<label>.-> TextThe value to set the parameter to.
-> RequestBuilder site ()
Suffix version of byLabelExact
Note: Just like byLabel, this function throws an error if it finds multiple labels
bySelectorLabelContain :: TextThe CSS selector.
-> TextThe text in the
<label>.-> TextThe value to set the parameter to.
-> RequestBuilder site ()
Note: This function throws an error if it finds multiple labels or if the CSS selector fails to parse, doesn't match any fragment, or matches multiple fragments.
fileByLabel :: TextThe text contained in the
<label>.-> FilePathThe path to the file.
-> TextThe MIME type of the file, e.g. "image/png".
-> RequestBuilder site ()
Deprecated. This function seems to have multiple bugs (ref: https://github.com/yesodweb/yesod/pull/1459). Use fileByLabelExact, fileByLabelContain, fileByLabelPrefix or fileByLabelSuffix instead
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>Warning: This function has the same issue as byLabel. Please use fileByLabelExact instead.
fileByLabelExact :: TextThe text contained in the
<label>.-> FilePathThe path to the file.
-> TextThe MIME type of the file, e.g. "image/png".
-> 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>fileByLabelContain :: TextThe text contained in the
<label>.-> FilePathThe path to the file.
-> TextThe MIME type of the file, e.g. "image/png".
-> RequestBuilder site ()
Contain version of fileByLabelExact
Note: Just like fileByLabel, this function throws an error if it finds multiple labels
fileByLabelPrefix :: TextThe text contained in the
<label>.-> FilePathThe path to the file.
-> TextThe MIME type of the file, e.g. "image/png".
-> RequestBuilder site ()
Prefix version of fileByLabelExact
Note: Just like fileByLabel, this function throws an error if it finds multiple labels
fileByLabelSuffix :: TextThe text contained in the
<label>.-> FilePathThe path to the file.
-> TextThe MIME type of the file, e.g. "image/png".
-> RequestBuilder site ()
Suffix version of fileByLabelExact
Note: Just like fileByLabel, this function throws an error if it finds multiple labels
Finds the <label> with the given value, finds its corresponding <input>, then make this input checked.
It is assumed the <input> has type=radio.
Examples
Given this HTML, we want to submit f1=2 (i.e. radio button with Blue label) to the server:
<form method="POST">
<label for="hident2">Color</label>
<div id="hident2">
<div class="radio">
<input id="hident2-none" type="radio" name="f1" value="none" checked>
<label for="hident2-none"><None></label>
</div>
<div class="radio">
<input id="hident2-1" type="radio" name="f1" value="1">
<label for="hident2-1">Red</label>
</div>
<div class="radio">
<input id="hident2-2" type="radio" name="f1" value="2">
<label for="hident2-2">Blue</label>
</div>
<div class="radio">
<input id="hident2-3" type="radio" name="f1" value="3">
<label for="hident2-3">Gray</label>
</div>
<div class="radio">
<input id="hident2-4" type="radio" name="f1" value="4">
<label for="hident2-4">Black</label>
</div>
</div>
</form>You can set this parameter like so:
request $ do
chooseByLabel "Blue"Finds the <label> with the given value, finds its corresponding <input>, then make this input checked.
It is assumed the <input> has type=checkbox.
Examples
Given this HTML, we want to submit f1=2 and f1=4 (i.e. checked checkboxes are Blue and Black) to the server:
<form method="POST">
<label for="hident2">Colors</label>
<span id="hident2">
<input id="hident2-1" type="checkbox" name="f1" value="1">
<label for="hident2-1">Red</label>
<input id="hident2-2" type="checkbox" name="f1" value="2" checked>
<label for="hident2-2">Blue</label>
<input id="hident2-3" type="checkbox" name="f1" value="3">
<label for="hident2-3">Gray</label>
<input id="hident2-4" type="checkbox" name="f1" value="4" checked>
<label for="hident2-4">Black</label>
</span>
</form>You can set this parameter like so:
request $ do
checkByLabel "Blue"
checkByLabel "Black"Finds the <label> with the given value, finds its corresponding <select>,
then finds corresponding <option> and make this options selected.
Examples
Given this HTML, we want to submit f1=2 (i.e. selected option is Blue) to the server:
<form method="post" action="labels-select">
<label for="hident2">Selection List</label>
<select id="hident2" name="f1">
<option value="1">Red</option>
<option value="2">Blue</option>
<option value="3">Gray</option>
<option value="4">Black</option>
</select>
</form>You can set this parameter like so:
request $ do
setMethod "POST"
selectByLabel "Selection List" "Blue"CSRF Tokens
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.
In order to prevent your forms from being rejected in tests, use one of these functions to add the token to your request.
For responses that display a single form, just lookup the only CSRF token available.
Examples
request $ do
addTokenLookups the hidden input named "_token" and adds its value to the params. Receives a CSS selector that should resolve to the form element containing the token.
Examples
request $ do
addToken_ "#formID"Calls addTokenFromCookieNamedToHeaderNamed with the defaultCsrfCookieName and defaultCsrfHeaderName.
Use this function if you're using the CSRF middleware from Yesod.Core and haven't customized the cookie or header name.
Examples
request $ do
addTokenFromCookieSince 1.4.3.2
addTokenFromCookieNamedToHeaderNamed :: HasCallStack=> ByteStringThe name of the cookie
-> CI ByteStringThe name of the header
-> RequestBuilder site ()
Looks up the CSRF token stored in the cookie with the given name and adds it to the request headers. An error is thrown if the cookie can't be found.
Use this function if you're using the CSRF middleware from Yesod.Core and have customized the cookie or header name.
See Yesod.Core.Handler for details on this approach to CSRF protection.
Examples
import Data.CaseInsensitive (CI)
request $ do
addTokenFromCookieNamedToHeaderNamed "cookieName" (CI "headerName")Since 1.4.3.2
Assertions
15 declarationsDeprecated. Use assertEq instead
Asserts that the two given values are not equal.
In case they are equal, the error message includes the values.
Asserts that the two given values are equal.
assertEq :: (HasCallStack, Eq a, Show a)=> StringThe message prefix
-> aThe expected value
-> aThe actual value
-> YesodExample site ()
Asserts that the two given values are equal.
In case they are not equal, the error message includes the two values.
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")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"))Assert the last response status is as expected. If the status code doesn't match, a portion of the body is also printed to aid in debugging.
Examples
get HomeR
statusIs 200Assert the last response is exactly equal to the given text. This is useful for testing API responses.
Examples
get HomeR
bodyEquals "<html><body><h1>Hello, World</h1></body></html>"Assert the last response has the given text. The check is performed using the response body in full text form.
Examples
get HomeR
bodyContains "<h1>Foo</h1>"Assert the last response doesn't have the given text. The check is performed using the response body in full text form.
Examples
get HomeR
bodyNotContains "<h1>Foo</h1>Queries the HTML using a CSS selector, and all matched elements must contain 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!"Queries the HTML using a CSS selector, and passes if any matched 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
Queries the HTML using a CSS selector, and fails if any matched element contains the given string (in other words, it is the logical 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
Performs a CSS query on the last response and asserts the matched elements are as many as expected.
Examples
{-# LANGUAGE OverloadedStrings #-}
get HomeR
htmlCount "p" 3 -- There are exactly 3 <p> tags in the responseParses the response body from JSON into a Haskell value, throwing an error if parsing fails.
This function also checks that the Content-Type of the response is application/json.
Examples
get CommentR
(comment :: Comment) <- requireJSONResponsepost UserR
(json :: Value) <- requireJSONResponseGrab information
3 declarationsGet the foundation value used for the current test.
Since 1.2.0
Get the most recently provided response value, if available.
Since 1.2.0
Returns the Cookies from the most recent request. If a request hasn't been made, an error is raised.
Examples
request $ do
cookies <- getRequestCookies
liftIO $ putStrLn $ "Cookies are: " ++ show cookiesSince 1.4.3.2
Debug output
2 declarationsOutputs the last response body to stderr (So it doesn't get captured by HSpec). Useful for debugging.
Examples
get HomeR
printBodyPerforms a CSS query and print the matches to stderr.
Examples
{-# LANGUAGE OverloadedStrings #-}
get HomeR
printMatches "h1" -- Prints all h1 tagsUtils for building your own assertions
3 declarationsPlease consider generalizing and contributing the assertions you write.
Query the last response using CSS selectors, returns a list of matched fragments
Use HXT to parse a value from an HTML tag. Check for usage examples in this module's source.
Performs a given action using the last response. Use this to create response-level assertions