When you create a Django app, a tests.py file is automatically generated.
Most developers have at least once wondered this.
“Is this file… really necessary?”
“What am I supposed to do with it, anyway?”
In fact, tests.py is the official test entry point recommended by Django, and
it becomes a true safety net during refactoring, feature additions, and version upgrades once you get used to it. (Django Project)
In this article, we will summarize:
-
What
tests.pyis for -
How to write and run tests
-
When it is especially useful
-
Real-world examples
All at once.
1. What is tests.py?
Django uses the test framework based on unittest from the Python standard library.
-
Create test classes that inherit from
django.test.TestCaseor similar -
Write methods that start with
test_inside -
Run them automatically using the
./manage.py testcommand.
When you create an app with startapp, a tests.py file is generated by default:
-
For small projects → one
tests.pyis sufficient -
For larger projects → it is recommended to manage them in a
tests/package (e.g.,test_models.py,test_views.py) as outlined in Django's documentation.
So, it can be understood as the “default location for app unit tests”.
2. When is it useful? (Why should we use tests?)
Django tests shine especially in the following situations.
-
Model/Business Logic Validation
-
The more complex the calculation logic, state changes, and custom methods are
-
Once you have created a test, you can confidently refactor during future changes
-
-
View/URL/Permission Checks
-
Whether the response code varies based on authentication status
-
Whether invalid input is properly redirected/error handled
-
Whether the necessary context passes correctly to the template
-
-
Preventing Regression Bugs
- Once you fix a bug, if you document it with tests, the same issue won’t occur again
-
Ensuring Stability During Version Upgrades
-
When you update Django or library versions
-
You can run
./manage.py testonce to check before starting
-
3. The Most Basic Example of tests.py
3-1. Model Tests
# myapp/tests.py
from django.test import TestCase
from .models import Article
class ArticleModelTests(TestCase):
def setUp(self):
self.article = Article.objects.create(
title="Test Title",
content="Content",
views=0,
)
def test_increase_views(self):
# given
self.assertEqual(self.article.views, 0)
# when
self.article.increase_views()
# then
self.assertEqual(self.article.views, 1)
-
TestCaseis a Django class that extendsunittest.TestCase. -
Each test is isolated at the transaction level, so the database is rolled back after each test to avoid interference.
4. Testing Views/URLs – Using Client
Django provides a test-specific HTTP client, django.test.Client.
You can send GET/POST requests to URLs and inspect responses without starting an actual server.
4-1. Simple View Test
# myapp/tests.py
from django.test import TestCase
from django.urls import reverse
from .models import Article
class ArticleViewTests(TestCase):
def setUp(self):
self.article = Article.objects.create(
title="Post 1",
content="Content",
views=0,
)
def test_article_detail_page_returns_200(self):
url = reverse("article_detail", args=[self.article.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Post 1")
The key points here are:
-
self.client: the instance of the test client provided byTestCase -
reverse("article_detail", args=[...]): retrieves the URL by its name instead of hardcoding it -
assertContains: checks if a specific string is included in the response body
4-2. Testing Login/Permission Checks
from django.contrib.auth import get_user_model
class ArticlePermissionTests(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(
username="tester",
password="pass1234",
)
self.article = Article.objects.create(
title="secret",
content="Secret Post",
views=0,
)
def test_anonymous_user_redirected_to_login(self):
url = reverse("article_edit", args=[self.article.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 302)
self.assertIn("/accounts/login", response["Location"])
def test_logged_in_user_can_access_edit_page(self):
self.client.login(username="tester", password="pass1234")
url = reverse("article_edit", args=[self.article.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
In this way:
-
Checks if an anonymous user is redirected
-
Checks if a logged-in user can access normally
You can filter out permission-related bugs early on.
5. When and how to run tests.py?
5-1. Running All Tests
$ python manage.py test
- Automatically finds and runs
test*.pyfiles in the current directory and below
subclasses ofunittest.TestCaseordjango.test.TestCase.
5-2. Running a Specific App
$ python manage.py test myapp
5-3. Running Specific Modules/Classes/Methods
# Entire myapp.tests module
$ python manage.py test myapp.tests
# Specific TestCase
$ python manage.py test myapp.tests.ArticleViewTests
# Specific test method
$ python manage.py test myapp.tests.ArticleViewTests.test_article_detail_page_returns_200
During development, it is good practice to quickly run only the modified part.
6. Expanding tests.py into a tests package
As projects get larger, one tests.py file won't suffice.
Django's documentation also recommends structuring it into packages. (Django Project)
For example:
myapp/
tests/
__init__.py
test_models.py
test_views.py
test_forms.py
test_api.py
Inside each file:
# myapp/tests/test_models.py
from django.test import TestCase
from myapp.models import Article
class ArticleModelTests(TestCase):
...
This way, the execution remains the same.
$ python manage.py test myapp # All
$ python manage.py test myapp.tests.test_models # Only model tests
7. Some Real-World Use Cases
7-1. Preventing Regression with “Bug Reproduction Tests”
-
A bug occurs in production
-
A reproduction test is written locally (it should fail under normal circumstances)
-
Code is modified
-
Check that the test passes
-
gitwith the test and modified code committed together
Doing this ensures that the same bug won’t occur again during refactoring.
7-2. Fixing API Response Formats
When working with the frontend, the API response format is very important.
class ArticleApiTests(TestCase):
def setUp(self):
self.article = Article.objects.create(
title="API Title",
content="Content",
views=0,
)
def test_article_detail_api_response(self):
url = reverse("api:article-detail", args=[self.article.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data["id"], self.article.id)
self.assertIn("title", data)
self.assertIn("content", data)
-
Clearly specify which fields must be included in the response
-
If a field name is accidentally changed or removed, the test will fail immediately
7-3. Testing Form Validation Logic
from .forms import ArticleForm
class ArticleFormTests(TestCase):
def test_title_is_required(self):
form = ArticleForm(data={"title": "", "content": "Content"})
self.assertFalse(form.is_valid())
self.assertIn("title", form.errors)
def test_valid_form(self):
form = ArticleForm(data={"title": "Title", "content": "Content"})
self.assertTrue(form.is_valid())
- You can document the form validation rules through tests.

8. Conclusion – Don’t Delete tests.py, Just Give It a Try Once
To summarize in one line:
tests.pyis the file that creates a “safety net for Django apps.”
-
It’s initially a hassle, but
-
Once you experience developing based on testing,
-
The stress around refactoring, adding features, and upgrading versions decreases significantly.
In real projects:
-
When creating new features – add at least 1-2 tests along with it
-
When fixing bugs – always start by writing a test to reproduce that bug
-
When starting to scale up the project – refactor
tests.pyinto atests/package
If you stick to this pattern, you will have a completely different development experience compared to a “Django project without tests.”
There are no comments.