Open In App

How to Spread Django Unit Tests Over Multiple Files?

Last Updated : 23 Aug, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

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

Python
# ...
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.

Python
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.

Python
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.

Python
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:

HTML
<!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.

Python
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.

Python
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.

Python
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.

Python
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.

Python
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
Screenshot-2024-08-23-142456
python manage.py test


To run tests from a single file in tests_models.py

python manage.py test  geekapp.tests_suite.tests_models
Screenshot-2024-08-23-142540
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
Screenshot-2024-08-23-142647
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
Screenshot-2024-08-23-142852
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.


Next Article
Practice Tags :

Similar Reads