Introduction to Testing in Django

Stuart Dines

Team Leader / Senior Software Engineer @ The Interaction Consortium 
http://interaction.net.au/

@sjdines

 http://stuartdines.com

Where to start?

  • The best way to start learning how to do anything in DJango - the docs!
    • https://docs.djangoproject.com/en/1.8/topics/testing/overview/

Types of Tests

  • Unit testing
    • The smallest testable parts of an application, called units, are individually and independently scrutinized for proper operation. Unit testing is often automated but it can also be done manually.
    • Each individual method / function
  • Integration testing 
    • Program units are combined and tested as groups in multiple ways. In this context, a unit is defined as the smallest testable part of an application.
    • Combinations of applications in a project and overall functionality

Why bother writing tests?

  • Tests help prevent problems by providing a base line of anticipated functionality
  • When changes occur tests can be run to ensure that you don't break other sections of your project / application with out your knowledge
  • This will save you time in the long run and the sanity of other people working on the project / application

Testing Strategies

  • "Test Driven Development" which entails writing the tests before you write the code i.e. having your acceptance criteria created and writing the code to make it pass
  • Writing tests after the code has been created
  • More often than not I work in between the two; creating some initial tests or code I expect and iterating between the two
  • This allows the targeting of code paths

Writing Tests

  • Django’s unit tests use a Python standard library module: unittest. This module defines tests using a class-based approach.

    Often used is `django.test.TestCase`, which is a subclass of `unittest.TestCase` that runs each test inside a transaction to provide isolation

from django.test import TestCase
from myapp.models import Animal

class AnimalTestCase(TestCase):
    def setUp(self):
        self.animal_1 = Animal.objects.create(name="lion", sound="roar")
        self.animal_2 = Animal.objects.create(name="cat", sound="meow")

    def test_animals_can_speak(self):
        """Animals that can speak are correctly identified"""
        lion = Animal.objects.get(name="lion")
        cat = Animal.objects.get(name="cat")
        self.assertEqual(lion.speak(), 'The lion says "roar"')
        self.assertEqual(cat.speak(), 'The cat says "meow"')

    def tearDown(self):
        self.animal_1.delete()
        self.animal_2.delete()

Basic Test

Anatomy of a test

  • The initial class declaration will all you to categorise your tests into logical sections of functionality and should subclass a testing class
    • `class AnimalTestCase(TestCase)`

Anatomy of a test

  • Each class allows a `setUp` and a `tearDown` method
    • A `setUp` method will run some code at the beginning of each test case which is useful for repetitive tasks required for each test
    • A `tearDown` method runs at the end of each test case which is useful for cleaning up

Anatomy of a test

  • Each individual test must start with `test` and becomes a
    method on the class
    • ` def test_animals_can_speak(self):`
  • Tests cases allow you test specific functionality
  • Each test should assert something you anticipate to occur
    • `self.assertEqual(lion.speak(), 'The lion says "roar"')`
    • There are many different assertion types which can be seen at https://docs.djangoproject.com/en/dev/topics/testing/tools/#assertions

Running Tests

  • Once you have written tests, run them using the test command of your project’s manage.py utility
./manage.py test

Running Tests

  • Do I have to run all the tests each time? No.
# Run all the tests in the animals.tests module
$ ./manage.py test animals.tests

# Run all the tests found within the 'animals' package
$ ./manage.py test animals

# Run just one test case
$ ./manage.py test animals.tests.AnimalTestCase

# Run just one test method
$ ./manage.py test animals.tests.AnimalTestCase.test_animals_can_speak

What happens to my database?!

  • Tests that require a database (namely, model tests) will not use your “real” (production) database. Separate, blank databases are created for the tests.

    Regardless of whether the tests pass or fail, the test databases are destroyed when all the tests have been executed.

  • By default the test databases get their names by prepending `test_` to the value of the NAME settings for the databases defined in DATABASES.

In which order to the test execute?

  • In order to guarantee that all TestCase code starts with a clean database, the Django test runner reorders tests in the following way:

    • All TestCase subclasses are run first.

    • Then, all other Django-based tests (test cases based on SimpleTestCase, including TransactionTestCase) are run with no particular ordering guaranteed nor enforced among them.

    • Then any other unittest.TestCase tests (including doctests) that may alter the database without restoring it to its original state are run.

