Chapter 22 · The test framework
22.12 Assertions on a response: assertContains, assertRedirects and assertTemplateUsed
SimpleTestCase gives every Django test case assertions about what a view answered: assertContains and assertNotContains look for text in a response’s body, assertRedirects checks a redirect and the page it leads to, assertTemplateUsed and assertTemplateNotUsed check which templates were rendered, and assertFormError and assertFormSetError check a form’s errors.
The assertions differ in what they read. A status and a body are on every response, so assertContains works on any, the test client’s or not. Which templates rendered and where redirects led are on a response only if the test client put them there: it sets them as HttpResponseBase and, after follow=True, HttpResponseBase, beside itself as HttpResponseBase and the request’s arguments as HttpResponseBase (The test client: Client and ClientHandler). Each takes msg_prefix="…", put before its own message with ": " after it; a parse failure under html=True leaves it out.
The harbour office’s tests of its responses:
# harbour/office/tests/test_responses.py
from django.contrib.auth.models import User
from django.template.loader import render_to_string
from django.test import TestCase
from harbour.fleet.models import Port, Ship
class ShipResponseTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.valletta = Port.objects.create(name="Valletta", country="Malta")
cls.endeavour = Ship.objects.create(name="Endeavour", tonnage=366, home=cls.valletta)
cls.beagle = Ship.objects.create(name="Beagle", tonnage=235, home=cls.valletta)
def test_list(self):
response = self.client.get("/ships/")
self.assertContains(response, "tons", count=2)
self.assertContains(response, "<h1>Ships</h1>", html=True)
self.assertTemplateUsed(response, "office/base.html", count=1)
def test_detail_tonnage(self):
response = self.client.get(f"/ships/{self.endeavour.pk}/")
self.assertContains(response, "368 tons")
def test_new_ship_needs_login(self):
response = self.client.get("/ships/new/")
self.assertRedirects(response, "/accounts/login/?next=/ships/new/")
def test_detail_extends_base(self):
with self.assertTemplateUsed("office/base.html"):
render_to_string("office/ship_detail.html", {"ship": self.endeavour})
def test_tonnage_of_zero(self):
self.client.force_login(User.objects.create_user("nora"))
response = self.client.post(
"/ships/new/", {"name": "Bounty", "tonnage": 0, "home": self.valletta.pk}
)
self.assertFormError(
response.context["form"], None, "Constraint “ship_has_tonnage” is violated."
)
assertContains and assertNotContains
SimpleTestCase and SimpleTestCase both call SimpleTestCase, which gets the body and counts the text in it:
- A response with a render method that has not yet rendered is rendered first. A
TemplateResponsethat came through the test client was rendered by the handler; one from a view called directly was not (SimpleTemplateResponseandTemplateResponse). - The status is compared with
status_code=200, or the one given, before any text is looked for: “Couldn’t retrieve content: Response code was 200 (expected 404)”. - A streaming response is read to its end, and
StreamingHttpResponseis set again from a list holding the joined bytes, so a next assertion finds it again (Streaming and file responses)..streaming_content - Text that is not bytes, or any text with
html=True, is made a string and the body decoded inHttpResponseBase; bytes are looked for in the raw body..charset - With
html=True, both sides are parsed byparse_htmlthroughassert_and_parse_html, a parse error failing with the side that was not valid HTML, and the parsed text is counted in the parsed body (Comparing HTML, XML and JSON:assertHTMLEqualandparse_html).
assertContains passes on one occurrence or more, or with count=2 on exactly two; assertNotContains on none. Their messages take the form of unittest’s own, “2 != 3 : …” or “False is not true : …”, and end with the body as bytes. The module’s run, lines left out:
$ python manage.py test harbour.office.tests.test_responses
...
.F...
======================================================================
FAIL: test_detail_tonnage (harbour.office.tests.test_responses.ShipResponseTests.test_detail_tonnage)
----------------------------------------------------------------------
...
AssertionError: False is not true : Couldn't find '368 tons' in the following response
b'<!doctype html>\n<html lang="en">\n<head><title>Endeavour</title></head>\n<body>\n<main>\n<h1>Endeavour</h1>\n<p class="tonnage">366 tons</p>\n<p class="home">Home port: Valletta</p>\n</main>\n</body>\n</html>\n'
...
assertRedirects
SimpleTestCase checks that the response redirected with status_code=302 or another given, that it led to the expected URL, and that the page there, where it is fetched or was followed, answers target_status_code=200.
A followed response ends the chain, and the HttpResponseBase the test client gave it lists each redirect’s URL and status. The chain must not be empty; its first status and its last URL are the ones compared, and the response’s own status must be the target’s. Nothing is fetched, and a request redirected twice is checked against where it ended.
A response not followed is the redirect itself: its status is compared, and its URL is HttpResponseRedirectBase, the Location header, a path not starting with "/" joined to the request’s path. Unless fetch_redirect_response=False, the target is then fetched. A host in the URL must be one validate_host accepts from ALLOWED_HOSTS, such as "testserver", which the test environment adds; another is a ValueError, not a failure, suggesting the host be added or not fetched. The client that made the response then gets the path without its host, the query as a QueryDict, secure=True for https, and the Client.extra and Client.headers stored by the latest call of its methods named for HTTP methods; that page must answer with the target status, as new_ship’s login page did.
Both ways end in SimpleTestCase, which sorts the pairs of each URL’s query string, parsed by urllib, and compares the rest as written: a run found new_ship’s relative Location unequal to the same URL with “http://testserver” before it.
assertTemplateUsed and assertTemplateNotUsed
SimpleTestCase and SimpleTestCase learn what rendered from template_rendered, which the test environment’s render method sends before rendering each template of Django’s own language (The test environment: setup_test_environment). While a request runs, the test client keeps each template the signal reports, and the list becomes the response’s HttpResponseBase. assertTemplateUsed reads their names, leaving out a template with none, as one compiled from a string. The list holds every template the request rendered, in the order each began: “office/ship_list.html”, then the “office/base.html” it extends, and, on a page with a form, those of its fields and widgets. count=1 is how many times the name is in it. An empty list fails as “No templates used to render the response”, a missing name with the templates used listed. A response without templates, given with a name, is a ValueError: the method “is only usable on responses fetched using the Django test Client”.
Given only a template name, the method returns a context manager, which checks nothing outside a with: SimpleTestCase takes a first argument without templates for the name, and no argument at all is a TypeError. The context manager, _AssertTemplateUsedContext, connects its own receiver to template_rendered on entering, disconnects it on leaving, and checks what it heard only if the block raised nothing. It hears any render in the block, as the render_to_string of test_detail_extends_base.
assertTemplateNotUsed works both ways, fails if the name is listed, and takes no count.
assertFormError and assertFormSetError
SimpleTestCase takes a form, a field’s name and the errors expected, one message or a list (to_list). It fails for an unbound form, which “will never have any errors”, and for a field the form lacks. With None for the field it compares BaseForm, otherwise the field’s entry in BaseForm.errors or an empty list, so errors=[] asserts a field is clean; reading the errors validates a bound form not yet validated (Validation: full_clean, cleaned_data and the errors). A difference fails through unittest, which shows both lists. In test_tonnage_of_zero the check constraint’s message is an error of the whole form.
SimpleTestCase also takes a form’s index: form_index=None with a field is a ValueError, and an unbound formset or an index past the last form fails. An index checks that form as above; None checks BaseFormSet (SimpleTestCase; Formsets: BaseFormSet and the management form).