Django TDD
affaan-m/ECC
Django testing strategies with pytest-django, TDD methodology, factoryboy, mocking, coverage, and testing Django REST Framework APIs.
Set up and write pytest tests for an op-django project — pytest-django configuration, two-sided Celery testing (patched dispatch sites + plain-function task bodies, never eager mode), freezegun for…
$ npx skills add dvf/opinionated-django --skill dj-pytest -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install dvf/opinionated-django dj-pytest --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/dvf/opinionated-django.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/dj-pytest .claude/skills/dj-pytest && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "dj-pytest" agent skill from https://github.com/dvf/opinionated-django/tree/master/skills/dj-pytest into .claude/skills/dj-pytest/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "dj-pytest", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/dvf/opinionated-django/tree/master/skills/dj-pytestType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add dvf/opinionated-django --skill dj-pytest -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install dvf/opinionated-django dj-pytest --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/dvf/opinionated-django.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/dj-pytest .agents/skills/dj-pytest && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "dj-pytest" agent skill from https://github.com/dvf/opinionated-django/tree/master/skills/dj-pytest into .agents/skills/dj-pytest/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "dj-pytest", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add dvf/opinionated-django --skill dj-pytest -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install dvf/opinionated-django dj-pytest --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/dvf/opinionated-django.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/dj-pytest .cursor/skills/dj-pytest && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "dj-pytest" agent skill from https://github.com/dvf/opinionated-django/tree/master/skills/dj-pytest into .cursor/skills/dj-pytest/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "dj-pytest", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/dvf/opinionated-django.git --path skills/dj-pytest--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add dvf/opinionated-django --skill dj-pytest -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install dvf/opinionated-django dj-pytest --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/dvf/opinionated-django.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/dj-pytest .gemini/skills/dj-pytest && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "dj-pytest" agent skill from https://github.com/dvf/opinionated-django/tree/master/skills/dj-pytest into .gemini/skills/dj-pytest/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "dj-pytest", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install dvf/opinionated-django dj-pytestInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add dvf/opinionated-django --skill dj-pytest -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/dvf/opinionated-django.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/dj-pytest .github/skills/dj-pytest && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "dj-pytest" agent skill from https://github.com/dvf/opinionated-django/tree/master/skills/dj-pytest into .github/skills/dj-pytest/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "dj-pytest", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add dvf/opinionated-django --skill dj-pytest -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install dvf/opinionated-django dj-pytest --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/dvf/opinionated-django.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/dj-pytest .opencode/skills/dj-pytest && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "dj-pytest" agent skill from https://github.com/dvf/opinionated-django/tree/master/skills/dj-pytest into .opencode/skills/dj-pytest/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "dj-pytest", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
dj-pytestSet up and write pytest tests for an op-django project — pytest-django configuration, two-sided Celery testing (patched dispatch sites + plain-function task bodies, never eager mode), freezegun for…
Dj Pytest is an agent skill from dvf/opinionated-django. Set up and write pytest tests for an op-django project — pytest-django configuration, two-sided Celery testing (patched dispatch sites + plain-function task bodies, never eager mode), freezegun for time-sensitive logic, shared conftest fixtures for DTOs and svcs overrides, and the three-layer test convention (repository against a real DB, service against mocked repos, API through HTTP). Use when adding tests to a new project, writing tests for a new feature, setting up test infrastructure, or explaining how tests…
Its SKILL.md is about 4.3k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Testing & QA, covering Unit testing, Backend development and Background jobs. It works with pytest and Django. The repository describes itself as: An opinionated Django project with Repository pattern, Pydantic DTOs, svcs DI, and Stripe-style ULID IDs. The licence is MIT.
2 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit f17fc2d. It shows what the files ask for, not the result of running them.
Pre-approves these tools, so the agent can use them without asking each time:
ReadWriteEditBashGrepGlobFrom allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
uvFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md. Its commands use uv, which can reach the network depending on how they are called.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Dj Pytest loads about 4.3k tokens when it runs. Until then it costs about 137 tokens; SKILL.md has 1,284 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check noted patterns worth knowing about, such as sudo or a known installer.
allowed-tools: Read, Write, Edit, Bash, Grep, GlobAutomated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from dvf/opinionated-django at commit f17fc2d, republished under its MIT licence (© dvf). 1,284 words, ~4,257 tokens.
.claude/skills/dj-pytest/SKILL.md (or your agent's skills folder).Testing in this project is layered the same way the code is. Each layer has its own rules, its own fixtures, and its own performance characteristics. The goal is to keep the fast tests fast — service tests should never touch a database — and to isolate the slow tests at the edges.
| File | What it covers | DB? | Speed |
|---|---|---|---|
test_repo.py | ORM ↔ DTO conversion, prefetches, transactions, ID prefixes | ✅ real | slow |
test_service.py | Business logic, validation, orchestration | ❌ mocked | fast |
test_api.py | HTTP integration — request → view → service → repo | ✅ real | slow |
Service tests are the most valuable layer and should outnumber the others. If a service test needs @pytest.mark.django_db, something has leaked — find the ORM call and push it into a repository.
uv add --dev pytest pytest-django pytest-xdist freezegun pytest-mockdjango_db marker, client fixture, settings integration, django_capture_on_commit_callbacks.-n auto parallel runs. Keep it out of addopts so debugger runs stay serial.datetime.now(), time.time(), and friends. Required for any logic that touches timestamps, TTLs, scheduled work, or ULIDs whose sort order matters to the test.mocker fixture (a thin wrapper around unittest.mock with autouse cleanup).Add to pyproject.toml:
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "project.settings"
python_files = ["test_*.py"]
pythonpath = ["src"]
addopts = [
"-ra",
"--strict-markers",
"--strict-config",
]
markers = [
"slow: deselect with '-m \"not slow\"'",
]Notes:
pythonpath = ["src"] is what lets from project.services import get resolve without an editable install.--strict-markers catches typos in @pytest.mark.xxx. --strict-config does the same for the config file.markers list small and meaningful — one or two genuine custom markers maximum.Tasks are tested two-sided, and never executed through Celery machinery:
some_task.delay(...) / .apply_async(...), patch the task and assert it was dispatched with the right args.Do NOT set CELERY_TASK_ALWAYS_EAGER. Eager mode executes tasks inline inside the test transaction, which breaks any task that opens its own connection ("Cannot open a new connection in an atomic block") and silently couples every service test to every receiver. It also tests less than it appears to: the dispatch args and the body still need their own assertions either way.
The broker must be unreachable-by-design under pytest so an unpatched .delay() can never publish a real message (a live local broker means a dev worker will consume and execute test tasks against the dev database). Add to src/project/settings.py:
if "pytest" in sys.modules:
CELERY_BROKER_URL = "memory://" # unpatched .delay() lands in-process, drained by nobody
CELERY_RESULT_BACKEND = "cache+memory://"send_reliable() defers dispatch to transaction.on_commit. To flush those hooks in a test, use pytest-django's django_capture_on_commit_callbacks(execute=True) — not django_db(transaction=True), which pays a full-table TRUNCATE teardown per test. More on that below.
tests/conftest.pyA single project-level conftest.py holds the fixtures every test layer can pull from. Keep it small and generic — feature-specific fixtures go in per-app conftest.py files.
from __future__ import annotations
from decimal import Decimal
from typing import Any
from unittest.mock import MagicMock
import pytest
from freezegun import freeze_time
from products.dtos.product import ProductDTO
# ---- time --------------------------------------------------------------
@pytest.fixture
def frozen_time():
"""Freeze time at a deterministic instant. Use when tests care about now()."""
with freeze_time("2026-01-01T00:00:00Z") as frozen:
yield frozen
# ---- svcs --------------------------------------------------------------
@pytest.fixture
def override_service():
"""
Swap a real service factory for a fake for the duration of a test.
Usage:
def test_something(override_service):
fake = MagicMock(spec=ProductService)
fake.list_products.return_value = []
override_service(ProductService, fake)
...
"""
from project.services import registry
originals: dict[type, Any] = {}
def _override(service_type: type, fake: Any) -> None:
originals.setdefault(service_type, registry._factories.get(service_type))
registry.register_factory(service_type, lambda _container: fake)
yield _override
for service_type, original in originals.items():
if original is not None:
registry._factories[service_type] = original
# ---- DTO builders ------------------------------------------------------
@pytest.fixture
def make_product_dto():
"""Build a ProductDTO with sensible defaults; override anything via kwargs."""
def _build(**overrides: Any) -> ProductDTO:
fields: dict[str, Any] = {
"id": "prd_01jq3v8f6a7b2c8d9e0f1g2h3j",
"name": "Widget",
"price": Decimal("9.99"),
"stock": 5,
}
fields.update(overrides)
return ProductDTO(**fields)
return _build
# ---- repository mocks --------------------------------------------------
@pytest.fixture
def mock_product_repo(make_product_dto):
"""A MagicMock spec'd against ProductRepository, pre-loaded with a DTO."""
from products.repositories.product import ProductRepository
repo = MagicMock(spec=ProductRepository)
repo.create.return_value = make_product_dto()
repo.get_by_id.return_value = make_product_dto()
repo.list_all.return_value = [make_product_dto()]
return repoA few patterns worth calling out:
make_product_dto() is more flexible than a product_dto fixture because tests can ask for make_product_dto(stock=0) instead of mutating a shared instance.spec= on mocks. Always pass spec=SomeRepository to MagicMock — it makes the mock fail fast on attribute typos and keeps tests honest when the real class changes.override_service lets API tests substitute a fake service without monkey-patching imports. The yield/restore dance is important so a test's override doesn't bleed into the next one.test_repo.py — Real databaseimport pytest
from decimal import Decimal
from products.repositories.product import ProductRepository
from products.dtos.product import ProductDTO
@pytest.mark.django_db
def test_create_returns_dto_with_prefixed_id():
repo = ProductRepository()
dto = repo.create(name="Widget", price=Decimal("9.99"), stock=5)
assert isinstance(dto, ProductDTO)
assert dto.id.startswith("prd_")
assert dto.price == Decimal("9.99")
@pytest.mark.django_db
def test_get_by_id_round_trips():
repo = ProductRepository()
created = repo.create(name="Widget", price=Decimal("9.99"), stock=5)
fetched = repo.get_by_id(created.id)
assert fetched == createdtransaction.on_commit behavior (i.e. reliable-signal dispatch), wrap the act step in django_capture_on_commit_callbacks(execute=True) under the plain marker. Reserve @pytest.mark.django_db(transaction=True) for tests that genuinely need real commits — thread-based concurrency tests, or code that reads from a second connection — because its teardown flushes every table and it is by far the slowest thing a test can ask for.test_service.py — No databasefrom decimal import Decimal
import pytest
from products.services.product import ProductService
def test_create_product_delegates_to_repo(mock_product_repo, make_product_dto):
mock_product_repo.create.return_value = make_product_dto(name="Gadget")
service = ProductService(mock_product_repo)
result = service.create_product(name="Gadget", price=Decimal("9.99"), stock=5)
assert result.name == "Gadget"
mock_product_repo.create.assert_called_once_with(
name="Gadget", price=Decimal("9.99"), stock=5
)
def test_create_order_rejects_insufficient_stock(make_product_dto):
order_repo = MagicMock()
product_repo = MagicMock()
product_repo.get_by_id.return_value = make_product_dto(stock=1)
service = OrderService(order_repo, product_repo)
with pytest.raises(ValueError, match="Insufficient stock"):
service.create_order(items=[{"product_id": "prd_fake", "quantity": 5}])
order_repo.create.assert_not_called()@pytest.mark.django_db. If you reach for it in a service test, you've found a leak.assert_not_called() on negative paths — it's the cleanest way to prove that a validation error short-circuited a write.test_api.py — HTTP integrationimport pytest
@pytest.mark.django_db
def test_create_product(client):
response = client.post(
"/api/products/",
data={"name": "Widget", "price": "9.99", "stock": 5},
content_type="application/json",
)
assert response.status_code == 201
body = response.json()
assert body["id"].startswith("prd_")
assert body["name"] == "Widget"
@pytest.mark.django_db
def test_list_products_empty(client):
response = client.get("/api/products/")
assert response.status_code == 200
assert response.json() == []client is provided by pytest-django.Services raise plain Python exceptions (ValueError, LookupError, PermissionError); the central exception handler in src/project/api/__init__.py maps them to HTTP responses (400, 404, 403) with a {"detail": "..."} body. API tests are the layer that proves the round-trip — assert on the status code and the JSON body, not on the raised exception.
@pytest.mark.django_db
def test_create_order_rejects_insufficient_stock(client):
# Create a product with only 1 in stock.
product_resp = client.post(
"/products/",
data={"name": "Limited", "price": "5.00", "stock": 1},
content_type="application/json",
)
product_id = product_resp.json()["id"]
response = client.post(
"/orders/",
data={"items": [{"product_id": product_id, "quantity": 10}]},
content_type="application/json",
)
assert response.status_code == 400
assert "Insufficient stock" in response.json()["detail"]The equivalent test_service.py test should assert the raw exception (with pytest.raises(ValueError, match="Insufficient stock"):) rather than a status code — the service test doesn't go through the API, so it never sees the HTTP mapping. Layering matters: service tests prove the exception is raised, API tests prove the exception handler maps it correctly.
Receivers run via Celery in production. In tests, cover each side separately: prove the dispatch was enqueued post-commit, and prove the receiver body works when called as a plain function.
import pytest
from orders.receivers import on_order_created
@pytest.mark.django_db
def test_order_created_dispatches_receiver(mocker, django_capture_on_commit_callbacks):
delay = mocker.patch("project.signals._dispatch_reliable_receiver.delay")
with django_capture_on_commit_callbacks(execute=True):
# ...build real repos, call service.create_order(...)
...
delay.assert_called_once() # and assert on the receiver path + payload args
def test_receiver_is_idempotent(mocker):
send_email = mocker.patch("orders.receivers.send_order_confirmation")
on_order_created(order_id="ord_fake")
on_order_created(order_id="ord_fake")
assert send_email.call_count == 1 # or whatever idempotency you've implementedThe django_capture_on_commit_callbacks(execute=True) context is what makes the first test honest: without it, on_commit hooks never fire under the plain marker and the dispatch assertion goes vacuous. The second test is the one that matters most — every receiver needs an explicit "called twice, ran once" test. At-least-once delivery is non-negotiable and so is the test that proves you respected it.
Use @freeze_time (or the frozen_time fixture) for any test that asserts on timestamps, TTLs, scheduling windows, or otherwise time-sensitive logic.
from freezegun import freeze_time
@freeze_time("2026-01-15T12:00:00Z")
def test_expires_at_is_24h_from_now(make_product_dto):
service = SubscriptionService(MagicMock())
dto = service.start_trial(user_id="usr_fake")
assert dto.expires_at.isoformat() == "2026-01-16T12:00:00+00:00""2020-01-01". Future-you will thank you.@pytest.mark.django_db in a service test. The service has an ORM import hiding in it. Fix the service, not the test.make_*) instead.response.json() == {...} with the full dict. Too brittle. Assert on the fields you care about plus an ID prefix.django_capture_on_commit_callbacks(execute=True). on_commit won't fire under the plain django_db marker, the dispatch never happens, and the test silently passes without exercising anything. (The inverse mistake — reaching for django_db(transaction=True) just to flush hooks — costs a full-table TRUNCATE per test.)CELERY_TASK_ALWAYS_EAGER "to make the tests realistic". It executes tasks inline inside the test transaction, breaks tasks that open their own connection, and still doesn't test dispatch args or the body in isolation. Patch .delay() at the dispatch site; call the body as a plain function..filter() work?" — trust the framework and test your code.test_service.py file. Service tests should use pytest.raises; only test_api.py round-trips through the exception handler.uv run pytest
uv run pytest -m "not slow" # fast loop for iterating
uv run pytest tests/orders/ # one app
uv run pytest --lf # re-run last failures onlytest_repo.py and test_api.py are outnumbering test_service.py, business logic is in the wrong layer.© dvf, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in skills/dj-pytest of dvf/opinionated-django.
Open the folder on GitHubat commit f17fc2d
Dj Pytest next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Dj Pytest this skilldvf/opinionated-django | 109 | — | ~4.3k | Automated safety check: Notes | MIT | |
| Django TDDaffaan-m/ECC | 274k | 7 repos | ~5.3k | Automated safety check: Pass | MIT | |
| Django TDDaffaan-m/ECC | 274k | 3 repos | ~4.9k | Automated safety check: Pass | MIT | |
| Write Backend Unit Testbaserow/baserow | 6.1k | — | ~1.8k | Automated safety check: Pass | Custom licence | |
| Django TDDxu-xiang/everything-claude-code-zh | 2k | — | ~4.7k | Automated safety check: Pass | MIT | |
| Pytestbobmatnyc/claude-mpm | 155 | — | ~8.2k | Automated safety check: Pass | MIT |
affaan-m/ECC
Django testing strategies with pytest-django, TDD methodology, factoryboy, mocking, coverage, and testing Django REST Framework APIs.
affaan-m/ECC
Django 测试策略,包括 pytest-django、TDD 方法、factoryboy、模拟、覆盖率以及测试 Django REST Framework API。
baserow/baserow
Write or update Baserow backend tests for core, premium, or enterprise code using pytest, Django, DRF APIClient, and the repo's shared fixture patterns.
xu-xiang/everything-claude-code-zh
使用 pytest-django 进行 Django 测试的策略、TDD 方法论、factoryboy、Mock 模拟、测试覆盖率以及测试 Django REST Framework API。
bobmatnyc/claude-mpm
pytest - Python's most powerful testing framework with fixtures, parametrization, plugins, and framework integration for FastAPI, Django, Flask
mathiasertl/django-ca
Instructions for running, writing, and maintaining tests in this project
dvf/opinionated-django
Implement a Django feature following the opinionated architecture — prefixed ULID IDs, repository pattern, Pydantic DTOs, svcs service locator, project-scoped django-ninja API, Celery reliable…
dvf/opinionated-django
Structure Django models with proper Meta classes, verbose names, and optimized indexes.
dvf/opinionated-django
Use Stripe-style prefixed ULID primary keys (e.g. An agent skill from dvf/opinionated-django.
dvf/opinionated-django
Set up a Django project into the op-django layout so the architecture, signals, and settings skills have a foundation to build on.
dvf/opinionated-django
Structure Django business logic as plain services that receive their dependencies via constructor injection, and wire them through an svcs registry so they can be resolved anywhere — views, Celery…
dvf/opinionated-django
Add reliable signals (async side-effects via Celery) to a Django feature.
Categories
Set up and write pytest tests for an op-django project — pytest-django configuration, two-sided Celery testing (patched dispatch sites + plain-function task bodies, never eager mode), freezegun for…. Dj Pytest is an agent skill from dvf/opinionated-django. Set up and write pytest tests for an op-django project — pytest-django configuration, two-sided Celery testing (patched dispatch sites + plain-function task bodies, never eager mode), freezegun for time-sensitive logic, shared conftest fixtures for DTOs and svcs overrides, and the three-layer test convention (repository against a real DB, service against mocked repos, API through HTTP).
Dj Pytest fits situations like: adding tests to a new project; writing tests for a new feature; setting up test infrastructure; explaining how tests should be organized.
Run `npx skills add dvf/opinionated-django --skill dj-pytest -a claude-code`. Or copy the skill folder (skills/dj-pytest in dvf/opinionated-django) into .claude/skills/dj-pytest in your project. Claude Code loads it when a task matches its description.
Run `npx skills add dvf/opinionated-django --skill dj-pytest -a codex`. Or copy the skill folder (skills/dj-pytest in dvf/opinionated-django) into .agents/skills/dj-pytest in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add dvf/opinionated-django --skill dj-pytest -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/dj-pytest, .gemini/skills/dj-pytest, .github/skills/dj-pytest and .opencode/skills/dj-pytest in your project.
Going by SKILL.md and its folder, Dj Pytest needs the command-line tools its instructions call (uv). Our summary lists: Python 3. Its frontmatter pre-approves these tools: Read, Write, Edit, Bash, Grep, Glob.
SKILL.md contains no URLs. Its commands use uv, which can reach the network depending on how they are called. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found notes only (pre-approves every shell command (allowed-tools: bash)), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.
Dj Pytest is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 4.3k tokens (SKILL.md is roughly 17k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Dj Pytest: Django TDD (affaan-m/ECC, 274k stars), Django TDD (affaan-m/ECC, 274k stars), Write Backend Unit Test (baserow/baserow, 6.1k stars) and Django TDD (xu-xiang/everything-claude-code-zh, 2k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
dvf (a GitHub user) maintains it in dvf/opinionated-django, which has 109 GitHub stars. The repository holds 9 skills in this directory. The repository was last updated on August 14, 2026.
Source: dvf/opinionated-django on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.