Chapter 14. Django Testing

In order to facilitate the production of higher-quality code (and maintain the sanity of developers), Django comes equipped with an automated testing suite. Writing perfect code is next to impossible. It can be done, but there's a very good reason that computer keyboards have a "Backspace" key on them: people make mistakes. While a few developers probably like to think that they embody super-human qualities such as never writing faulty code, the simple truth is that we all make mistakes without realizing it at first.

In this chapter, we are going to take a look at the Django test suite and how you can use it to ensure that the code you've written for your web site is behaving the way you expected. We're going to write two kinds of tests: unit tests and functional tests. There are several other kinds of tests that might be done on a much larger web application, such as browser, regression, usability, integration, acceptance, performance, and stress tests. However, for simplicity in this introductory chapter, we're just going to stick with unit and functional tests. For small- to medium-sized web sites, these two kinds of tests are likely to catch most of the bugs that might get introduced in your code.

Django also comes equipped with another type of test that you can write: doctests. Doctests are little snippets of code that resemble the interaction you might see inside your Python shell. You put these tests inside the docstring on your models and, when the test runner executes, it will attempt to execute the bits of code you have in the docstring and check to make sure that the results it gets when it executes those lines are the same as the results you have written in your docstring.

Doctests can be very useful in some cases, but I'm not going to cover them in this chapter. This is largely because I haven't ever encountered a type of test I wanted to do but couldn't do with the Django testing framework instead of a doctest. Furthermore, at the risk of starting a Python flame war, I believe that doctests are clutter inside your application's docstrings, which are really intended to serve as comments that describe what your code is supposed to do. On the whole, I felt that focusing our efforts on the regular Django testing framework in depth instead of trying to include doctests basics would yield you a higher return on your invested time.

While writing tests for a Django web application is not terribly difficult, learning how to manage the state of them can be a difficult task. We're going to write functional tests to make sure that the interface is behaving the way you would expect, and write unit tests to ensure that the behavior of your models is the way you would expect. Lastly, we'll write some basic security tests to make sure that the measures we took in Chapter 12 are in place and working as we would expect them to be.

Why We Test

Even if you are some superstar developer who is able to produce hundreds of lines of bug-free code a day, there is still the problem with the interrelated parts of your web application. That is, the dependencies in one app rely on the code that runs in another app someplace else. Sure, you can take great pains to architect your code so that all the separate parts are loosely coupled, but that won't completely eliminate the problem. Testing makes the maintenance of a large application much easier. When you edit one app, you can potentially break the code in other places on your site, and because it's just not feasible to manage all of these dependencies inside your head, you need to test your code.

This might not sound like a very big problem in theory, but as your application grows in complexity, so too does the need for testing. Imagine that customers can add products to their shopping cart from the product page, from product thumbnails, and from their wish list pages. You make a small change to how products get added to the shopping cart, and you update the product page and product thumbnail template...but you forget about the wish list page. One month later (in this story, not many people are using your wish list pages), a customer complains that they're having trouble adding products to the cart from their wish list, leaving you with the task of figuring out what's wrong.

There's a problem with this. I don't know about you, but I have trouble remembering details about the code that I wrote an hour ago. I can drudge up the memory and the logic behind what I did pretty easily, but I still need to drag the waters. One whole month later, and there's no recovering the logic behind what I did from my own head. At that point, I have to approach the code I wrote as though it were someone else's code. I just need to read through it and dissect what's it's doing, which tends to be much more difficult than fixing code when it's fresh in your mind.

The moral is that new bugs are much easier to deal with if you catch them earlier rather than much later. Preferably, all the automated tests are run every time you make a change, before you check your code into your version control repository, to ensure that your changes haven't broken any existing functionality. Lots of coding methodologies, like agile development practices, have the writing of tests baked right into the procedure. I don't really think it matters what coding method your organization uses; writing tests is immensely helpful to the production of quality code, regardless of the rest of your process.

Adding automated testing to your code is probably just one more step in a testing process you already do without realizing it. Whenever you write the code for a new web page, the first thing you do is fire the page up in a browser to make sure that everything is formatted correctly and appears the way you intended it to. Writing Python code to test the state of your application for you is this kind of manual testing on steroids. It's faster, more thorough, and it's easy. And when you take the trouble to make a process easy for yourself, you're much more likely to do it.

However, I don't want to spend lots of time in this chapter trying to convince you of why you should be writing tests. If I haven't piqued your curiosity by this point, then chances are I won't be able to.

Ideally, we would have been writing tests for our code right from the start, adding them in conjunction with each piece of our application to ensure that we had test coverage for every part. I opted not to do this, simply because the flow of the book would have been hampered had I included tests in every chapter. However, in a real-world application (and a much more perfect book, perhaps), I would have been writing my tests right alongside my models, views, and templates, every step of the way. Getting started now, in any case, is better than not doing it at all.

How to Test Code

