Skip to main content

Testing With Python: The Django Way Part 2

This is the second part of what I started here.
You can read the first post of Testing With Python: The Django Way to understand this one.

Now where were we?
The last time we left at test data. Now we will delve into requests.

GET Requests

The easiest of your views to test are the ones that only handle GET requests. All you have to do is retrieve the correct item from the database and display the information. Let's use the canonical book/author/publisher example. Suppose we're testing a view that displays the details for a particular book. 


def test_book_detail(self):
    testbook = Book.objects.all()[0]
    url = '/books/id/' + str(testbook.id)
    response = self.client.get(url)
    self.assertContains(response, testbook.title)

The first thing I want to stress, is that I think it's important that no specific test data is used in the tests. Always try to select objects you're going to use for your tests in a generic way. This way it won't matter how your test data gets moved around or changed over time. In this example I just picked the first book returned in the queryset for all books. What you don't want to do is something like this:

testbook = Book.objects.get(title="The Test Book")


That assumes your test database has a book with that title in it, which it may or may not. Over time it's bound to get mighty confusing trying to keep the data in your test database synchronized with the data in your tests, so it's best to save yourself the headache.
The Django TestCase class comes with a built in client to make all your requests. For a GET request all you have to do is pass it a URL and it will return the response generated by that request. The TestCase class also defines some new assertions in addition to the standard ones Python provides. assertContains, used in this example, takes a response object and verifies that it has a particular piece of text in it; in this case, the book's title. Optionally, you can specify a particular response code, and number of repititions for the piece of text. If you're expecting a particular url to not be found you might test:

badurl = '/books/id/notabookid'
response = self.client.get(badurl)
self.assertContains(response, "Book not found", status_code=404)


POST requests

