Testing Django application for lazy programmer

Testing Django application for lazy programmer

I am lazy, but accurate, perfectionist Django developer. I like things being in order, but with minimum of my personal effort. I want my daily software development to be a pleasant job, not overhelmed with boring tasks. Thats why I keep on improving methods of my work (learn emacs, technologies etc) with some hope to learm some magic methods which will do some [at least] work for me. Here is one of excerts of how I test my final applications.

Because I am lazy, I prefer to avoid some boring tasks. Like using django TestCase as a base class for my tests makes it boring to fill up all necessary data for test database, save it to Fixtures (which btw have big disadvantage of maintanance need in case of db schema change) and then doing some logical tests on this artifical data. I feel this work to be unnecessary and time consuming for most real projects I work in. Why not to test things against the real data in a database? For the most of cases, we deal with a real use cases, and most of data pieces almost never change. An artifical test instead is useful to test the product which you are going to sell, but for a final end-user project it seems to be much more useful to test the final result.

But with a standard unittest approach assummes that you will also need to use 'low level' functions   instead of getting some code sugar from django TestCase. After some considerations and playing around with code we've come (together with Avanguard) with solution which fixes that. And so we get all the functions from Django TestCase and stilll not dropping database. All this is possible with our utility function class DjHDGutils.testutils.TestCase which overrides some destructuve functions of main TestCase and provides some easy-to-use syntax sugar like flow integration with django testClient and parsing of output with lxml, BeautifulSoup (html5parser on the way to add here).

Code by example

I will use yesterdays site testing example to show it step by step. Here is test plan: we have small e-commerce site to be tested. It is minimalistics, and do have the following parts to be tested:

  • Front page with some advertisements
  • Category page with products list
  • Product page with product description, image, etc
  • Search (+auto complete)
  • Ordering process
    • Order to Georgia: apply Georgia tax
    • Order to any other US state and worldwide: no tax applied
    • Order is shipped via UPS, USPS with online caclulator of shipment price
  • Flatpages: "F.A.Q", "Shipping", etc

Testing homepage

Even on the simplest example, we get rid of manual call to TestClient and skip over assigning to response variable and checking the result. And so here is example with standard Django test:

from django.test.client import Client
from django.test import TestCase

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

    def test_homepage(self):
        response = self.client.get('/customer/details/')
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Call 1-877-475-4656")

And here is my code, improved for more lazy developer:

from DjHDGutils.testutils import TestCase


class GeneralNavigationTest(TestCase):
    def test_homepage(self):
        self.get('/')
        self.assertContains("Call 1-877-475-4656")

if __name__ == '__main__':
    unittest.main()

Test category page

With this test we need to check that it does not produce error 500, renders category title and text. Here is code snippet:

def test_public_category(self):
    category = Category.objects.filter(published=True)[0]
    self.get(reverse('category', args=[category.slug]))
    self.assertContains(category.name)
    self.assertContains(category.description)

So, what is being done: check against database / ORM values with those values which we read over http call. Ommiting response/passing response makes code more clean.

Test other pages

I had no difficulties of testing them all: django testing is very easy thing to do because:

  • ORM is avaliable to test some values
  • no need to start http server to start testing application

The only small difficulty I had with Captcha on "Contact Us" form where requirement was to read the data from page to find the captha. Here package 'lxml.html' has been used to help with this task:

def test_contact_form(self):
    self.get('/form/contact-us/')
    doc = self.lxml_doc
    captcha_0 = doc.xpath('//*[@id="id_captcha_0"]')[0].get('value')

… and the task of testing this page was resolved.

The final code

The final code looks much more clean and not cluttered with non-useful data. Here is example of my yesterday work on testing of one of our customer sites:

 

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import re
import unittest

from captcha.models import CaptchaStore
from django.contrib.webdesign import lorem_ipsum
from django.core.urlresolvers import reverse

from DjHDGutils.testutils import TestCase
from djw.core.Cart.models import Cart
from djw.core.Order.models import Order
from djw.core.ProductCatalog.models import Category, Product
from djw.core.Shipment.models import ShipmentMethod