One of the tenets of the Test-Driven Development methodology is that the developer should write the test code first, run it with the expectation that they will fail (because the actual code doesn't exist yet), write the actual code, and then re-run the tests with the expectation that they will succeed. I've never really been able to get my brain around coding in this fashion... although in the agile development world, writing tests first makes sense, particularly if you're coding without any written specification. Writing tests before writing code forces you to design what you're going to write. In spite of this, I still don't think writing tests first really replaces the need for a spec up front... but to each their own. Personally, I don't start coding until I have a specification and I write my tests after I write the code, but that doesn't make it the right approach 100% of the time. Use whatever techniques you find help you test your code effectively.

Writing tests is all about gauging expectations. When the code you've written runs, what do you expect to happen? At the most basic, here is how you can approach writing tests:

  1. Determine the state of things before any action is taken, list the characteristics, and test them.

  2. Write code to execute the action in question.

  3. Determine which characteristics the action in #2 should have altered, and test that they've changed.

That three-step process is a little esoteric. Let's consider a very simple case to test a customer coming to our site and logging in. We first expect that the user will not be authenticated, so we check their session and make sure that it doesn't contain any authentication data. Then, we emulate the event of the customer logging into the site. Finally, we check that the customer's session now has valid authentication data in it.

While tests can get much more complicated than this, and you'll find yourself testing multiple things in the "before" and "after" stages of each action, the basic strategy remains the same: to determine that your application is responding to inputs the way you expect.

Creation of the Test Database

The Django test runner should not run tests on your development or production database. Often, you'll want to run tests that perform creation, manipulation, or deletion of large amounts of data in your database, and you don't want all of these changes to persist after your test suite has run. For this reason, Django creates a separate database on which to run your project's tests. This database is given the same name as your development database, prefaced with test_.

When the Django test runner tries to access to the test database, it will use the same credentials stored for your production database. Right now, the MySQL user in our settings.py file doesn't have permission to create a new database named test_ecomstore, let alone the permissions to create tables and start mucking around with the data. For this to work, we need to first make sure that the MySQL login our project uses has permission to create and manipulate the test database. Inside of MySQL, run the following two commands:

mysql> CREATE DATABASE test_ecomstore;
mysql> GRANT ALL ON test_ecomstore.* TO 'ecomstore'@'localhost';

Now the test database is created, and our project will be allowed to access it when tests are run.

Python & Django Test Methods

To aid in testing, Python and Django offer default test methods that allow you to check the conditions of your application while the tests are running. These allow you to assert that certain things are true at any given point, or to cause the test to fail if the condition you're testing for is not met.

Some of the more common and useful test methods available on the basic Python unittest.TestCase instance are listed in Table 14-1.

Table 14-1. The Python unittest.TestCase testing methods

Method Name

assertEqual(actual, expected)

assertFalse(expression)

assertNotEqual(actual, expected)

assertRaises(ExceptionClass, method)

assertTrue(expression)

failIf(expression)

failIfEqual(actual, expected)

failUnless(expression)

failUnlessEqual(actual, expected)

Some of these are functionally equivalent. For instance, using failUnless() and assertTrue() are both testing that the argument you've passed in evaluates to True. Any Python value that evaluates to False will cause the test to fail. When there is more than one option available to you, use the one that's most logical for the given situation. As one simple guideline, try to avoid double negatives. For example, instead of this:

self.failUnless(not logged_in)

just reverse the logic of the process. If you really expect the logged_in variable to have a value of False:

self.assertFalse(logged_in)

This will greatly enhance the readability of your testing code. All of these methods take an optional string parameter after the required arguments, which is a message that will be displayed when the expression fails. For example:

self.assertFalse(logged_in, 'User is logged in!')

There are also available test methods on the enhanced Django TestCase class, which inherits from the Python unittest.TestCase class. These extra methods are designed to help you use the Python unittest base class in order to write tests that check conditions specific to the web environment, such as the flow between pages, form errors given invalid input, and templates used to render view responses:

Table 14-2. The Django TestCase testing methods

Method Name

assertRedirects(response, url)

assertFormError(response, form, field, [error1, error2...])

assertTemplateUsed(response, template_name)

assertTemplateNotUsed(response, template_name)

assertContains(response, value)

assertNotContains(response, value)

The good news is that if you're a Python developer and you've been using the unittest base class for writing unit tests, starting to use the Django TestCase subclass for web testing will be a very easy transition for you. If the Django TestCase methods still don't make much sense to you at this point, keep on reading. We're going to use all of them in the tests we're going to write in this chapter.

Anatomy of a Test Class

So when you create your test classes, where should you put them? If you're using at least Django 1.1, the easiest thing to do is simply to put them in the tests.py file that Django creates inside every app in your project. If you're using an earlier version of Django, you can easily create tests.py files for your apps manually. When you run the test runner, it will look in each of your INSTALLED_APP directories and load any test classes that it finds inside your tests.py files.

Inside these files, you define test classes that inherit from the Django TestCase class, and create methods inside each test class. When you run the test runner, each class is instantiated and each of the test methods inside the class runs. Here is how an example test class you define might look:

class MyTestClass(TestCase):
    def test_something(self):
        pass

    def test_something_else(self):
        pass

Test methods are prefaced with test_. Test classes can contain other helper methods that don't start with test_ that can be called by test methods, but these are not called by the test runner directly. In the context of test methods, the pass placeholder keyword for empty Python blocks takes on a logical, more literal meaning.

When you write your tests, it helps immensely if you give them descriptive names that describe exactly what part of your application their testing, as well as the nature of the response that you're expecting. That way, when you run your tests and one of the test methods encounters a failure, you'll have some context about the problem that was encountered just by reading the name of the test method that failed.

Testing the Product Catalog

So let's start by writing a few small sample tests for our application, to see how the test methods and Django test classes are designed to be used. These first tests that we're going to write are not really unit tests in the strictest sense of the word. They are more functional tests, because we're testing that our application is working as expected when a user is browsing the product catalog. We'll get to some basic unit tests for our models a little later.

In this section, after writing some rudimentary tests for a new customer accessing the home page, we'll look at how to run Django tests. We'll also look at how overriding the setUp() and tearDown() methods on test classes can reduce the repetition in your tests, by giving you a simple means of defining code that runs before and after each method.

Writing Functional Tests

First, let's consider the simplest of all cases: a new user comes to the site, not logged in, and loads the catalog home page. What kinds of things can we expect to happen in this case? Well, we expect that the home page will load without any errors, so we can check the status code of the response and make sure that it's the 200 that's indicative of a success. Open the tests.py file in your catalog app. If there is any placeholder code in there already that Django added when it created the app, you can remove all of it and replace it with the following:

from django.test import TestCase, Client
from django.core import urlresolvers

import httplib

class NewUserTestCase(TestCase):
    def test_view_homepage(self):
        client = Client()
        home_url = urlresolvers.reverse('catalog_home')
        response = client.get(home_url)
        # check that we did get a response
        self.failUnless(response)
        # check that status code of response was success
        # (httplib.OK = 200)
        self.assertEqual(response.status_code, httplib.OK)

Inside this new test class, we create a new instance of the Django client test class, Client, which is designed to help simulate the actions of a user. We make our new Client instance request our home page, using the get() method on our test client instance. There are two methods you can call on the test client instance in order to make requests to pages in your test methods: get() and post(). Each makes the request using its corresponding HTTP verb, and both take the URL path of a page on your site. Mostly, we're going to use get() to emulate catalog browsing, but later we'll use post() to test form submissions.

We check to see that a response was returned, and that the status code indicates a success. Notice that we're using the urlresolvers module in order to get the URL path of the homepage before we navigate to it. This is very useful and, just like in our actual web application, makes our code much less prone to 404 Page Not Found errors due to simple typos. Also, if we ever choose to update our URL entries to be something different, the tests won't break because of it.

Also notice that instead of hard-coding the value of 200 to check for the status code, we're falling back on the httplib module to provide the numerical HTTP status codes for us, looking them up by their enumerated names. Table 14-3 shows the names of the status codes that you'll be using most frequently in testing your application.

Table 14-3. httplib HTTP Common Status Codes

Status

HTTP Status Code

httplib.OK

200

httplib.MOVED_PERMANENTLY

301

httplib.FOUND

302

httplib.FORBIDDEN

403

httplib.NOT_FOUND

404

In order to run these tests and ensure that users can, in fact, load our home page, let's take the test runner for a spin right now. To run the tests for a single app in your project, you can use the syntax manage.py test [app name]. Run the following in your shell:

$ python manage.py test catalog

You should see a lot of code output that's creating all of the tables and indexes for the test database, and then some output about how your tests succeeded. If there were any errors in the syntax of your code, the test runner will fail to start up. You cannot test a project that won't run under normal circumstances.

Note

When you run your tests for the first time, you may get an error about the test database already existing, and a prompt for a "yes/no" about whether it should try and create it. This is because we created the database test_ecomstore earlier in the chapter manually. Remember, the test database is created at the start of each run and then dropped at the end. If you ever interrupt the test runner before it has a chance to get to the last step (say, by using Ctrl-C), then it will get confused the next time it starts because it wasn't expecting the database to exist already. As a courtesy, it prompts you before overwriting this existing database. In most cases, and in this one as well, you'll be all right typing "yes".

You can run tests for your entire project by merely omitting the name of the app from this call in your shell. If you run this, however, it may take a little while, as all the tests bundled with Django will be run in addition to your own tests:

$ python manage.py test

Before we go much further, I should point out one very important test method that the Python and Django test cases provide: the setUp() method. When you run your tests, the Django test runner will call the setUp() method on each test class instance before it runs any of the test methods. This is useful in cases where you want to create variables that will be used across multiple test methods.

Take our test Client class: as we start adding test methods, we'll find ourselves creating an instance of this class in every method. Instead of all this repetition, we can simply instantiate the Client class once, in the setUp() method of the class, assign the instance as a variable on the test class, and use that client instance on the class in our test methods. So, to refactor the work we just did:

class NewUserTestCase(TestCase):
    def setUp(self):
        self.client = Client()

    def test_view_homepage(self):
       home_url = urlresolvers.reverse('catalog_home')
        response = self.client.get(home_url)
        # check that we did get a response
        self.failUnless(response)
       # check that status code of response was success
       self.assertEqual(response.status_code, httplib.OK)

The setUp() method can contain assertions and tests for failures itself. As a practical means of demonstrating this point, let's add code that tests to ensure that the user is not authenticated. We can check this by looking at their session data. Authenticated users will have an entry named _auth_user_id in their session dictionary, while anonymous users won't have this yet. We can retrieve this from the Django auth module, by grabbing the SESSION_KEY constant:

from django.contrib.auth import SESSION_KEY

class NewUserTestCase(TestCase):
    def setUp(self):
        self.client = Client()
        logged_in = self.client.session.has_key(SESSION_KEY)
       self.assertFalse(logged_in)

The setUp() method is run before each test method in the class. As an illustration of this, the following test class will run successfully without any failures:

class ExampleTestCase(TestCase):
    def setUp(self):
        self.my_value = 1

    def test_setup_method(self):
        self.my_value = 0

    def test_setup_method_2(self):
        self.assertEqual(self.my_value, 1)
       self.failIfEqual(self.my_value, 0)

If, in the course of running the setUp() method, the test runner encounters any failure or exceptions in the code, the test will return an error response and it will be assumed that the test has failed. There is also a corresponding tearDown() method, which runs after all of the test methods, and is useful for performing any cleanup operations on data that you don't need.

Go ahead and re-run the tests for the catalog app a second time, to make sure that these few additions correctly assert themselves without any unexpected problems.

Managing Test State with Fixtures

Now let's test some of the other pages, which are bound to be a bit more complex than the home page. Let's add a single test method for navigating to an active category page. Add this method inside your tests.py file:

from ecomstore.catalog.models import Category

class NewUserTestCase(TestCase):
    ... code omitted here ...

     def test_view_category(self):
         category = Category.active.all()[0]
         category_url = category.get_absolute_url()
         # test loading of category page
         response = self.client.get(category_url)
         # test that we got a response
         self.failUnless(response)
         # test that the HTTP status code was "OK"
         self.assertEqual(response.status_code, httplib.OK)

Now if you go back and run the tests for the catalog app, you should get some interesting results. While the test database is still created and the tests run, you should see a message about a failure we encountered when the test suite was trying to load one of the categories. This is because our test database is empty by default. While the categories table does exist, it doesn't contain any category data. Because we're trying to get the zero-index on an empty result set, an IndexError is raised trying to get the category.

Testing a data-driven dynamic web site without any data won't be sufficient for our purposes. In order to populate your test database schema with some actual data you can reference in your test methods, you could create new objects from scratch right in your setUp() method, using the standard Django ORM methods. However, this is a lot of extra typing you need to do, and clutters up your test classes with all kinds of code that does little more than create some basic classes.

A much superior approach to loading initial data is to use fixtures. Fixtures are test files that reside in your project's apps folders, containing JSON or YAML-formatted data that conforms to the schema of your database tables.

Django provides a means for you to create fixtures from your test database, by simply dumping the data from the database you've been using for development all along. If, at this point, your product database already contains thousands of products, you might not want to run this, since it might take a while to run. If that's the case, simply choose another app that has a small amount of data in it, and run this command:

$ python manage.py dumpdata catalog --indent=2

This produces some nicely formatted JSON text that we can use as fixtures in our test cases. The dumpdata command dumps JSON data by default, but you can also choose YAML as the outputted format if you'd prefer. Inside your catalog app, create a new subdirectory called fixtures. Then, go back and run this command to dump the data into that directory, into a file named initial_data.json:

$ python manage.py dumpdata catalog --indent=2 > catalog/fixtures/initial_data.json

This will create a fixtures file that contains all of the categories, products, and product reviews currently in our development database. The file name initial_data (followed by any extension for a data format that Django supports for fixtures, such as .json or .yaml) means that the test runner will load this data into the test database after it creates the test database and before it runs any test methods.

You're free to edit this file by hand. As a matter of fact, in order to test that our ActiveProductManager is working, we should make sure that at least one of the products in our fixtures file has its is_active field set to False. Open the new initial_data.json file, choose one of the catalog.product model records, and make sure that at least one has is_active set to 0, which is False in JSON. (At this point, I'm assuming you have more than one product in your database.) When you run Django tests, any fixtures found in your apps is loaded into the database. Fixtures are loaded before each test is run, so you are free to wipe out all existing data in one of your test methods, even though other subsequent test methods might depend on this data. For example, given an initial_data.json file that contains fixtures with Product model data in them, the following two tests succeed:

