A Beginners Guide to Unit Testing in Django

I’ve never met a developer who is against testing, not even on Twitter. But for those who aren’t experienced in writing unit and integration tests, it is more easily said than done. Given that few course creators include testing in their curriculum makes it even harder to get started.

Why Write Unit Tests?

For me personally, it boils down to these:

  • Testing applications manually in the browser is slow work, particularly when you have to create objects in the databases to test certain behaviours. Tests will save you time in the long run by automating that process.

  • It makes debugging quicker by ruling out sources of error. Say my view returns an error I don’t recognise but I’ve tested my model, I can say with reasonable confidence, the error is not in the model.

  • Tests can tell me if my application still works after I add or refactor code. This is particularly important when I add a feature and another developer comes in to work on the same files later. If their code breaks my feature, then my tests will make sure they know about it.

Tests will save you a lot of time, but…

Writing tests is not a substitute for Quality Assurance (QA)

Writing tests will catch bugs as you write them, but a suite of passing tests, no matter how comprehensive does not guarantee that your application is bug free.

Testing serves to reduce the volume of manual testing but it can’t eliminate it. If you’re fortunate to work somewhere with a QA team, writing tests means less of their time is spent uncovering programming errors, freeing up more resource for in-depth testing.

Getting Started

Where to place unit tests

  • Each app has a file called tests.py

  • I prefer to have a separate folder for tests with at least one file just for testing models

  • Make sure each folder has a file called __init__.py (it can be empty)

  • Make sure each file name begins with test

  • Delete tests.py if you’re putting tests in its own folder

What should you test?

Make sure you test your models and your views. Test all methods that you have defined yourself including any magic methods like __str__.

If functions have multiple outcomes, say one route returns an object and the other raises an Exception, test both routes. Or if your view handles both GET and POST requests, test both kinds of request.

Should you use the database?

This depends on the code you are testing.

As a rule, I avoid creating objects in the database for testing if I can. It’s not always possible. For example, if I’m testing a function that queries the database and a passing test depends on that query returning something, then I’ll create the necessary objects in the database.

The reason I avoid the database where possible is it slows down the tests. For personal projects, you’re unlikely to notice the difference. But for large projects in with thousands of tests, it adds up.

For example, when writing tests with model instances, it is often sufficient to test the instance my_instance = MyModel(...) rather than my_instance = MyModel.objects.create(...). The first just creates an instance of the model. The second both creates the instance and saves it to the database.

If you are testing whether a database allows something, like a uniqueness constraint, then you will need to use the database.

How are Tests Structured

Django tests are class-based. In order for Django to do its magic and detect your tests, they need to inherit from a class called TestCase.

How to set up your testing files (full example at the end of the section):

1. Import TestCase from django.test

To make things a bit complicated, there are two TestCase classes you can import.

  1. from unitttest import TestCase

  2. from django.test import TestCase

If in doubt, always import from django.test. The Django TestCase includes a test database which means any objects your tests create will get deleted after the test (so you don’t end up with a messy database like I did).

2. Make sure your class name begins with “Test”

This is another box you must tick to make sure Django picks up your tests. The reason you need to do this is you might want to define your own Test Case that inherits from TestCase, and you don’t want them to be mistaken for a testing class.

3. Make sure each of your methods (tests) begin with “test_”

Another box to tick. All tests have their own method. You may have additional methods in your class for setting up tests. Django will use the method name to know what’s what.

4. Set up your tests: setUp vs setUpTestData

The setUp and setUpTestData are methods that are used to contain any code that sets up your tests. This so you don’t have to repeat code for every test. I usually use them to create instances of models.

setUp is run before the start of every test and setUpTestData is run once before all of the tests.

If you need to create lots of objects in the test database, then setUpTestData is going to be faster, as object creation only needs to be run once.

The downside of setUpTestData is the risk of coupling your tests where one test can influence another. So if one test creates an object in the database, that object will be in the test database when the next test runs, which can impact the result.

My rule of thumb is to only use setUpTestData when my tests are only consuming the data. If I’ve got tests that create objects themselves, then I prefer to use setUp to avoid inadvertently coupling my tests.

5. Make your Assertions

The final box to tick is to make sure all tests include at least one assertion. This is the line of code that determines whether the test has passed or failed.

The one I most commonly use is self.assertEqual(expected, actual) . Other assertions include:

  • self.assertIn(...)

  • self.assertTrue(...)

  • self.assertRaises(...)

Some Django specific assertions include:

  • self.assertTemplateUsed(...)

  • self.assertRedirects

An Example

Here is an example of some tests that test a view. It uses Django’s setUpTestData to set up the tests.

https://gist.github.com/aliceridgway/2f860b54f1649f6c63ee30c399065b35

Test Driven Development (TDD)

Test Driven Development is a concept where you write your tests, check that fail, and then write the code that will make your tests pass.

While a popular concept, TDD only really works if you know exactly how the code will be structured before you write it. I’ve found myself in situations where I thought I knew how the code was going to be structured and wrote tests for the functions I expected to write, only to find my design was wrong. I had to scrap the tests and start again.

My preferred approach is to write tests alongside your code, but not necessarily beforehand. Sometimes, particularly when the task is unfamiliar and complex, I prefer to spend some time exploring how to solve the problem before attempting to write the tests.

The downside here of course, is you risk writing tests based on your code and not the functional requirements. If you refactor your code and have to immediately refactor your tests to make them pass, then that’s a sign that you’ve been testing the structure of the code, rather than the behaviour.

Conclusion

We’ve covered an introduction to unit tests, mainly how to set up your files and folder structure to get started.

Writing tests is an investment that takes time and patience, but will quickly pay off. A suite of automated tests will have you spending less time doing manual testing in the browser, make it easier to debug your code, and help protect your features against future code changes.