skip to content

In Django tests, what do assertContains and assertTemplateUsed check that a plain substring search on response.content does not?

level: juniorimportance: should knowfreq 40%

answer

  1. status code comes for free
  2. bytes versus text
  3. html=True compares structure
  4. templates recorded by the client

basics

~20 s

assertContains also asserts the status code (200 by default), decodes the body, can count occurrences and, with html=True, compares parsed HTML. assertTemplateUsed checks which templates the test Client recorded as rendered, which a content search cannot see.

solid answer

~40 s

`self.assertContains(response, text, count=None, status_code=200, html=False)` first asserts the response status, so an error page that happens to contain the text fails; it renders an unrendered `TemplateResponse`, decodes the bytes with the response charset, and can require an exact `count`. With `html=True` both sides are parsed and compared as HTML, so attribute order and insignificant whitespace do not matter. `assertNotContains` is the mirror. `assertTemplateUsed(response, "dashboard/index.html")` checks the list of templates the test Client captured through the template-rendered signal, including included and extended ones, which the output alone cannot prove; it works only on responses fetched with the test Client, or as a context manager. A bare `b"Welcome" in response.content` checks none of this and gives a useless failure message.

code

python · 5 lines
python
response = self.client.get(reverse("dashboard"))
# passes even if the page is a 500 that echoes the word
assert b"Projects" in response.content
# fails with a clear message unless status is 200
self.assertContains(response, "Projects")

go deeper

for a junior

Use assertContains and assertNotContains instead of raw bytes checks, and remember the default expected status is 200.

for a middle

Explain the extras: count, html=True semantic comparison, rendering of TemplateResponse, and how the Client records templates for assertTemplateUsed.

for a senior

Pick assertions that fail for the right reason: structural HTML checks, template assertions where the view branches, and context assertions for data.

for a principal

Balance brittle markup assertions against behaviour-level checks so template refactors do not break hundreds of tests.

## Why not just search the bytes? A first Django test often looks like `assert b"Welcome, Ana" in response.content`. It works, but it proves less than it seems and fails unhelpfully. Django's `SimpleTestCase` (and therefore `TestCase`) adds response-aware assertions that close those gaps. ## `assertContains` and `assertNotContains` Signature: `assertContains(response, text, count=None, status_code=200, msg_prefix="", html=False)`. It performs, in order: 1. **Renders deferred responses.** If the response is a `TemplateResponse` that has not been rendered yet, typical when a view is called with a `RequestFactory` request, it renders it first. 2. **Asserts the status code.** The response must have `status_code` (200 by default). A 404 or 500 page that happens to contain your text fails with "Couldn't retrieve content". 3. **Decodes the body** with the response's charset when `text` is a `str`, so you compare text with text instead of juggling bytes. Streaming responses are joined first, and since Django 6.1 the assertion can be called repeatedly on the same `StreamingHttpResponse`. 4. **Counts occurrences.** With `count=None` one occurrence is enough; with an integer, the number must match exactly, useful for "the table has three rows". 5. **Optionally compares HTML semantically.** With `html=True` both the response and `text` are parsed, so `<a class="btn" href="/x">` matches `<a href="/x" class="btn">` and whitespace differences are ignored. `assertNotContains(response, text, status_code=200, html=False)` asserts the status and that the text is absent, the right tool for "an anonymous user must not see the admin link". Failure messages include the searched text and, since Django 5.1, the response content, so a failing test explains itself. ## `assertTemplateUsed` and `assertTemplateNotUsed` Signature: `assertTemplateUsed(response=None, template_name=None, msg_prefix="", count=None)`. - The **test Client** listens to Django's template-rendered signal during the request and stores every template rendered, including base templates pulled in by `{% extends %}` and fragments pulled in by `{% include %}`, as `response.templates`. - `assertTemplateUsed` checks that `template_name` appears in that list, optionally exactly `count` times. - It works only for the **`DjangoTemplates` backend**, because that is what emits the signal. - Passed a response that did not come from the test Client, for example one returned by calling a view with a `RequestFactory` request, it raises **`ValueError`** saying it is only usable on responses fetched using the Django test Client. - It also works as a **context manager**, which captures templates rendered inside the block and works with any caller: ```python with self.assertTemplateUsed("dashboard/widgets/usage.html"): render_to_string("dashboard/index.html", {"user": user}) ``` ## A dashboard example ```python from django.test import TestCase from django.urls import reverse class DashboardPageTests(TestCase): def test_signed_in_dashboard(self): self.client.force_login(self.user) response = self.client.get(reverse("dashboard")) self.assertTemplateUsed(response, "dashboard/index.html") self.assertTemplateUsed(response, "base.html") self.assertContains(response, "Welcome, Ana") self.assertContains(response, '<tr class="project">', count=3) self.assertContains(response, '<a href="/settings/" class="nav">Settings</a>', html=True) self.assertNotContains(response, "Admin panel") ``` ## What each approach proves | Check | Status code | Charset-aware | Counts | Structural HTML | Template choice | |---|---|---|---|---|---| | `b"..." in response.content` | no | no | no | no | no | | `assertContains` | yes | yes | optional | with `html=True` | no | | `assertTemplateUsed` | no | n/a | optional | n/a | yes | ## Choosing the right assertion - Prefer **`assertContains`** for visible output, and add `html=True` whenever the expected text is markup. - Use **`assertTemplateUsed`** when the view chooses between templates (an empty-state template, a partial for an HTMX request), where the content alone is ambiguous. - Use **`response.context`** when you want to assert data rather than presentation, such as the number of projects passed to the template. - Do not assert on template names alone: a template can be used and still render the wrong data. ## Reading failures quickly - A failure that says **"Couldn't retrieve content: Response code was 302 (expected 200)"** usually means the test forgot to sign in, so the protected page redirected before rendering. - **"No templates used to render the response"** means the view returned a response that rendered no Django template, such as a redirect or a JSON response. - A count mismatch prints the actual number of occurrences, which often reveals a duplicated fragment in a base template. Each message points at a different root cause, which is the practical payoff of using the dedicated assertions instead of a bare `assert`.

  • How would you assert that a Django view rendered exactly three rows of a table without writing a parser?
    Use `self.assertContains(response, '<tr class="project">', count=3)`, which asserts status 200 and an exact occurrence count.

saying these in an interview costs you the question

  • Believing assertContains ignores the response status code
  • Thinking html=True does a case-insensitive substring search
  • Assuming assertTemplateUsed works with any template engine
  • Expecting assertTemplateUsed to see only the top-level template, not included ones
  • Calling assertTemplateUsed on a RequestFactory response and expecting it to work