from ecomstore.catalog.models import Product

class MyTestCase(TestCase):
    def test_delete_all(self):
        for p in Product.objects.all():
            p.delete()
        # check that the data
        self.assertEqual(Product.objects.all().count(),0)

    def test_products_exist(self):
        self.assertTrue(Product.objects.all().count() > 0)

Fixtures can also be specified in each test class, by passing in a list of fixture file names to a property of the test class called, aptly enough, fixtures. The following will scan the project's fixtures directories for any fixture files with these names:

class MyTestCase(TestCase):
    fixtures = ['products', 'categories']

    def test_something(self):
       # test stuff here

Including the extension of the fixture file is optional. In this case, the test runner will look for any files that have valid fixture extensions, such as .json or .yaml, and load them into the test database before running any test methods for that class.

Category Testing

So, now that we have some fixtures in place, let's get back to the category testing that we were trying to do before. Inside tests.py in your catalog app, add the following lines to the test class we created in the last section:

class NewUserTestCase(TestCase):
    ... code omitted here ...

    def test_view_category(self):
        category = Category.active.all()[0]
        category_url = category.get_absolute_url()
        # test loading of category page
        response = self.client.get(category_url)
        # test that we got a response
        self.failUnless(response)
        # test that the HTTP status code was "OK"
        self.assertEqual(response.status_code, httplib.OK)
       # test that we used the category.html template in response
       self.assertTemplateUsed(response, "catalog/category.html")