class GeneralNavigationTest(TestCase):
    def test_homepage(self):
        self.get('/')
        self.assertContains("Call 1-877-475-4656")

    def test_public_category(self):
        category = Category.objects.filter(published=True)[0]
        self.get(reverse('category', args=[category.slug]))
        self.assertContains(category.name)
        self.assertContains(category.description)

    def test_hidden_category(self):
        hidden_categories = Category.objects.filter(published=False)
        if hidden_categories.count() > 0:
            category = hidden_categories[0]
            self.get(reverse('category', args=[category.slug]))
            self.assertCode(404)

    def test_product(self):
        product = Product.objects.filter(published=True)[0]
        self.get(product.get_absolute_url())
        self.assertContains(product.name)
        self.assertCode(200)

    def test_hidden_product(self):
        hidden_products = Product.objects.filter(published=False)
        if hidden_products.count() > 0:
            product = hidden_products[0]
            url = product.get_absolute_url()
            self.get(url)
            self.assertContains('Sorry, this item currently out of stock.')

    def test_search_autocomplete(self):
        self.get(reverse('search-autocomplete') + '?' + 'query=wheel')
        self.assertContains('Floor & Table Black Clicker Prize Wheel')

    def test_search(self):
        self.get(reverse('search') + '?q=deluxe six foot')
        self.assertContains('Giant Christmas Stocking')

        self.get(reverse('search') + '?q=Simply attach the padlock')
        self.assertContains('Treasure Chest Kit (includes lock and keys)')

    def add_product_to_cart(self, product_slug):
        product = Product.objects.get(slug=product_slug)
        self.post_url(reverse('cart-add', args=[
            product.productcategory_set.all()[0].category.slug,
            product.slug]),
                      {'quantity': '1'}, follow=True)
        self.assertContains('Shopping cart:')
        return re.search(
            re.compile(
                'Shopping\scart:\s(\w\w\-\d{4}\-\d{4}-\d{4})'),
            self.text).group(1)

    def ship_to(self, cart_key, address):
        self.get(reverse('shipment-enter-address', args=[cart_key]))
        self.assertContains('Delivery Address')
        data = {}
        for var in ('name',
                    'company_name',
                    'email',
                    'street1',
                    'street2',
                    'city',
                    'region',
                    'post_code',
                    'tel',
                    'country_id'):
            data[var] = address.get(var, lorem_ipsum.words(1))
        self.post(data=data, follow=True)
        self.assertContains('Choose Delivery and Payment Method')
        self._test_shipping_output()

    def check_by_mail(self, ship_via):
        self.post(data={
            'button': 'continue',
            'pay_via': 'check-by-mail.check-by-mail',
            'selected_shipment_method': ship_via},
                  follow=True)
        self.assertContains('Thank You!')

    def test_order1(self):
        """ Order with delivery outside Georgia """
        cart_key = self.add_product_to_cart('plinko-game-with-three-pucks')
        self.ship_to(cart_key, {'country_id': '1',
                                'region': 'FL',
                                'city': 'Miami',
                                'email': 'avk@tst.koval.kharkov.ua',
                                'post_code': '33196'})
        cart = Cart.objects.get(key=cart_key)
        self.check_by_mail(cart.shipment_submethod)
        order = Order.objects.get(key=cart_key)
        self.get(reverse('invoice', args=[cart_key]))
        self.assertContains('Invoice ' + cart_key)
        self.assertContains(
            'Total for this Order: ${0:.2f}'.format(
                order.total))
        self.assertContains('Tax: $0.00')

    def test_order2(self):
        """ test order inside of Georgia - should apply georgia tax """
        cart_key = self.add_product_to_cart('plinko-game-with-three-pucks')
        self.ship_to(cart_key, {'country_id': '1',
                                'region': 'GA',
                                'city': 'Macon',
                                'street1': '777 Hemlock Street',
                                'email': 'avk@tst.koval.kharkov.ua',
                                'post_code': '31201'})
        cart = Cart.objects.get(key=cart_key)
        self.check_by_mail(cart.shipment_submethod)
        order = Order.objects.get(key=cart_key)
        self.get(reverse('invoice', args=[cart_key]))
        self.assertContains('Invoice ' + cart_key)
        self.assertContains(
            'Total for this Order: ${0:.2f}'.format(
                order.total))
        self.assertContains('Tax: $17.50')

    def _test_shipping_output(self):
        """ call this function within test orders, check if shipping is enabled
        and produces some outout for goods in cart """
        usps = ShipmentMethod.objects.filter(module='djw.modules.USPS.views',
                                             isEnabled=True)
        if usps.count() > 0:
            self.assertContains('USPS Parcel Post')

        ups = ShipmentMethod.objects.filter(module='djw.modules.USP.views',
                                            isEnabled=True)
        if ups.count() > 0:
            self.assertContains('UPS')

    def test_flatpages(self):
        self.get('/info/catalog/')
        self.assertContains('PrizeWheel.com 2011 Catalog')

        self.get('/info/FAQ/')
        self.assertContains('How do I customize the Prize Wheel')

        self.get('/info/Privacy/')
        self.assertContains('Our Commitment To Privacy')

        self.get('/info/Shipping/')
        self.assertContains('Standard production time on all products')

    def test_contact_form(self):
        self.get('/form/contact-us/')
        doc = self.lxml_doc
        captcha_0 = doc.xpath('//*[@id="id_captcha_0"]')[0].get('value')
        captcha = CaptchaStore.objects.get(hashkey=captcha_0)
        self.post({'name': 'test',
                   'phone': 'test',
                   'email_address': 'avk@tst1.koval.kharkov.ua',
                   'captcha_0': captcha_0,
                   'captcha_1': captcha.challenge,
                   'questions_and_comments': lorem_ipsum.paragraph()})
        self.assertContains('Your comments have been sent')

    def test_error(self):
        try:
            self.get(reverse('error500'), code=500)
            self.fail('Must fail here')
        except ValueError:
            pass

        self.get(reverse('error404'))
        self.assertCode(404)

    def test_blog(self):
        """ test external fetch of blog """
        self.get(reverse('rss'))
        self.assertContains('More posts')
        self.assertContains('div class="rss_block"')

if __name__ == '__main__':
    unittest.main()

Comments

Comments powered by Disqus