Understanding the test output

  • When you run your tests, you’ll see a number of messages as the test runner prepares itself
  • You can control the level of detail of these messages with the verbosity option on the command line
Creating test database...
Creating table myapp_animal
Creating table myapp_mineral
Loading 'initial_data' fixtures...
No fixtures found.

Understanding the test output

  • For each test that is run a `.` will appear on your screen
  • At the end of each of the tests you should see a summary of the number of tests run
----------------------------------------------------------------------
Ran 22 tests in 0.221s

OK

Understanding the test output

  • If something goes wrong you will be alerted with which test failed and which assertion was not correct for each test failure
  • If there is an error such as invalid code, missing imports, etc it will show you a list of errors
======================================================================
FAIL: test_was_published_recently_with_future_poll (polls.tests.PollMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/dev/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_poll
    self.assertEqual(future_poll.was_published_recently(), False)
AssertionError: True != False

----------------------------------------------------------------------
Ran 1 test in 0.003s

FAILED (failures=1)

What does it mean when a test fails?

  • When a test fails it means there is an incongruity between the expectations of the code and the actual code
  • Judgement needs to be used to work out which is correct! Sometimes it will be the test other times the code especially with functionality changes

Test Example

https://github.com/stephenmcd/mezzanine/blob/master/mezzanine/pages/tests.py

class PagesTests(TestCase):

    def test_page_ascendants(self):
        """
        Test the methods for looking up ascendants efficiently
        behave as expected.
        """
        # Create related pages.
        primary, created = RichTextPage.objects.get_or_create(title="Primary")
        secondary, created = primary.children.get_or_create(title="Secondary")
        tertiary, created = secondary.children.get_or_create(title="Tertiary")
        # Force a site ID to avoid the site query when measuring queries.
        setattr(current_request(), "site_id", settings.SITE_ID)

        # Test that get_ascendants() returns the right thing.
        page = Page.objects.get(id=tertiary.id)
        ascendants = page.get_ascendants()
        self.assertEqual(ascendants[0].id, secondary.id)
        self.assertEqual(ascendants[1].id, primary.id)

        # Test ascendants are returned in order for slug, using
        # a single DB query.
        self.reset_queries(connection)
        pages_for_slug = Page.objects.with_ascendants_for_slug(tertiary.slug)
        self.assertEqual(len(connection.queries), 1)
        self.assertEqual(pages_for_slug[0].id, tertiary.id)
        self.assertEqual(pages_for_slug[1].id, secondary.id)
        self.assertEqual(pages_for_slug[2].id, primary.id)

continued next slide....

Test Example

https://github.com/stephenmcd/mezzanine/blob/master/mezzanine/pages/tests.py

        # Test page.get_ascendants uses the cached attribute,
        # without any more queries.
        self.reset_queries(connection)
        ascendants = pages_for_slug[0].get_ascendants()
        self.assertEqual(len(connection.queries), 0)
        self.assertEqual(ascendants[0].id, secondary.id)
        self.assertEqual(ascendants[1].id, primary.id)

        # Use a custom slug in the page path, and test that
        # Page.objects.with_ascendants_for_slug fails, but
        # correctly falls back to recursive queries.
        secondary.slug += "custom"
        secondary.save()
        pages_for_slug = Page.objects.with_ascendants_for_slug(tertiary.slug)
        self.assertEqual(len(pages_for_slug[0]._ascendants), 0)
        self.reset_queries(connection)
        ascendants = pages_for_slug[0].get_ascendants()
        self.assertEqual(len(connection.queries), 2)  # 2 parent queries
        self.assertEqual(pages_for_slug[0].id, tertiary.id)
        self.assertEqual(ascendants[0].id, secondary.id)
        self.assertEqual(ascendants[1].id, primary.id)

Stuart Dines

Team Leader / Senior Software Engineer @ The Interaction Consortium 
http://interaction.net.au/

@sjdines

 http://stuartdines.com

Questions?

SyDjango - November - Introduction to Testing

By Stu Dines

SyDjango - November - Introduction to Testing

  • 1,549