Let's have a quick look at template testing. One of the test methods we've been using is assertTemplateUsed(), which takes a response and the path to a template file in our templates directory as an argument. The method checks that the template file specified was used by the view function in generating a response. So far, we've just been putting the name of the template file into the function to check it.

While this works, it's less than ideal because if we ever change the name of the template that the URL is using, it will break the test, even if we correctly update the URL entry and the view function with the view template name. A much better solution is to look up the name of the template being used to render the response by retrieving the value of the template_name keyword argument using the resolve() method on the URL. You can test this inside a Python shell in your project:

>>> from django.core import urlresolvers
>>> urlresolvers.resolve('/product/something/')
(<function show_product at 0x11e0488>, (),
   {'template_name': 'catalog/product.html', 'product_slug': 'some-product'})
>>> urlresolvers.resolve('/product/something/')[2]['template_name']
'catalog/product.html'

So, we can refactor our existing test method to look up the template_name variable on the URL entry and use that value in the line that checks the template used:

from django.core import urlresolvers

class NewUserTestCase(TestCase):
    # other test methods here

    def test_view_category(self):
        category = Category.active.all()[0]
        category_url = category.get_absolute_url()
# get the template_name arg from URL entry
url_entry = urlresolvers.resolve(category_url)
template_name = url_entry[2]['template_name']
  # test loading of category page
  response = self.client.get(category_url)
  # test that we got a response
  self.failUnless(response)
  # test that the HTTP status code was "OK"
  self.assertEqual(response.status_code, httplib.OK)
  # test that we used the category.html template in response
 self.assertTemplateUsed(response, template_name)

This test method is pretty good, but at the moment, it's really only testing that Django is successfully generating and returning the response with the correct template file. There's nothing here to test that the content on the page is what we expect it to be. This is what the assertContains() method is used for. This method takes two required arguments: a response object and a string that you expect to occur in the document body of the response. If the test method finds the string in the content of the response, the test succeeds. We can put this to good use by testing that the category page contains the category name and the category description:

