بنقرة واحدة
testing
Best practices for writing Django tests (updated template)
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Best practices for writing Django tests (updated template)
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
GlueFetchHelper patterns for making AJAX calls in templates
Advanced seeding patterns for linking specific foreign keys
Apply DjangoSpire app CSS variables in templates instead of inline styles
Django URL configuration patterns, namespaces, and nested structure conventions
Best practices for AI bots, prompts, and intel in scope_of_work
Examples on how we build our form views.
استنادا إلى تصنيف SOC المهني
| name | testing |
| description | Best practices for writing Django tests (updated template) |
This skill outlines testing conventions and best practices for Django applications. Tests should focus on verifying business logic outcomes rather than framework functionality.
All tests live in the app's tests/ directory:
app/
company/
models.py
services/
user_service.py
tests/
__init__.py
factories.py
test_models.py
test_services/
__init__.py
test_user_service.py
Critical Rules:
tests/ directories inside services/ submodulesapp/{app_name}/tests/app/{app_name}/tests/test_services/tests/All tests inherit from django_spire.core.tests.test_cases.BaseTestCase:
from __future__ import annotations
from django_spire.core.tests.test_cases import BaseTestCase
class MyModelTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.user = create_super_user()
self.company = create_test_company()
BaseTestCase provides:
self.client)self.super_user)All factory functions MUST use the create_test_ prefix. This distinguishes test data factories from production code.
Correct:
def create_test_company(**kwargs) -> Company:
...
def create_test_location(**kwargs) -> Location:
...
Incorrect:
def create_location(**kwargs) -> Location: # Missing create_test_ prefix
...
def create_company(**kwargs) -> Company: # Missing create_test_ prefix
...
When importing factories from other apps, always verify the exact function name in the source file before using it.
Factories belong to the app that owns the model. Use optional FK parameters with auto-creation plus **kwargs for flexible field overrides:
# app/company/tests/factories.py
from __future__ import annotations
def create_test_company(**kwargs):
defaults = {
'name': 'InMotion Logistics',
'description': 'Supply chain management solutions',
}
defaults.update(kwargs)
return Company.objects.create(**defaults)
# app/company/user/tests/factories.py
from __future__ import annotations
from typing import TYPE_CHECKING
from django.contrib.auth.models import User
from django_spire.auth.user.tests.factories import create_user
from app.company.user.models import CompanyUser
from app.company.tests.factories import create_test_company
if TYPE_CHECKING:
from app.company.models import Company
def create_test_company_user(
company: Company | None = None,
user: User | None = None,
**kwargs
) -> CompanyUser:
if company is None:
company = create_test_company()
if user is None:
user = create_user('test_user')
# Set sensible defaults that can be overridden via kwargs
defaults = {
'company': company,
'user': user,
'is_active': True,
'is_deleted': False,
}
defaults.update(kwargs)
return CompanyUser.objects.create(**defaults)
Factory Rules:
tests/factories.py file**kwargs for flexible field overridescreate_super_user() for main test userfrom __future__ import annotations and proper type hints# Create test directory structure
mkdir -p app/{app_name}/tests/test_services
touch app/{app_name}/tests/__init__.py
touch app/{app_name}/tests/factories.py
touch app/{app_name}/tests/test_models.py
touch app/{app_name}/tests/test_services/__init__.py
# app/{app_name}/tests/factories.py
from __future__ import annotations
from django_spire.auth.user.tests.factories import create_super_user, create_user
from app.{app_name}.models import {ModelName}
def create_test_{model_lower}(**kwargs):
defaults = {
'name': 'Test {ModelName}',
'description': 'Test description',
}
defaults.update(kwargs)
return {ModelName}.objects.create(**defaults)
from __future__ import annotations
from django_spire.core.tests.test_cases import BaseTestCase
from app.{app_name}.tests.factories import create_test_{model_lower}
class {ModelName}TestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.{model_lower} = create_test_{model_lower}()
def test_{action}_returns_expected_result(self):
# Arrange
# Setup test data if needed
# Act
result = self.{model_lower}.services.processor.process()
# Assert - verify business logic outcome
assert result
assert result.value == expected_value
DO Test:
DO NOT Test:
A test is only as good as its assertions. Two failure modes make an assertion worthless.
Avoid trivially true assertions and tautologies. An assertion that cannot fail tests nothing. Do not assert a literal (assert True), compare a value to itself (assert user.id == user.id), or re-assert what the line above just set:
# Tautology - cannot fail, tests nothing
user.name = 'Alice'
assert user.name == 'Alice'
If swapping the implementation for a no-op stub would leave the assertion green, the assertion is tautological. Delete it, or replace it with one that pins real behavior.
Assert the positive space AND the negative space. The golden rule of assertions is to assert the positive space that you do expect AND to assert the negative space that you do not expect, because where data moves across the valid/invalid boundary between these spaces is where interesting bugs are often found. Asserting only that the right thing happened misses the bugs where the wrong thing also happened: a duplicate row was created, a sibling record was mutated, an error was swallowed.
def test_activate_only_target_user(self):
target = create_test_company_user(is_active=False)
other = create_test_company_user(is_active=False)
CompanyUser.services.factory.activate(target)
target.refresh_from_db()
other.refresh_from_db()
assert target.is_active # positive: target activated
assert not other.is_active # negative: sibling untouched
assert CompanyUser.objects.filter(is_active=True).count() == 1 # negative: nothing else flipped
Test exhaustively, across the valid/invalid boundary. This is also why tests must test exhaustively, not only with valid data but also with invalid data, and as valid data becomes invalid. Cover the value just inside the boundary, the value on it, and the value just outside it, plus the transition itself (a record going from active to inactive, a quota at, just under, and just over the limit). Bugs cluster at these edges, not in the middle of the valid range.
def test_quantity_boundary(self):
assert OrderLine(quantity=1).is_valid() # just inside
assert not OrderLine(quantity=0).is_valid() # on the boundary
assert not OrderLine(quantity=-1).is_valid() # just outside
For view/page tests, use high-level integration tests that check response status codes. Keep tests simple and resilient to template changes.
Rules:
reverse() instead of hardcoded URLs# app/location/tests/test_views/test_page_views.py
from __future__ import annotations
from django.urls import reverse
from django_spire.core.tests.test_cases import BaseTestCase
from app.location.tests.factories import create_test_location
class LocationPageViewTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_dashboard_returns_200(self):
response = self.client.get(reverse('location:page:dashboard'))
assert response.status_code == 200
def test_list_returns_200(self):
response = self.client.get(reverse('location:page:list'))
assert response.status_code == 200
def test_detail_returns_200(self):
location = create_test_location()
response = self.client.get(reverse('location:page:detail', kwargs={'pk': location.pk}))
assert response.status_code == 200
Why keep view tests simple:
DON'T test framework basics:
def test_create_new_company_user(self):
company_user = CompanyUser.services.factory.create_or_activate(
company=self.company,
user=self.user
)
# These test Django, not our logic
assert company_user is not None
assert company_user.company == self.company
assert company_user.user == self.user
DO test business logic:
def test_create_new_company_user(self):
initial_count = CompanyUser.objects.count()
company_user = CompanyUser.services.factory.create_or_activate(
company=self.company,
user=self.user
)
assert CompanyUser.objects.count() == initial_count + 1
def test_activate_existing_company_user(self):
create_test_company_user(
company=self.company,
user=self.user,
is_active=False
)
company_user = CompanyUser.services.factory.create_or_activate(
company=self.company,
user=self.user
)
assert company_user.is_active
assert CompanyUser.objects.count() == 1
def test_to_wav_creates_file(self):
self.episode.services.transformation.to_wav()
original_path = self.episode.audio_path
new_file = Path(original_path).with_suffix('.wav')
assert new_file.exists()
new_file.unlink()
from __future__ import annotations
from typing import TYPE_CHECKING
from django_spire.auth.user.tests.factories import create_user
from app.podcast.models import Podcast
from app.podcast.tests.factories import create_test_podcast
from app.transcription.models import Transcription
from app.transcription.tests.factories import create_test_transcription
if TYPE_CHECKING:
from django.contrib.auth.models import User
def create_test_episode(
podcast: Podcast | None = None,
transcription: Transcription | None = None,
user: User | None = None,
**kwargs
):
if podcast is None:
podcast = create_test_podcast()
if transcription is None:
transcription = create_test_transcription()
if user is None:
user = create_user('test_user')
defaults = {
'podcast': podcast,
'transcription': transcription,
'user': user,
'name': 'Finding Obvious - DK & Wes go on a journey...',
'episode_number': 1,
}
defaults.update(kwargs)
return Episode.objects.create(**defaults)
Usage in tests:
def test_with_existing_podcast(self):
episode = create_test_episode(podcast=self.podcast)
def test_with_auto_created_podcast(self):
episode = create_test_episode(transcription=self.transcription)
def test_inactive_episode(self):
episode = create_test_episode(is_active=False)
def test_custom_episode_number(self):
episode = create_test_episode(episode_number=5, is_featured=True)
pytest app/explorer
pytest app/podcast/episode
pytest app/podcast/episode/tests/services/test_transformation_service.py
pytest app/podcast/episode/tests/services/test_transformation_service.py::EpisodeServiceTransformationTestCase
pytest app/podcast/episode/tests/services/test_transformation_service.py::EpisodeServiceTransformationTestCase::test_to_wav
When reviewing test files, verify the following:
django_spire.core.tests.test_cases.BaseTestCase{ModuleName}TestCase, methods use test_{action}create_test_ prefix (e.g., create_test_location)**kwargs for flexible field overridespytest's bare assert (e.g. assert x == y), never unittest's assertEqual/assertTrue/assertIsNotNone or any variantcreate_super_user() for main test user, create_user() for additional usersobjects.create() callsfrom __future__ import annotations and TYPE_CHECKING guards**kwargsreverse() instead of hardcoded URLsassert True, no self-comparisons, no re-asserting a value the test just assigned