django-testing
Skill0 starsChanged 26 days agoMITView on GitHub
Testing patterns for multi-tenant Django POS projects using pytest-django, RequestFactory, and mocks. No factories, no complex conftest. Trigger: When writing or reviewing tests in any apps/ directory.
In this repository219 other files it gives its agents.
- .agents/AGENTS.mdAGENTS.md
- AGENTS.mdAGENTS.md
- .claude/CLAUDE.mdCLAUDE.md
- CLAUDE.mdCLAUDE.md
- .cursor/rules/behavioral-discipline.mdcCursor rule
- .cursor/rules/context-budget.mdcCursor rule
- .cursor/rules/orchestration.mdcCursor rule
- .cursor/rules/yagni-stdlib.mdcCursor rule
- androidSkill
- android-ui-designSkill
- api-versioningSkill
- astroSkill
- auth-token-inspectorSkill
- backlogSkill
- cc-api-contractSkill
- cc-db-migrationSkill
- cc-featureSkill
- cc-fixSkill
- cc-pagespeedSkill
- cc-refactorSkill
- cc-reviewSkill
- cc-spec-mutationSkill
- cc-tdd-cycleSkill
- cc-test-planSkill
- code-reviewSkill
- commitSkill
- conductor-setupSkill
- councilSkill
- database-migrationsSkill
- django-ormSkill
- django-uvSkill
- drizzle-schema-architectSkill
- evaluationSkill
- fastapi-pydantic-strictSkill
- find-skillsSkill
- jpa-nplusone-detectorSkill
- jpa-postgresSkill
- laravel-specialistSkill
- livewire-alpine-bridgeSkill
- multi-agent-orchestrationSkill
- nextjs-typescriptSkill
- openspecSkill
- pagespeed-insightsSkill
- pagespeed-perfSkill
- php-proSkill
- python-django-stackSkill
- python-fastapi-stackSkill
- pythonSkill
- rust-asyncSkill
- rust-engineeringSkill
- rust-sqlxSkill
- security-ai-llmSkill
- security-blue-teamSkill
- security-cloudSkill
- security-cryptoSkill
- security-exploit-devSkill
- security-grcSkill
- security-incident-responseSkill
- security-log-analysisSkill
- security-malware-analysisSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 234
Every test file must explain the multi-tenant constraint:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
When to Use
- Writing any test in
apps/*/tests/orapps/*/tests.py - Deciding which test base class to use
- Mocking ORM calls for TENANT_APP models
- Testing views that use custom access decorators
- Testing APIs, PDFs, and cart operations
Test Architecture in Multi-Tenant Projects
The Schema Problem
This project uses django-tenants with multi-schema PostgreSQL:
- Shared apps (public schema):
core,users, Django contrib - Tenant apps (per-store schema):
employees,catalog,cart,orders,pos,storefront,analytics
The test runner uses the public schema, so tenant app tables DON'T exist in tests. This profoundly affects how we write tests.
Base Class Selection
| Condition | Use | Notes |
| No DB access needed | SimpleTestCase | No transaction, no DB |
Only public schema models (User, Store) | TestCase | Uses public schema |
| Any tenant app model | SimpleTestCase + mocks | ALWAYS |
# WRONG — crashes because tables don't exist
class TestProductAPI(TestCase):
def test_list_products(self):
product = Product.objects.create(...) # Table does not exist!
# CORRECT — SimpleTestCase with mocks
class TestProductAPI(SimpleTestCase):
@patch("apps.catalog.views.Product")
def test_list_products(self, mock_product):
mock_product.objects.filter.return_value = MagicMock()
response = api_catalog_products(request)Mock Patterns
1. RequestFactory — Never TestClient
TestClient runs TenantMainMiddleware, which redirects tenant URLs and breaks
view tests. Use RequestFactory and call the view directly:
from django.test import RequestFactory
factory = RequestFactory()
# GET request
request = factory.get("/pos/api/products/")
request.user = mock_user
# POST request with JSON
request = factory.post(
"/pos/api/cart/add/",
data=json.dumps({"product_id": 1, "quantity": 2}),
content_type="application/json"
)
request.user = mock_user
# Call the view directly — no middleware
response = api_search_products(request)2. Bypass Access Decorators
require_pos_access and require_manager_access check EmployeeProfile
(tenant app). Bypass with is_superuser:
# The decorator does:
# def require_pos_access(view):
# def wrapper(request, *args, **kwargs):
# if not request.user.is_superuser:
# emp = EmployeeProfile.objects.get(user=request.user)
# if not emp.has_pos_access:
# return JsonResponse({"error": "No access"}, status=403)
# return view(request, *args, **kwargs)
mock_user = MagicMock()
mock_user.is_authenticated = True
mock_user.is_superuser = True # Skip EmployeeProfile lookup
request.user = mock_user3. FakeSession Pattern
Carts and session-dependent views need a writable session dict:
class FakeSession(dict):
modified = False
def save(self):
pass
request.session = FakeSession()
request.session["pos_cart"] = {}4. MagicMock.DoesNotExist Trap
except Model.DoesNotExist won't catch a plain MagicMock. Create a real
exception class:
# When mocking a model
mock_cart = MagicMock()
mock_cart.DoesNotExist = type("DoesNotExist", (Exception,), {})
with pytest.raises(mock_cart.DoesNotExist):
TemporalCart.objects.get(session_key=...)5. MagicMock name= Trap
MagicMock(name="X") sets the internal repr, NOT the .name attribute:
# WRONG — .name will be a MagicMock, not "Papel A4"
mock = MagicMock(name="Papel A4")
print(mock.name) # <MagicMock name='Papel A4'>
# CORRECT
mock = MagicMock()
mock.name = "Papel A4"
mock.price = "1500.00"6. Queryset Chain Mocking
Mock each chained call explicitly:
def _setup_product_mock(mock_product_model, product_data=None):
"""Setup complete Product mock with chained queryset."""
mock_qs = MagicMock()
# Each method in the chain returns the same mock
mock_product_model.objects.filter.return_value = mock_qs
mock_qs.select_related.return_value = mock_qs
mock_qs.annotate.return_value = mock_qs
mock_qs.order_by.return_value = mock_qs
mock_qs.__getitem__ = lambda self, s: [mock_product] if isinstance(s, slice) else mock_product
mock_qs.count.return_value = 1
return mock_qs7. Mock Model with Real Data
def _create_mock_product(product_id=1, name="Test Product", price="1000.00"):
mock = MagicMock()
mock.id = product_id
mock.name = name
mock.price = price
mock.is_active = True
mock.stock = 10
mock.is_service = False
mock.category_id = 1
mock.__str__ = lambda self: self.name
return mockFile Structure
apps/{app}/tests.py # simple apps, one file
apps/{app}/tests/__init__.py # complex apps
apps/{app}/tests/test_{feature}.pySection Separators
# ── TestClassName ────────────────────────────────────────Module Docstring
Every test file must explain the multi-tenant constraint:
"""
Tests for catalog API.
NOTE: catalog models are TENANT_APP — they live in per-store schemas.
The test runner uses the public schema, so these tables don't exist.
All tests use SimpleTestCase + mocks.
"""Naming Conventions
_make_{thing}()— builds a request or object_build_{thing}()— constructs data structures_setup_{thing}_mock()— configures a mock for reuse
Test Types
1. API/View Tests
class TestProductSearch(SimpleTestCase):
"""Tests for POS product search."""
def _make_request(self, query="", offset=0, limit=20):
factory = RequestFactory()
request = factory.get(f"/pos/api/search/?q={query}&offset={offset}&limit={limit}")
request.user = MagicMock()
request.user.is_authenticated = True
request.user.is_superuser = True
return request
@patch("apps.pos.views.Product")
def test_search_returns_results(self, mock_product):
mock_product_obj = _create_mock_product(name="Lápices")
mock_qs = MagicMock()
mock_qs.select_related.return_value = mock_qs
mock_qs.annotate.return_value = mock_qs
mock_qs.filter.return_value = mock_qs
mock_qs.__getitem__ = lambda self, s: [mock_product_obj] if isinstance(s, slice) else mock_product_obj
mock_product.objects.filter.return_value = mock_qs
request = self._make_request(query="lápices")
response = api_search_products(request)
self.assertEqual(response.status_code, 200)
data = json.loads(response.content)
self.assertIn("results", data)2. Service Tests
class TestCartOperations(SimpleTestCase):
"""Tests for cart operations."""
def _build_cart_data(self, items):
"""Build cart data as it arrives from request."""
return {str(k): {"quantity": v["quantity"], "price": str(v["price"])}
for k, v in items.items()}
@patch("apps.pos.services.CartService.get_products_from_db")
def test_calculate_total(self, mock_get_products):
mock_get_products.return_value = {
1: _create_mock_product(price="1000.00"),
2: _create_mock_product(price="2500.00"),
}
cart_data = self._build_cart_data({1: {"quantity": 2}, 2: {"quantity": 1}})
total = CartService.calculate_total(cart_data)
self.assertEqual(total, Decimal("4500.00"))3. PDF Generation Tests
class TestRestockPDF(SimpleTestCase):
"""Tests for restock PDF generation."""
def test_pdf_generates_correct_content(self):
products = [
{"name": "Product 1", "sku": "SKU001", "stock": 5, "min_stock": 10},
{"name": "Product 2", "sku": "SKU002", "stock": 0, "min_stock": 5},
]
pdf_bytes = generate_restock_pdf("Test Store", products)
# Verify it's a valid PDF
self.assertTrue(pdf_bytes.startswith(b"%PDF"))Testing Commands
# Run all tests
make tests
# Run a single file
uv run pytest apps/pos/tests/test_cart_checkout.py
# Run a single test
uv run pytest apps/pos/tests/test_cart_checkout.py::TestCartCheckout::test_checkout_success -v
# Force fresh DB (after schema changes)
uv run pytest --create-db
# With coverage
make tests-coverage
# Run with verbose output
uv run pytest -v --tb=short
# Run only failed tests from last run
uv run pytest --lfpytest Configuration
In pyproject.toml:
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings"
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "--reuse-db --nomigrations -x"
testpaths = ["apps"]Common Errors
Error: "no such table"
# CAUSE: TestCase for tenant app model
# SOLUTION: SimpleTestCase + mocks
# WRONG
class TestProduct(TestCase):
def test_create(self):
Product.objects.create(name="Test")
# CORRECT
class TestProduct(SimpleTestCase):
@patch("apps.catalog.models.Product")
def test_create(self, mock_product):
mock_product.objects.create.return_value = MagicMock(id=1)Error: "relation does not exist"
Same as above — schema mismatch between test and tenant.
Mocking transaction.atomic
from unittest.mock import patch, MagicMock
@patch("django.db.transaction.atomic")
def test_order_creation(self, mock_atomic):
# transaction.atomic as context manager
mock_context = MagicMock()
mock_atomic.return_value = mock_context
# Code uses "with transaction.atomic()"
# Mock must support context protocolResources
- Canonical queryset mock:
apps/pos/tests/test_search_products.py - FakeSession + transaction mock:
apps/pos/tests/test_cart_checkout.py - DoesNotExist + MagicMock.name traps:
apps/pos/tests/test_temporal_cart.py - Multi-decorator bypass:
apps/catalog/tests/test_api_catalog_products.py - pytest config:
pyproject.toml→[tool.pytest.ini_options] - pytest-django docs: https://pytest-django.readthedocs.io/
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