def test_view_category(self):
        category = Category.active.all()[0]
        category_url = category.get_absolute_url()
        # get the template_name arg from URL entry
        url_entry = urlresolvers.resolve(category_url)
        template_name = url_entry[2]['template_name']
        # test loading of category page
        response = self.client.get(category_url)
        # test that we got a response
        self.failUnless(response)
        # test that the HTTP status code was "OK"
        self.assertEqual(response.status_code, httplib.OK)
        # test that we used the category.html template in response
        self.assertTemplateUsed(response, template_name)
       # test that category page contains category information
       self.assertContains(response, category.name)
       self.assertContains(response, category.description)

Now that we've got all the basic methods used in test methods covered, let's use all of them
to write a brief test method to ensure that our product view is working correctly. Put the
following method inside your test class:

    def test_view_product(self):
        """ test product view loads """
        product = Product.active.all()[0]
        product_url = product.get_absolute_url()
        url_entry = urlresolvers.resolve(product_url)
        template_name = url_entry[2]['template_name']
        response = self.client.get(product_url)
self.failUnless(response)
  self.assertEqual(response.status_code, httplib.OK)
  self.assertTemplateUsed(response, template_name)
  self.assertContains(response, product.name)
  self.assertContains(response, product.description)

Don't forget to add the Product model to the import statement or else this won't work!

Note

Your product names or descriptions might contain characters that will be escaped by Django when they're rendered in your templates. We'll see how to get around this in your tests when we write the code for the search later in this chapter.

One other issue that you'll encounter is an error with the logging of the product page view. The ProductView model we created in Chapter 9 for tracking the pages that customers are viewing requires a valid IP Address... something that the Django test runner does not simulate on the Client instance. So, in order to make our tests succeed, we need to add a little bit of additional logic to the log_product_view() function in our stats.py file:

v.ip_address = request.META.get('REMOTE_ADDR')
if not request.META.get('REMOTE_ADDR'):
    v.ip_address = '127.0.0.1'
 v.user = None

Test classes can also contain other methods that exist only to act as helper methods for your test methods. Take a look at the lines of code that are getting the name of the template file given the URL; the code to do this might be repeated across several of your test methods. You can create such a method by simply defining a method that doesn't start with test_, and called from your test methods using the self keyword:

def get_template_name_for_url(self, url):
    """ get template_name kwarg for URL """
    url_entry = urlresolvers.resolve(url)
    return url_entry[2]['template_name']

Helper methods on test classes can also contain assertions, and they will cause the test to fail if the assertion fails. However, they won't be called by the test runner by default. You must call them explicitly from within either setUp() or one of your test methods for them to run. For this reason, it's generally best practice to leave your assertions in your test methods and only use helper methods to reduce repetition of common tasks.

The response object also comes equipped with a context property. This is useful for testing the existence of variables that you expect to appear in the returned response. For example, our product view function should be returning a variable called form with each product page response. We can test that this variable is included in the response by asserting that it exists in the response context, and checking that it is an instance of the correct form class:

from ecomstore.catalog.forms import ProductAddToCartForm

# check for cart form in product page response
cart_form = response.context[0]['form']
self.failUnless(cart_form)
# check that the cart form is instance of correct form class
self.failUnless(isinstance(cart_form, ProductAddToCartForm))

There are other variables returned in the product page view that we could also test for in the response context. For example, each product page is supposed to return a variable called product_reviews. Testing for this variable is a little trickier. For instance, the following code will not work:

product_reviews = response.context[0]['product_reviews']
self.failUnless(product_reviews)

This will work if we are certain that the product we've selected for our test has at least one product review. However, if the product has no reviews, then the variable product_reviews will be an empty list, which will cause the failUnless() method to fail in the preceding case. On top of this, if the variable product_reviews is missing from the dictionary, then the first line of the preceding code will raise a KeyError exception.

A much better approach for testing the existence of variables in response contexts is to check that they exist by using the get() function available on Python dictionaries, and returning the Python null value None if the object doesn't exist. Then, check that the return value is not None:

product_reviews = response.context[0].get('product_reviews',None)
self.failIfEqual(product_reviews, None)

Testing the ActiveProductManager

One of the assumptions we've made about our code is that inactive categories and products will not be returned to the user interface. We can test this simply by trying to load an inactive product and checking that Django returns a 404 Page Not Found error.

Add the following class to the test file and re-run your tests. Remember to make sure that at least one product in the initial_data.json fixtures file you created earlier is set to inactive:

class ActiveProductManagerTestCase(TestCase):
    def setUp(self):
        self.client = Client()

    def test_inactive_product_returns_404(self):
        """ test that inactive product returns a 404 error """
        inactive_product = Product.objects.filter(is_active=False)[0]
        inactive_product_url = inactive_product.get_absolute_url()
        # load the template file used to render the product page
        url_entry = urlresolvers.resolve(inactive_product_url)
        template_name = url_entry[2]['template_name']
        # load the name of the default django 404 template file
        django_404_template = page_not_found.func_defaults[0]
        response = self.client.get(inactive_product_url)
self.assertTemplateUsed(response, django_404_template)
self.assertTemplateNotUsed(response, template_name)

One thing to notice here is that we cannot test the HTTP status code of the 404 page, because when Django serves up the custom Page Not Found page we created, it actually uses an HTTP status code of 200 and not 404. So, in order to test that the inactive product is not being loaded, we're using the template assertion methods to check the behavior.

Product Catalog Model Tests

Now that we've tested some of the basic parts of the interface, let's jump back down to the model level and write some tests to check the configuration of our model classes. TestCase classes are pretty simple to create for your models, so your test code should have a TestCase class for each model you've defined in your projects.

Writing unit tests to check the models in a web project often entails writing tests to check the create, update, and delete operations of any given model. These can be very simple to define. For example, when writing a test class to test the Product model, you could just check the total count of products in the database before and after a create and delete operation, making sure that the count incremented or decremented by one in each case, respectively. Or you could run an update on a model instance by calling the save() method, and then check to make sure that those changes were actually stored.