Of course all of this data has to get into your database somehow. You know you don't want to put it all there yourself, so you're probably going to have your users do it. This likely means you'll have some forms on your site that you want them to submit. For our example, let's pretend authors are submitting their own books to the site. Let's assume a few things about how this function works, so we know what we're testing.



  • The user must be logged in to submit a book
  • A book requires a title, an author, and an ISBN field
  • Upon successfully submitting a book, we want to redirect the user to the detail page for that book.
  • The ID field for the Book model is auto-incremented (Django's default)
Simple enough, right? Django provides all the tools you need to test every aspect of this function.

def test_addbook(self):
    url = '/books/add/'

    author = Author.objects.all()[0]    
    user = author.user

    # User not logged in
    response = self.client.get(url)
    self.assertEqual(response.status_code, 403)

    self.client.login(username=user.username, password='password')

    # Valid user
    response = self.client.get(url)
    self.assertEqual(response.status_code, 200)

    # Invalid Form
    response = self.client.post(url, {'title': "Book Title",
                                      'author_id': author.id,
                                      'ISBN': "invalid ISBN"})
    self.assertFormError(response, 
                         "BookSubmitForm", 
                         'ISBN',
                         "ISBN field must contain a number")

    # Valid submission
    newid = Book.objects.aggregate(Max('id'))['id__max'] + 1
    redirect = '/books/id/%d' % newid
    response = self.client.post(url, {'title': "Test book",
                                      'author_id': author.id,
                                      'ISBN': 123456},
                                      follow=True)

    self.assertRedirects(response, redirect)
    self.assertContains(response, "Test book")

First you'll want to test a GET request for the url, to make sure the user can see the form properly to fill it in. Also check that the form is not visible to users who aren't logged in. In this case, that means simply making sure the response returns a 403 Forbidden status code. To log in as a user, simply use the self.client.login function with the appropriate name and password. It's easy enough to pull a random user from the ones you set up in the testsetup function, but I've violated my own rule a little by specifying a value for the user's password. In fact, I'm not sure there's any other way to do this. For the sake of simplicity, I'm just giving all of the users created in testsetup the same password. For POST requests, simply add a dictionary with all the required form fields and values. You can make sure the form validation is catching errors properly by submitting an invalid form and using the assertFormError assertion to verify that the appropriate form validation error occurs. This assertion takes 4 arguments: the response, the name the template uses for the form, the form field in question, and the error text (or a list of strings for multiple errors) that is expected. Finally, since the id for new books is just auto-incremented, we can assume the id for the book we create will be one more than highest one in the database yet. By plugging this into the book detail url, we have the address of the page we'll be redirected to when the book is successfully submitted. If you want to use assertContains or anything else to check the contents of the final page you get redirected to, be sure to add follow=True in the post function. This way it will return the final response, not the intermediate redirect response.
I suppose now is a good time to mention that the Django test runner will return the database to its original state (after populated by the setUp method) after each test. That means that any books you add during the test__addbook method will be deleted when that test is over. You won't have access to them in, for example, test_deletebook that you might write afterwards, so keep that in mind.

So this is what in a nutshell should get you started.
I've done these basic things to get myself up-to speed. Hope you'll find this useful too.Like I said, the documentation is up to Django's usual high standards, so definitely make the most of it.




Comments

Popular posts from this blog

Racecraft (Project Koru) · Prologue — The Origin Story

Racecraft · Prologue , The Origin Story It Started With a Wine List and a Question About Racing How a happy-hour conversation in the Bay Area turned into a trustable AI race coach , and then into a second version that runs entirely on a phone, on the NPU. This is the prologue to a five-part series. Two years ago(1st November, 2024) I was in the Bay Area for a GDE Summit. If you've never been: it's a couple of days of talks among Google Developer Experts, the kind of people who get unreasonably excited about a new on-device runtime, and then , mercifully , a happy hour where everyone stops performing and just eats. We ended up at a restaurant(Puesto Santa Clara), a long table of GDEs, and I was doing the most important engineering of the evening: trying to decide which wine to order. Across the table was Ajeet Mirwani . I don't even remember how the wine talk turned into racing talk , these things drift , but the moment the word "racing" ...

A Split‑Brain Neuro‑Symbolic Training Method for High‑Velocity Autonomous Coaching from Telemetry

 Author: Rabimba Karanjai Scope: Problem statement + data methodology + model training (no deployment discussion) Abstract Real‑time coaching in motorsport is a safety‑critical learning problem : a system must map noisy, high‑frequency telemetry to short, actionable guidance that remains physically consistent and avoids hazardous recommendations . This paper proposes a “Split‑Brain” training formulation that separates (i) a semantic coaching target (what action/critique should be expressed) from (ii) a reflexive interface (how actions are represented as compact, verifiable tokens). The approach trains a Small Language Model (SLM) in the Gemma family [1] using QLoRA fine‑tuning [2] , and introduces a telemetry tokenizer plus teacher‑student synthesis pipeline to generate instruction‑action pairs at scale. Core contribution: a reproducible method to convert “ golden lap ” differential tel...

The Throughput Trap: Benchmarking vLLM on OpenXLA and the Reality of Production LLM Serving

vLLM Systems · DevLab 2026, Deep Dive I was recently invited by the Google TPU team to speak at the OpenXLA Summer DevLab 2026 . This post breaks down our deep-dive evaluation of the matured vLLM + OpenXLA stack, the fundamental engineering mismatches between CUDA and XLA serving paths, and why traditional capacity metrics are lying to you. If you are operating large language models at enterprise scale right now, your platform architecture team is likely staring at a massive infrastructure crossroads: Should we migrate our core serving workloads from GPUs to TPUs? Historically, NVIDIA's CUDA ecosystem was the only serious option for user-facing, low-latency LLM generation. But here in 2026, the economics and infrastructure options have transformed. Google TPUs are highly available, cheaper per chip, and the open-source serving stack built around vLLM and OpenXLA has officially achieved absolute production readiness. Yet, when our infrast...