HTTP Tests
Introduction
Section titled “Introduction”TestClient makes requests against the application’s own ASGI app in-process:
no socket, no server, and the full middleware stack. Every request is a
coroutine, because every request Almasix serves is one.
class HomeTest(TestCase): async def test_the_home_page_answers(self) -> None: response = await self.get("/")
response.assert_ok().assert_see("Welcome")TestCase delegates get, post, put, patch, delete and their *_json
siblings to the client, so self.get(...) and self.client.get(...) are the
same call.
Making requests
Section titled “Making requests”await self.get("/posts", params={"page": 2})await self.post("/posts", {"title": "Hello"}) # form fieldsawait self.post_json("/api/posts", {"title": "Hi"}) # JSON in, JSON expectedawait self.put("/posts/1", {"title": "Edited"})await self.delete("/posts/1")await self.post("/webhook", "raw body text")| Method | Sends |
|---|---|
get / post / put / patch / delete |
Form fields, or a string as the raw body |
get_json / post_json / put_json / patch_json / delete_json |
JSON, with Accept: application/json |
options / head |
Nothing |
files= uploads, headers= and cookies= add to what the client already
carries, and follow_redirects= overrides the client’s setting for one call.
Headers, cookies, and tokens
Section titled “Headers, cookies, and tokens”Whatever is set on the client travels with every request it makes:
self.with_headers({"X-Request-Id": "abc"})self.with_header("Accept-Language", "sw")self.with_token("a-bearer-token") # Authorization: Bearer ...self.client.with_basic_auth("ada", "secret")self.client.without_token()self.client.flush_headers()
self.client.with_cookie("theme", "dark")self.client.with_cookies({"theme": "dark", "seen": "1"})Cookies the application sets are kept, so a login in one request is still a login in the next.
Sessions and authentication
Section titled “Sessions and authentication”self.with_session({"cart": ["cog", "sprocket"]})self.acting_as(user) # signed in on the `web` guardself.acting_as(user, "admin") # or another oneself.client.flush_session()with_session seeds the session the next request starts with, the way a
browser would carry it. acting_as writes the guard’s own login payload, so
the request arrives authenticated without going through the login form.
Redirects
Section titled “Redirects”Redirects are not followed unless you ask:
response = await self.get("/go-away")response.assert_redirect("/hello")
followed = await self.client.following_redirects().get("/go-away")followed.assert_ok()self.from_("/posts") sets the Referer, for a controller that redirects back.
Asserting on the response
Section titled “Asserting on the response”Every assertion returns the response, so they chain, and every failure says what the response actually was.
Status
Section titled “Status”response.assert_ok() # 200response.assert_created() # 201response.assert_accepted() # 202response.assert_no_content() # 204, and an empty bodyresponse.assert_successful() # 2xxresponse.assert_redirect() # 3xx, or a named locationresponse.assert_bad_request() # 400response.assert_unauthorized() # 401response.assert_payment_required() # 402response.assert_forbidden() # 403response.assert_not_found() # 404response.assert_method_not_allowed() # 405response.assert_not_acceptable() # 406response.assert_conflict() # 409response.assert_gone() # 410response.assert_unprocessable() # 422response.assert_too_many_requests() # 429response.assert_server_error() # 5xxresponse.assert_status(418)A failing status assertion quotes the body, because that is nearly always what explains it.
Headers and cookies
Section titled “Headers and cookies”response.assert_header("X-Pot")response.assert_header("X-Pot", "tea")response.assert_header_missing("X-Debug")response.assert_content_type("application/json")response.assert_cookie("session")response.assert_cookie("theme", "dark")response.assert_cookie_missing("tracker")response.assert_location("/hello")response.assert_redirect_contains("hello")response.assert_download("report.csv")The body
Section titled “The body”response.assert_see("Hello") # HTML-escaped, like Laravel'sresponse.assert_see("<b>", escape=False)response.assert_dont_see("Goodbye")response.assert_see_text("Almasix & friends") # tags stripped firstresponse.assert_dont_see_text("Goodbye")response.assert_see_in_order(["First", "Second"])response.assert_content("exactly this")response.assert_streamed_content("exactly this")response.text, response.content, response.status, response.headers, and
response.cookies are there when an assertion is not what you want.
response.assert_json({"ok": True}) # these pairs are in the bodyresponse.assert_json({"ok": True}, strict=True) # and nothing else isresponse.assert_exact_json({"ok": True})response.assert_json_path("user.name", "Ada")response.assert_json_path("user.id", lambda value: value > 0)response.assert_json_missing_path("user.password")response.assert_json_fragment({"title": "Search"})response.assert_json_missing({"title": "Nothing"})response.assert_json_count(2, "posts")response.assert_json_structure({"user": ["id", "name"], "posts": {"*": ["id"]}})response.assert_json_is_array("posts")response.assert_json_is_object()
assert response.json("user.roles") == ["author", "admin"]assert response.json()["ok"] is TruePaths are dotted, and * in a structure means “every item looks like this”.
Validation errors
Section titled “Validation errors”response.assert_invalid() # some field failedresponse.assert_invalid("email")response.assert_invalid(["email", "name"])response.assert_invalid({"email": "is required"})response.assert_valid() # nothing failedresponse.assert_valid("email") # this field did notassert response.errors() == {"email": ["Required."]}assert_session_has_errors and assert_session_has_no_errors read the same
errors, for a form that redirects back rather than answering with JSON.
The session
Section titled “The session”response.assert_session_has("cart")response.assert_session_has("cart", ["cog"])response.assert_session_has("total", lambda value: value > 0)response.assert_session_has_all({"cart": ["cog"], "step": 2})response.assert_session_missing("token")assert response.session("cart") == ["cog"]response.assert_view_is("posts.index")response.assert_view_has("posts")response.assert_view_has("title", "Posts")response.assert_view_missing("secret")The client records what Prism rendered while the request ran, so these read the data the template was given rather than the HTML it produced.
Debugging
Section titled “Debugging”response.dump() prints the status, the headers, and the body — the response
equivalent of dd().
Middleware
Section titled “Middleware”A test can stand middleware down for the rest of the test:
from almasix.testing import with_middleware, without_middleware
self.without_middleware() # all of itwithout_middleware(self.app, "csrf") # by aliaswithout_middleware(self.app, [VerifyCsrfToken]) # by classwith_middleware(self.app) # put it all backPrefer testing with the middleware in place; a route whose protection is only ever bypassed in tests is a route nobody has tested.