Writing these simple tests to check the CRUD operations of a model class is very simple. You just write test methods that exercise the Django ORM techniques you've been using through the rest of the book. In my opinion, testing for these kinds of things is testing to ensure that Django is doing what it's supposed to do and not testing the specific code that you've written for your project. As such, I won't be including any of them in this book.

There are more interesting things that we can test for that check the code we have written. For example, on the Product model, we have defined a model method called sale_price(), which will return the price if an old price is specified on the model and is greater than the price field. This is exactly the kind of logic we should be testing when we write our test classes.

Inside your catalog/tests.py file, add the following test class for the Product model:

from decimal import Decimal

class ProductTestCase(TestCase):
    def setUp(self):
        self.product = Product.active.all()[0]
        self.product.price = Decimal('199.99')
        self.product.save()
        self.client = Client()

    def test_sale_price(self):
        self.product.old_price = Decimal('220.00')
        self.product.save()
        self.failIfEqual(self.product.sale_price, None)
        self.assertEqual(self.product.sale_price, self.product.price)

    def test_no_sale_price(self):
        self.product.old_price = Decimal('0.00')
        self.product.save()
        self.failUnlessEqual(self.product.sale_price, None)

This defines a setup() method that retrieves an active product for us to use in all of our test methods, and sets a default price. In this case, it really doesn't matter what the actual price of the product was; we can just set it to some value that helps us write our tests. The test methods check that the sale_price() method returns a value of None when the old price is less than the price field, and that the sale price returned matches the price field when there is an old price greater than price.

There are other methods on the Product model class that we can check. There is also a __unicode__() method that should return the name of the product, as well as the get_absolute_url() method, which, for active products, should return a valid product page. Add the following two test methods to the ProductTestCase class:

def test_permalink(self):
     url = self.product.get_absolute_url()
     response = self.client.get(url)
     self.failUnless(response)
     self.assertEqual(response.status_code, httplib.OK)

def test_unicode(self):
   self.assertEqual(self.product.__unicode__(), self.product.name)

The latter two cases are fairly self-explanatory. Writing a test class for the Category model class is similar, and quite a bit simpler:

class CategoryTestCase(TestCase):
     def setUp(self):
         self.category = Category.active.all()[0]
         self.client = Client()

     def test_permalink(self):
         url = self.category.get_absolute_url()
         response = self.client.get(url)
         self.failUnless(response)
         self.failUnlessEqual(response.status_code, httplib.OK)

     def test_unicode(self):
         self.assertEqual(self.category.__unicode__(), self.category.name)

One other thing we can check for at the model level is any validation we expect to be present. Django doesn't provide validation at the model level at the time of this writing, but there are still a couple of very basic things we can test for at the model level in our application. For example, looking at the product model, we know that we cannot save a ProductReview instance without a valid product or user set in the foreign key field. An attempt to save an orphaned product review without a corresponding product will raise an IntegrityError. To test for this, we can assert that the error is raised after any attempt to call the save() method an invalid instance:

from ecomstore.catalog.models import ProductReview, Product
from django.db import IntegrityError

class ProductReviewTestCase(TestCase):
    def test_orphaned_product_review(self):
        pr = ProductReview()
        self.assertRaises(IntegrityError, pr.save)

There are a couple of fields that have default values set in the ProductReview model definition: is_approved and rating. As long as we provide a product review with both a product and a user, the save should occur successfully, and those two fields should contain the default values defined in the fields on the model. To make sure that this is true, we can create a new product review, iterate through all the fields on the model, and check that fields with a default value fall back on the provided default:

from django.contrib.auth.models import User

class ProductReviewTestCase(TestCase):
    # other code here

    def test_product_review_defaults(self):
        user = User.objects.all()[0]
        product = Product.active.all()[0]
        pr = ProductReview(user=user, product=product)
        pr.save()
        for field in pr._meta.fields:
            if field.has_default():
              self.assertEqual(pr.__dict__[field.name], field.default)

Note that in order for this to run without errors, you need to make sure that you create some fixture data for the users of your site. We can dump the data from the Django auth app into one of our own apps fixtures subdirectory so that at least one User instance will be present in the database for the test runner. Even though we're testing the catalog app only at the moment, the fixture data from all apps is loaded before the test runner starts running the tests. So, to dump the auth data and user logins into the accounts directory in our project, use the following commands:

$ mkdir accounts/fixtures
$ python manage.py dumpdata auth --indent=2 > accounts/fixtures/initial_data.json

You should create a TestCase subclass for each of your models and test the logic of it that is specific to your application. What you want to focus on are the parts of your models that get exposed via the user interface. Once you have a Django ModelForm that's created from the model class, then you want to start testing that the form instance enforces all of those rules. If there are relations between your models, or if any fields are required, test for IntegrityError exceptions when calling the save() on an instance.

Testing Forms & Shopping Cart

Now that we know a user can make it to the product pages without technical difficulties, let's test that the customer can add a product to their shopping cart. There is actually a lot going on in this simple operation. The user is submitting a form containing very basic data with a POST request to the product page that we need to validate. If the data is valid, then we need to create the new cart item and redirect the user to the cart page.

Let's have a look at emulating a successful add-to-cart operation, as well as testing the before and after expectations we have about the process. Inside your cart app, add the following test code to tests.py:

from ecomstore.catalog.models import Product
from ecomstore.cart.models import CartItem
from ecomstore.cart import cart
from django.test import TestCase, Client
from django.core import urlresolvers
from django.db import IntegrityError
from django.contrib import csrf
from django.conf import settings

import httplib

