Django is a robust backend framework that seamlessly integrates with the "unit test" module. As the Django project grows, the complexity of the tests also increases. It is essential to organize unit tests across multiple files to keep the test suite manageable. This maintains the code readability and helps in easier debugging. In this article, we will learn to separate our tests for views, models, and forms in separate files.
Spread Django Unit Tests Over Multiple Files
Let's set up the project:
django-admin startproject geekproject
cd geekproject
python manage.py startapp geekapp
Add the geekapp to the installed app in geekproject/settings.py file
# ...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'geekapp',
]
# ...
Now, let's create a model, a view and a form.
Creating a Model
In the geekapp/models.py, we can create a Book model.
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=100)
author = models.CharField(max_length=100)
published_date = models.DateField(null=True, blank=True)
def __str__(self):
return self.title
Migrate the database
Appling migrations to create the necessary database tables for the model.
python manage.py makemigrations
python manage.py migrate
Creating a Form
In the geekapp/forms.py, create a form for the Book model.
from django import forms
from .models import Book
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ['title', 'author', 'published_date']
Creating a View
In the geekapp/views.py, create a view that uses the Book form.
from django.shortcuts import render, redirect
from .forms import BookForm
def create_book(request):
if request.method == 'POST':
form = BookForm(request.POST)
if form.is_valid():
form.save()
return render(request, 'geekapp/create_book.html', {'form': form})
else:
form = BookForm()
return render(request, 'geekapp/create_book.html', {'form': form})
Creating a Template
In the geekapp/templates/geekapp/create_book.html create a simple template to render the form:
<!DOCTYPE html>
<html>
<head>
<title>Create Book</title>
</head>
<body>
<h1>Create a new book</h1>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
</body>
</html>
Add the URL Patterns
In the geekapp/urls.py, add a URL pattern for the views.
from django.urls import path
from .views import create_book
urlpatterns = [
path('create/', create_book, name='create_book'),
]
Also include this URL pattern in the project in "main urls.py". To do this use the following code.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('geekapp.urls')),
]
Write the tests
The django tests are located in tests.py within the app directory. When the test cases grow a single file is not suitable. So, the need for tests into multiple files arises. To structure the extended test files. Create a "tests_suite" directory inside the Django app. Organize the test cases by functionality inside the "tests_suite" directory and create separate files for different aspects based on the application. For example,
- "test_models.py" is used for testing models
- "test_views.py" is used for testing views
- "test_forms.py" is used for testing forms
Include the "__init__.py" file to make the tests, create an empty "__init__.py" file inside the tests directory. This allows Django to run all the tests within the directory. The directory structure is in the following format.
Running Tests from Multiple Files
Testing the Models:
In the geekapp/tests_suite/test_models.py, write tests for the Book model.
from django.test import TestCase
from geekapp.models import Book
class BookModelTest(TestCase):
def setUp(self):
self.book = Book.objects.create(title='Test Book',
author='Test Author', published_date='2023-01-01')
def test_book_creation(self):
self.assertEqual(self.book.title, 'Test Book')
self.assertEqual(self.book.author, 'Test Author')
self.assertEqual(self.book.published_date, '2023-01-01')
Testing the Views
In the geekapp/tests_suite/tests_views.py, write tests for the views.
from django.test import TestCase
from django.urls import reverse
from geekapp.models import Book
from geekapp.forms import BookForm
class CreateBookViewTests(TestCase):
def setUp(self):
self.url = reverse('create_book')
def test_create_book_view_get(self):
"""
Test that the create_book view renders the form on a GET request.
"""
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'geekapp/create_book.html')
self.assertIsInstance(response.context['form'], BookForm)
def test_create_book_view_post_valid_data(self):
"""
Test that the create_book view saves the book and redirects on a valid POST request.
"""
valid_data = {
'title': 'Test Book',
'author': 'Test Author',
'published_date': '2024-08-23',
'isbn': '1234567890123',
'price': 19.99,
}
response = self.client.post(self.url, valid_data)
self.assertEqual(Book.objects.count(), 1)
book = Book.objects.first()
self.assertEqual(book.title, 'Test Book')
self.assertEqual(book.author, 'Test Author')
def test_create_book_view_post_invalid_data(self):
"""
Test that the create_book view re-renders the form with errors on an invalid POST request.
"""
invalid_data = {
'title': '', # Title is required, so this should fail
'author': 'Test Author',
'published_date': '2024-08-23',
'isbn': '1234567890123',
'price': 19.99,
}
response = self.client.post(self.url, invalid_data)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'geekapp/create_book.html')
self.assertIsInstance(response.context['form'], BookForm)
self.assertFalse(response.context['form'].is_valid())
self.assertEqual(Book.objects.count(), 0) # No book should be created
Testing the Forms
In the geekapp/tests_suite/tests_forms.py, write tests for the form.
from django.test import TestCase
from .forms import BookForm
class BookFormTest(TestCase):
def test_valid_form(self):
form = BookForm(data={
'title': 'Test Book', 'author': 'Test Author', 'published_date': '2023-01-01'})
self.assertTrue(form.is_valid())
def test_invalid_form(self):
form = BookForm(data={'title': '', 'author': '', 'published_date': ''})
self.assertFalse(form.is_valid())
Run the tests
When the user starts to run the tests, Django's default behavior is to find all the test cases (i.e. subclasses of unittest.TestCase) in any of the file whose name starts with test, it automatically builds the test suite for those test case classes .Then it runs that suite. The default startapp template creates a tests.py file for the new application, which works well for a few tests. However, as your test suite expands, you’ll probably want to organize it into a tests package, allowing you to split tests into separate modules like test_models.py, test_views.py and test_forms.py
Django automatically detects the pattern "test*.py". When the Django test command starts to run, it picks up all the files in this directory. To run the Django project use the command.
To Run all the test cases at once:
python manage.py test
To run tests from a single file in tests_models.py
python manage.py test geekapp.tests_suite.tests_models
To run a specific test class from a test file, MyModelTestCase from test_models.py
python manage.py test geekapp.tests_suite.tests_models.BookModelTest
To run a specific test method from a class, test_model_creation from MyModelTestCase.
python manage.py test myapp.tests_suite.tests_models.BookModelTest.test_model_creation
Conclusion
Organizing the test cases in Django across multiple files is a best practice for maintenance of clean and scalable code. By creating a "tests" directory and dividing them into separate files based on their functionalities make the test suite more modular and easier to manage. Django built in test_discovery helps to run all the test cases regardless of how they are organized in the directory structure.
1. What are Django test cases?
Django test cases are a way to ensure that your Django application behaves as expected. They are written using Python’s unittest framework and Django’s test utilities. Test cases can cover various aspects of your application, such as views, models and forms
2. How do I handle test databases in Django?
Django automatically sets up a separate test database for running tests. This ensures that your test cases do not affect your production or development databases. If you don't need to manually configure this; Django handles it when you run python "manage.py test".
3. How can I ensure that my tests are running in a clean state?
Django's TestCase class uses transactions to ensure that the database is rolled back to its previous state after each test method. This means that your tests will not interfere with each other as long as they are using the TestCase class.