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

FirefoxOS, A keyboard and prediction: Story of my first contribution

Returning to my cubical holding a hot cup of coffee and with a head loaded with frustration and panic over a system codebase that I managed to break with no sufficient time to fix it before the next morning.  This was at IBM, New York where I was interning and working on the TJ Watson project. I returned back to my desk, turned on my dual monitors, started reading some blogs and engaging on Mozilla IRC (a new found and pretty short lived hobby). Just a few days before that, FirefoxOS was launched in India in the form of an Intex phone with a $35 price tag. It was making waves all around, because of its hefty price and poor performance . The OS struggle was showing up in the super low cost hardware. I was personally furious about some of the shortcomings, primarily the keyboard which at that time didn’t support prediction in any language other than English and also did not learn new words. Coincidentally, I came upon Dietrich Ayala in the FirefoxOS IRC channel, who...

April Fool and Google Part 2: A Round Up of ALL of Google’s April Fools Jokes

Ok....this post I think will contain all of the pranks I could find  for today. After my last post here http://rkrants.blogspot.com/2012/04/april-fool-and-google-my-favorite.html Last Time I reported Only a handful of the pranks.. Understandable, as it was only the morning. After that I stumbled upon more of them Which I am gonna round up here. Now staring with the list. The very first one is obviously our favourite Google Maps Quest The above is their official video. In a post in Google Plus they say about it as follows  Today  + Google Maps  announced Google Maps 8-bit for NES. With #8bitmaps , you can do everything you'd normally do in Maps—search for famous landmarks and sites around the world, get directions and even use Street View. Just in time for April Fool's Day, Google has introduced Google Maps Quest, a retro 8-bit version of its mapping tool that is... totally awesome. In a characteristically whimsical video, availabl...

Curious case of Cisco AnyConnect and WSL2

One thing Covid has taught me is the importance of VPN. Also one other thing COVID has taught me while I work from home  is that your Windows Machine can be brilliant  as long as you have WSL2 configured in it. So imagine my dismay when I realized I cannot access my University resources while being inside the University provided VPN client. Both of the institutions I have affiliation with, requires me to use VPN software which messes up WSL2 configuration (which of course I realized at 1:30 AM). Don't get me wrong, I have faced this multiple times last two years (when I was stuck in India), and mostly I have been lazy and bypassed the actual problem by side-stepping with my not-so-noble  alternatives, which mostly include one of the following: Connect to a physical machine exposed to the internet and do an ssh tunnel from there (not so reliable since this is my actual box sitting at lab desk, also not secure enough) Create a poor man's socks proxy in that same box to have...