class CartTestCase(TestCase):
    def setUp(self):
        self.client = Client()
        self.product = Product.active.all()[0]

    def test_cart_id(self):
        home_url = urlresolvers.reverse('catalog_home')
        self.client.get(home_url)
        # check that there is a cart_id set in session
        # after a page with cart box has been requested
        self.failUnless(self.client.session.get(cart.CART_ID_SESSION_KEY,''))

    def test_add_product(self):
        QUANTITY = 2
        product_url = self.product.get_absolute_url()
        response = self.client.get(product_url)
        self.assertEqual(response.status_code, httplib.OK )

        # store count in cart_count variable
        cart_item_count = self.get_cart_item_count()
        # assert that the cart item count is zero
        self.failUnlessEqual(cart_item_count, 0)

        # perform the post of adding to the cart
        cookie = self.client.cookies[settings.SESSION_COOKIE_NAME]
        csrf_token = csrf.middleware._make_token(cookie.value)
        postdata = {'product_slug': self.product.slug,
                    'quantity': QUANTITY,
                     'csrfmiddlewaretoken': csrf_token }
        response = self.client.post(product_url, postdata )

        # assert redirected to cart page - 302 then 200
        cart_url = urlresolvers.reverse('show_cart')
        self.assertRedirects(response, cart_url, status_code=httplib.FOUND,
target_status_code=httplib.OK)

        # assert cart item count is incremented by one
        self.assertEqual(self.get_cart_item_count(), cart_item_count + 1)
cart_id = self.get_cart_id()
       last_item = CartItem.objects.filter(cart_id=cart_id).latest('date_added')
       # assert the latest cart item has a quantity of two
       self.failUnlessEqual(last_item.quantity, QUANTITY)
       # assert the latest cart item is the correct product
       self.failUnlessEqual(last_item.product, self.product)

def get_cart_item_count(self):
       cart_id = self.get_cart_id()
       return CartItem.objects.filter(cart_id=cart_id).count()

def get_cart_id(self):
     return self.client.session.get(cart.CART_ID_SESSION_KEY)

Have a look at the test_add_product() method. You'll notice that before we actually make a POST to the product page that we expect to be successful, we have to add the hidden input to make sure that form is valid. In Chapter 12, we added a middleware class to our project to help reduce the likelihood of Cross-Site Request Forgery attacks. This ensures that every POST request we make with a form on our site is checked to make sure that the request is valid by checking that it has a valid hash value in a hidden form input named csrfmiddlewaretoken. Because we're generated the POST data by hand, we need to generate this value and add it to the dictionary by hand.

This process is simple enough. We first retrieve the cookie from the test client's session using the value from the settings module. Notice that, in this case, we're importing settings from django.conf and not from our own project root directory. Then, we call the _make_token() function from the CSRF middleware class, passing in the cookie value. We just add the return value from this call to the data of the POST request. If you omit this value from the form, your HTTP status code will not be the 200 OK that you're expecting, but will instead be a 403 Forbidden error.

Now that we've covered successful additions to the cart, let's look at the list of things that can go wrong when a user submits a form. The quantity might be empty, or it may contain a value that isn't a valid integer. We can easily add some test methods to our class to check these errors:

from ecomstore.catalog.forms import ProductAddToCartForm

class CartTestCase(TestCase):
    # other test methods here
    def test_add_product_empty_quantity(self):
        product_url = self.product.get_absolute_url()
        postdata = {'product_slug': self.product.slug, 'quantity': '' }
        response = self.client.post(product_url, postdata )
        expected_error = unicode(ProductAddToCartForm.
            base_fields['quantity'].error_messages['required'])
        self.assertFormError(response, "form", "quantity", [expected_error])

    def test_add_product_zero_quantity(self):
        product_url = self.product.get_absolute_url()
        postdata = {'product_slug': self.product.slug, 'quantity': 0 }
        response = self.client.post(product_url, postdata )

        # need to concatenate the min_value onto error_text containing %s
error_text = unicode(ProductAddToCartForm.
         base_fields['quantity'].error_messages['min_value'])
     min_value = ProductAddToCartForm.base_fields['quantity'].min_value
     expected_error = error_text % min_value

     self.assertFormError(response, "form", "quantity", [expected_error])

def test_add_product_invalid_quantity(self):
    product_url = self.product.get_absolute_url()
    postdata = {'product_slug': self.product.slug, 'quantity': 'bg' }
    response = self.client.post(product_url, postdata )
    expected_error = unicode(ProductAddToCartForm.
        base_fields['quantity'].error_messages['invalid'])
   self.assertFormError(response, "form", "quantity", [expected_error])

assertFormError() takes the response object from the POST, the name of the form as it appears in our view function, the name of the field we expect to have the error, and finally, a Python list of expected error messages. It's important to notice that, in the interest of not repeating ourselves, we're actually going to the name of the form class and pulling out the error that we expect the form to raise when we submit it. Then, we just check for the presence of that particular error in the response on that form and field.

Testing the Checkout Form

Next, we're going to write some tests for a very important aspect of our site: the checkout process. I'm not going to cover checkout in great detail, but we're going to take a quick look at how you can check for errors on form fields without listing each and every field by name. While it's all well and good to check for errors on specific fields on a small form like Add To Cart that contains only a single quantity field, we'd really like to be able to check that all of the fields are being validated without having to hard-code a check for each one. This is easy enough to do.

Before we start writing tests for the checkout, there's one last important point to keep in mind: the user needs to have at least one item in their shopping cart to successfully access the checkout page. Otherwise, the site will redirect them back the cart page. So, as a part of our basic setUp() method, we need to create a Client instance that has an item in the cart.

In your checkout directory, add the following test class to tests.py:

from django.test import TestCase, Client
from django.core import urlresolvers

from ecomstore.checkout.forms import CheckoutForm
from ecomstore.checkout.models import Order, OrderItem
from ecomstore.catalog.models import Category, Product
from ecomstore.cart import cart
from ecomstore.cart.models import CartItem

import httplib

class CheckoutTestCase(TestCase):
    def setUp(self):
