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

HackRice 7.5: How "uFilter" was born

I have a thing for Hackathon. I am a procrastinator. A lazy and procrastinator graduate student, not a nice combination to have. But still when I see hundreds of sharp minds in a room scrabbling over idea, hungry to build and prototype their idea. Bring it to life, it finally pushes me to activity, makes me productive.  That is why I love Hackathon, that is why I love HackRice, our resident Hackathon of Rice University.

TL;DR: if you just want to try the extension, chrome version is here and Firefox version is here.
I have been participating at HackRice since 2014, when I think for the first time it was open for non-rice students, and have been participating ever since. What a roller coaster ride it has been, but that is a story for another day. HackRice 7.5 being the last one I will be able to attend at Rice, it was somewhat special and emotional for me.
HackRice 7.5 was a tad different form the other iterations. For starters it was the first time it was being held in Spring semester…

Story of a Drupal theme mis-configuration, Hacking and Ministry of Defense India

If you have been following news or were online for past couple of hours you might have noticed this news making a tweet-storm and appearing all over your timeline regarding how India's Ministry of Defense website got hacked (allegedly by 'Chinese' origin).
Almost all the big media outlets covered it. Including
* Youtube : TimesNow * Times Now * Hindustan Times * NDTV
* Business Standard * Times of India An example of the coverage

Fueled by our own famous ministers chiming in with their own ideas

Action is initiated after the hacking of MoD website ( https://t.co/7aEc779N2b ). The website shall be restored shortly. Needless to say, every possible step required to prevent any such eventuality in the future will be taken. @DefenceMinIndia@PIB_India@PIBHindi — Nirmala Sitharaman (@nsitharaman) April 6, 2018
It all seemed for the fact that the homepage of the websites showed this image with a Chinese character
And though most of india's government portals and websites aren'…

LinuxCon China 2017: Trip Report

Linux Foundation held a combination of three events in China as part of their foray into Asia early this year. It was a big move for them since this was supposed to be the first time Linux Foundation would hold an event in Asia. I was invited to present a talk on Hardening IoT endpoints. The event was held in Beijing, and since I have never been to Beijing before I was pretty excited for the talk. However, it turned out the journey is pretty long and expensive. Much more than a student like me can hope to bear. Normally I represent Mozilla in such situations, but the topic of the talk was too much into security and not aligned much with the goals of Mozilla at that moment. Fortunately, Linux Foundation gave me a Scholarship to come and speak at LinuxCon China which enabled me to attend LinuxCon and the awesome team at Mozilla TechSpeakers including Michael Ellis and Havi helped me get ready for the talk.

The event was held at China National Convention Center. It's a beautiful and …