Even with a small site, manually navigating to each page and making a quick check that everything is working properly can take several minutes. As we make changes and the site grows, the time required to make checks will only increase. If we kept this up, at some point we would spend more time running tests than writing code and making changes. Automated tests could seriously help us deal with this problem.
Django provides a framework for creating tests based on a hierarchy of classes, which in turn depend on the standard Python unittest library. Despite the name, this framework is suitable for both unit and integration testing. The Django framework adds API methods and tools that help you test both web and Django-specific behavior. This allows you to simulate URL requests, add test data, and validate the output of your applications.
And that's great because, in Django application development, it's important to perform different types of testing. Unit tests help to check individual functions or methods, integration tests make sure that different parts of the system interact with each other correctly. Functional or end-to-end tests check how the system works as a whole from the user's point of view. It's also worthwhile to perform regression testing to prevent already fixed bugs from reappearing. All these types of testing help ensure your application's reliability and stability.
TestCase in Django
It's straightforward to create tests in Django. Django employs the unit-test module, an in-built test "discoverer" that identifies tests in the current working directory and any file named with the test*.py template. Use any structure you prefer by naming files accordingly. We recommend creating a package for your testing code and keeping the model files, mappings, forms, and any other files apart from the code used for tests. Therefore, let's create a file test_wallet.py intended for our currency application. Now, we'll create our initial test case.
from django.test import TestCase
from .models import Wallet
class WalletTest(TestCase):
def test_wallet(self):
my_wallet = Wallet.objects.create()
self.assertEqual(my_wallet.balance, 'default value')You import the essential modules and classes first. After that, create the WalletTest class, which inherits from Django's TestCase. Within this class, you will define a test_wallet function, which is the test itself. You establish an instance of the wallet in this method, and then verify if its balance matches the 'default value'. If it doesn't, the test is considered failed.
Now, consider a more complicated example with a setup and multiple tests. Imagine having an application with Wallet and Owner models. Here’s an effective way to test it:
from django.test import TestCase
from .models import Wallet, Owner
class PostTest(TestCase):
def setUp(self):
self.owner = Owner.objects.create(name='Test Owner')
self.wallet = Wallet.objects.create(title='Test Wallet', body='Test Body', owner=self.owner)
def test_owner_name(self):
self.assertEqual(self.owner.name, 'Test Owner')
def test_wallet_info(self):
self.assertEqual(self.wallet.name, 'Test Wallet')
self.assertEqual(self.wallet.body, 'Test Body')
self.assertEqual(self.wallet.owner, self.owner)You use the setUp method to create objects needed for your tests. Since the setUp method runs before each test method, it's a convenient place to create common objects. Then, you have individual test methods to check the owner's name and the wallet's details.
A Django TestCase is a class inheriting from django.test.TestCase or another Django test case class. Each method in this class that starts with "test" will run as a separate test. Most of the time, django.test.TestCase is the best base class for tests. This class generates a clean database before running its methods and executes each test function in its own transaction. The class also includes a test Client, which you can utilize to simulate user interaction with your application at the display level. Other available classes include SimpleTestCase, TransactionTestCase, and LiveServerTestCase.
Django Test Client
The Django Test Client is a robust tool that lets you pretend you're a user interacting with your web application, and then observe how the application reacts. When you need to ensure your application behaves as expected with different user inputs, it's highly useful. Think of it as a simple web browser which you can use to replicate standard user actions, such as clicking on links or submitting forms, and then see what comes back. Everything about the application's responses can be observed, from basic elements like HTTP headers and status codes to detailed features like the HTML templates used to build the page, and the data passed to the templates. In instances where the application redirects the user to a different page, you can follow each step and check the URLs and status codes.
The Test Client is offered by Django and can be accessed in each TestCase instance through the self.client attribute. You can use self.client to make GET, POST, and other HTTP requests to your application and then test the responses. Here's an example of how to use Test Client:
from django.test import TestCase
class MyWalletTest(TestCase):
def test_wallet_home_view(self):
response = self.client.get('/')
self.assertEqual(response.status_code, 200)In this example, you create a GET request to your web application's main page (path '/') and then ensure that the response status is 200, which indicates a successful response. Test Client also supports sessions and authorization, enabling you to test more complex scenarios where the user must be logged in or have certain access rights.
Here's an example of a more elaborate test case using Django's Test Client:
from django.test import TestCase
from django.contrib.auth.models import User
class MyWalletTest(TestCase):
def setUp(self):
# Create a test user
self.user = User.objects.create_user(username='testuser', password='testpassword')
def test_wallet_home_view(self):
# Log in the test user
self.client.login(username='testuser', password='testpassword')
# Send a GET request to the main page of our web application
response = self.client.get('/')
# Check that the response status is 200, which means a successful response
self.assertEqual(response.status_code, 200)
# Check that the user's username appears in the response
self.assertContains(response, 'testuser')
# Log out the test user
self.client.logout()You create a test user in the setUp method, which is a special method that Django runs before each test. Then you log in the test user, send a GET request to the main page, and check that the response is successful and contains the user's username.
Test configuration
Django offers multiple settings to control the test framework's behavior. The TEST_RUNNER is among the crucial variables. This setting manages how Django identifies and runs tests. By default, Django utilizes django.test.runner.DiscoverRunner. If you wish to tailor the test runner process, you can replace it with your own class.
TEST_RUNNER = 'myapp.mytestrunner.MyTestRunner'The PASSWORD_HASHERS setting allows you to define the password hashers to be used in testing. Normally, Django would use the same hashers defined in PASSWORD_HASHERS. However, you may replace them with quicker alternatives to accelerate the testing process.
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]The DATABASES setting lets you decide which databases will be used during testing. You can specify that your tests should use an in-memory SQLite database to speed up the testing process.
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:',
}
}@override_settings deserves special mention. The @override_settings decorator in Django is useful for temporarily altering settings during testing. This decorator allows you to override settings within a test method and revert back to the original settings after the test concludes.
from django.test import TestCase
from django.test import override_settings
class MyTest(TestCase):
@override_settings(DEBUG=True)
def test_debug(self):
assert settings.DEBUGIn this example, the DEBUG setting is set to True during the execution of test_debug. Once the test is completed, the DEBUG setting will return to its original value.
Running tests
To execute your tests, use the test command provided by Django's manage.py script:
python manage.py testThis command will discover and run all tests in your application. If you wish to run tests for a specific app, supply its name as an argument:
python manage.py test myappThe output will display the number of tests run, the number of failures, and the number of errors. If a test fails, Django will also provide a detailed traceback of the problem. Here's a sample of what the output may look like:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
Destroying test database for alias 'default'...Two tests ran (denoted by the two dots), and both passed (indicated by 'OK'). If a test had failed, it would be indicated by an 'F', and an error would be displayed as an 'E'.
Conclusion
Commercial applications can't operate without testing. Django provides a robust testing framework built upon Python's unittest module. It streamlines the creation of both unit and integration tests with its API methods and tools. Your task is to learn how to utilize these tools. Start with TestCase. Each method in TestCase that starts with "test" will run as an individual test. Write some tests for your standalone project. Test the functioning of various objects. Django provides many settings to manipulate the behavior of the testing framework, covering the test runner, password hashers, and databases. Therefore, Django's testing facilities form a crucial part of ensuring your Django applications' dependability and stability.