self.client = Client()
         home_url = urlresolvers.reverse('catalog_home')
         self.checkout_url = urlresolvers.reverse('checkout')
         self.client.get(home_url)
         # need to create customer with a shopping cart first
         self.item = CartItem()
         product = Product.active.all()[0]
         self.item.product = product
         self.item.cart_id = self.client.session[cart.CART_ID_SESSION_KEY]
         self.item.quantity = 1
         self.item.save()

     def test_checkout_page_empty_cart(self):
         """ empty cart should be redirected to cart page """
         client = Client()
         cart_url = urlresolvers.reverse('show_cart')
         response = client.get(self.checkout_url)
         self.assertRedirects(response, cart_url)

     def test_submit_empty_form(self):
         """ empty form should raise error on required fields """
         form = CheckoutForm()
         response = self.client.post(self.checkout_url, form.initial)
         for name, field in form.fields.iteritems():
             value = form.fields[name]
             if not value and form.fields[name].required:
                 error_msg = form.fields[name].error_messages['required']
               self.assertFormError(response, "form", name, [error_msg])

Take a look at the last test method: one of the great things about Django forms is that you have access to all of the fields on an instance of that form. First, we post a completely empty instance of the form to the checkout page. Then, we're iterating through all of the fields in the checkout form and, if they don't have a value but are required by the form definition, assert that the "required" error message was raised by posting the form.

Security Testing

Security is a very important aspect of any web application and, like any other important part of our site, we should test it. There are a couple of very simple tests we can add to our test classes to make sure that our site is performing basic security measures.

First of all, any time that user input is displayed back on a page, we expect that Django will escape any HTML found in the template variable tags to eliminate any possibility of Cross-Site Scripting attacks. We can test for this by adding a simple test class that performs a basic search and ensures that the search text appears, HTML-encoded, on the results page. Inside your search app, add the following code to the tests.py file:

from django.test import TestCase, Client
from django.core import urlresolvers
from django.utils import html

import httplib

class SearchTestCase(TestCase):
    def setUp(self):
        self.client = Client()
        home_url = urlresolvers.reverse('catalog_home')
        response = self.client.get(home_url)
        self.failUnless(response.status_code, httplib.OK)

    def test_html_escaped(self):
        search_term = '<script>alert(xss)</script>'
        search_url = urlresolvers.reverse('search_results')
        search_request = search_url + '?q=' + search_term
        response = self.client.get(search_request)
        self.failUnlessEqual(response.status_code, httplib.OK)
        escaped_term = html.escape(search_term)
       self.assertContains(response, escaped_term)

Here, we perform a search for a search term that contains the dreaded <script></script> tags. In our test method, we encode the search text using the escape() function in the django.utils.html module and then check that the results template contains the escaped text, instead of rendering the potentially harmful tags.

We can also check that a POST request with form data that isn't signed by our CSRF middleware fails. Earlier in this chapter, when we wrote test methods for adding products to the shopping cart, we needed to add a special entry to the POST data we were submitting in order to pass the validation check by the CSRF middleware. For security purposes, we can test that a POST request to add an item to the shopping cart fails the security check if this special input is missing.

Inside your CartTestCase class, add the following method to check that a POST missing the validation input field fails, and returns an HTTP 403 Forbidden error status code:

def CartTestCase(TestCase):
    # other test methods here

    def test_add_to_cart_fails_csrf(self):
        quantity = 2
        product_url = self.product.get_absolute_url()
        response = self.client.get(product_url)
        self.assertEqual(response.status_code, httplib.OK )

        # perform the post of adding to the cart
        postdata = {'product_slug': self.product.slug,
                        'quantity': quantity }
        response = self.client.post(product_url, postdata )

        # assert forbidden error due to missing CSRF input
       self.assertEqual(response.status_code, httplib.FORBIDDEN )

Lastly, we can also test to make sure that the encryption and decryption methods we created for our credit card data in Chapter 12 are working as we'd expect them to. Inside billing/tests.py, add the following single test class:

from django.test import TestCase, Client
from ecomstore.billing.passkey import encrypt, decrypt

class EncryptionTestCase(TestCase):
    def test_encrypt_decrypt(self):
        to_encrypt = 'Some text here'
        self.failUnlessEqual(to_encrypt, decrypt(encrypt(to_encrypt)))
       self.failIfEqual(to_encrypt, encrypt(to_encrypt))

This test class doesn't actually do anything to test how secure the actual cipher we're using is. It merely tests that the encryption and decryption functions are working the way we expect them to, and that the ciphertext is not equal to the plaintext.

While there are certainly other security tests you could perform on your site, and there are definitely other places you could apply these two test cases, for brevity's sake, these are all of the security tests that I'm going to include in this book. However, security testing is a very important (and oft-overlooked) aspect of writing tests for web applications.

Summary

It's extremely important to keep in mind that just because you've written tests for your application, and just because you can run all of them without a single failure, that doesn't mean that your application is perfect. If you've forgotten to test some part of your application, then you won't detect any failures if that area of your web site breaks for some reason. And, of course, these kinds of tests only check for errors in the way that your application is supposed to function. If the interface of your web site is a terrible garble of HTML that no customer could possibly stand to look at, the tests you've written in this chapter certainly won't catch that problem. (That's what usability testing is for.)

As you start writing tests for your Django project, and having nightmares every night wondering whether or not you've got sufficient code coverage in your tests to check for every possible failure in your application (I'm only kidding about that... sort of), you'll find yourself starting to look much deeper into what Django has to offer. If you take testing into consideration from the outset, I strongly believe that you'll end up with a better application that will cause you much less grief maintenance.

Personally, I've found writing tests to be a very useful form of code review for myself. We often make silly little mistakes in the logic of our application, and having to write tests forces me to re-examine the thought processes and assumptions behind my initial work. They're also immensely helpful when you're creating parts of your application that can't easily be tested by manual means.

Now that the tests have been written and they run without fail, we're ready to show off the work we've done throughout the rest of this book to the world. Let's get this bad boy deployed onto the web so we can start selling products.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.3.122