Testing is vital. Without properly testing your code, you will never know if the code works as it should, now or in the future when the codebase changes. Countless hours can be lost fixing problems caused by changes to the codebase. What’s worse, you may not even know that there are problems at all until your end users complain about it, which is obviously not how you want to find out about code breaks.
Having tests in place will help ensure that if a specific function breaks you will know about it. Tests also make debugging breaks in code much easier, which saves time and money.
I’ve literally lost gigs in the past from not properly testing new features against the old codebase. Do not let this happen to you. Take testing seriously. You will have more confidence in your code, and your employer will have more confidence in you. It’s essentially an insurance policy.
Testing helps you structure good code, find bugs, and write documentation.
In this post, we’ll be first looking at a brief introduction that includes best practices before looking at a few examples.
Intro to testing in Django
Types of tests
Unit and integration are the two main types of tests:
- Unit Tests are isolated tests that test one specific function.
- Integration Tests, meanwhile, are larger tests that focus on user behavior and testing entire applications. Put another way, integration testing combines different pieces of code functionality to make sure they behave correctly.
Focus on unit tests. Write A LOT of these. These tests are much easier to write and debug vs. integration tests, and the more you have, the less integration tests you will need. Unit tests should be fast. We will look at a few techniques for speeding up tests.
That said, integration tests are sometimes still necessary even if you have coverage with unit tests, since integration tests can help catch code regressions.
In general, tests result in either a Success (expected results), Failure (unexpected results), or an error. You not only need to test for expected results, but also how well your code handles unexpected results.
- If it can break, it should be tested. This includes models, views, forms, templates, validators, and so forth.
- Each test should generally only test one function.
- Keep it simple. You do not want to have to write tests on top of other tests.
- Run tests whenever code is PULLed or PUSHed from the repo and in the staging environment before PUSHing to production.
- When upgrading to a newer version of Django:
- upgrade locally,
- run your test suite,
- fix bugs,
- PUSH to the repo and staging, and then
- test again in staging before shipping the code.
Structure your tests to fit your Project. I tend to favor putting all tests for each app in the tests.py file and grouping tests by what I’m testing – e.g., models, views, forms, etc.
You can also bypass (delete) the tests.py file altogether and structure your tests in this manner within each app:
└── app_name └── tests ├── __init__.py ├── test_forms.py ├── test_models.py └── test_views.py
Finally, you could create a separate test folder, which mirrors the entire Project structure, placing a tests.py file in each app folder.
Larger projects should use one of the latter structures. If you know your smaller project will eventually scale into something much larger, it’s best to use one of the two latter structures as well. I favor the first and the third structures, since I find it easier to design tests for each app when they are all viewable in one script.
Use the following packages and libraries to assist with writing and running your test suite:
- django-webtest: makes it much easier to write functional tests and assertions that match the end user’s experience. Couple these tests with Selenium tests for full coverage on templates and views.
- coverage: is used for measuring the effectiveness of tests, showing the percentage of your codebase covered by tests. If you are just starting to set up unit tests, coverage can help offer suggestions on what should be tested. Coverage can also be used to turn testing into a game: I try to increase the percent of code covered by tests each day, for example.
- django-discover-runner: helps locate tests if you organize them in a different way (e.g, outside of tests.py). So if you organize your tests into separate folders, like in the example above, you can use discover-runner to locate the tests.
- factory_boy, model_mommy, and mock: all are used in place of fixtures or the ORM for populating needed data for testing. Both fixtures and the ORM can be slow and need to be updated whenever your model changes.
In this basic example, we will be testing:
- forms, and
- the API.
Download the Github repo here to follow along.
Install coverage and add it to your INSTALLED_APPS:
Use verbosity level 2,
-v 2, for more detail. You can also test your entire Django Project at once with this command:
coverage run manage.py test -v 2.
Build your report to see where testing should begin:
Open django15/htmlcov/index.html to see the results of your report. Scroll to the bottom of the report. You can skip all rows from the virtualenv folder. Never test anything that is a built-in Python function or library since those are already tested. You can move your virtualenv out of the folder to make the report cleaner after it’s ran.
Let’s start by testing the models.
Within the coverage report, click the link for “whatever/models”. You should see this screen:
Essentially, this report is indicating that we should test the title of an entry. Simple.
Open tests.py and add the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
What’s going on here? We essentially created a
Whatever object and tested whether the created title matched the expected title – which it did.
Note: Make sure your function names start with
test_, which is not only a common convention but also so that django-discover-runner can locate the test. Also, write tests for all of the methods you add to your model.
You should see the following results, indicating the test passed:
1 2 3 4 5 6
Then if you look at the coverage report again, the models should now be at 100%.
Add the following code to the WhateverTest class in tests.py:
1 2 3 4 5 6 7 8 9
Here we fetch the URL from the client, store the results in the variable
resp and then test our assertions. First, we test whether the response code is 200, and then we test the actual response back. You should get the
1 2 3 4 5 6 7
Run your report again. You should now see a link for “whatever/views”, showing the following results:
You can also write tests to make sure something fails. For example, if a user needs to be logged in to create a new object, the test would succeed if it actually fails to create the object.
Let’s look at a quick selenium test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
Run the tests. Firefox should load (if you have it installed) and run the test. We then assert that the correct page is loaded upon submission. You could also check to ensure that the new object was added to the database.
Add the following methods:
1 2 3 4 5 6 7 8 9 10 11
Notice how we are generating the data for the form from JSON. This is a fixture.
You should now have 5 passing tests:
1 2 3 4 5 6 7 8 9 10
You could also write tests that assert whether a certain error message is displayed based on the validators in the form itself.
Testing the API
First, you can access the API from this URL: http://localhost:8000/api/whatever/?format=json. This is a simple setup, so the tests will be fairly simple as well.
Install lxml and defused XML:
Add the following test cases:
1 2 3 4 5 6 7 8 9 10 11
We are simply asserting that we get a response in each case.
In the next tutorial, we’ll be looking at a more complicated example as well as using model_mommy for generating test data. Again, you can grab the code from the repo.
Got something to add? Comment below. Please.