Testing Guide¶
pytest-django for the backend, Vitest for both frontends. 1200+ backend tests, 360+ admin tests.
Backend Testing¶
Running Tests¶
cd backend
uv run pytest # All tests
uv run pytest apps/loans/tests/ # App-specific
uv run pytest -k "test_create_loan" # By name pattern
uv run pytest -v --tb=long # Verbose with full tracebacks
pytest Configuration¶
# backend/pyproject.toml
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings.test"
python_files = ["tests.py", "test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = "-v --tb=short"
asyncio_mode = "auto"
asyncio_mode = "auto" means async test functions run automatically without needing @pytest.mark.asyncio.
Test Infrastructure (conftest.py)¶
The root backend/conftest.py provides session-scoped tenant setup:
Custom model-bakery generators:
generators.add("djmoney.models.fields.MoneyField", lambda: Decimal("100.00"))
generators.add("phonenumber_field.modelfields.PhoneNumberField", lambda: "+12025551234")
generators.add("django.db.models.DecimalField", lambda: Decimal("0.50000"))
generators.add("timezone_field.TimeZoneField", lambda: "UTC")
Key fixtures:
| Fixture | Scope | Purpose |
|---|---|---|
test_tenant |
Session | Creates a test tenant + domain once per run |
tenant_schema |
Function (autouse) | Activates test tenant schema for every test |
_close_db_connections |
Function (autouse) | Closes DB connections after each test |
_terminate_db_connections_on_teardown |
Session | Terminates all PostgreSQL sessions before DB drop |
user_factory |
Function | Creates User instances with custom email/password |
user |
Function | Single test user |
api_user |
Function | Superadmin user for API tests |
tenant_user_factory |
Function | Creates user with specific tenant role |
model-bakery Fixtures¶
Use baker.make() for test data. Never manually construct model instances with hardcoded fields:
from model_bakery import baker
# Auto-generates all required fields
loan = baker.make("loans.Loan")
# Override specific fields
loan = baker.make("loans.Loan", status="active", term_months=36)
# Prepare without saving (unit tests)
loan = baker.prepare("loans.Loan")
# Bulk creation
loans = baker.make("loans.Loan", _quantity=5)
# Override nested relationships
payment = baker.make("payments.Payment", loan__status="active")
Recipes for reusable scenarios live in baker_recipes.py per app:
# apps/loans/baker_recipes.py
from model_bakery.recipe import Recipe
active_loan = Recipe(Loan, status="active", term_months=36)
delinquent_loan = active_loan.extend(days_past_due=45)
Test Patterns¶
Service Tests (Primary Focus)¶
pytestmark = pytest.mark.django_db(transaction=True)
class TestCreateNote:
async def test_creates_note(self):
note = await services.create_note(
content_type="loans.loan",
object_id=uuid.uuid4(),
body="Important note.",
created_by=uuid.uuid4(),
)
assert note.body == "Important note."
assert note.note_type == "general"
Selector Tests¶
class TestNoteList:
async def test_returns_all_notes(self):
baseline = await Note.objects.acount()
borrower = baker.make("borrowers.Borrower")
baker.make("notes.Note", content_object=borrower, _quantity=3)
qs = await selectors.note_list()
assert await qs.acount() == baseline + 3
API Tests¶
from ninja_extra.testing import TestAsyncClient
class TestNoteController:
@pytest.fixture()
def client(self):
return TestAsyncClient(NoteController)
async def test_list_notes(self, client, api_user, test_tenant):
baker.make("notes.Note", _quantity=2)
response = await client.get("", user=api_user, tenant=test_tenant)
assert response.status_code == 200
TestAsyncClient tests controllers directly, bypassing middleware for faster execution.
Provider Tests¶
@pytest.fixture()
def mock_s3_client():
client = MagicMock()
client.generate_presigned_url.return_value = "https://s3.amazonaws.com/..."
return client
class TestS3StorageProvider:
async def test_generate_upload_url(self, storage, mock_s3_client):
result = await storage.generate_upload_url("doc.pdf", "application/pdf")
assert result["method"] == "PUT"
mock_s3_client.generate_presigned_url.assert_called_once()
Test File Organization¶
apps/{module}/tests/
├── test_services.py # Service layer tests (primary focus)
├── test_selectors.py # Selector/query tests
└── test_api.py # API integration tests
Frontend Testing¶
Admin Dashboard¶
Uses renderWithAdmin() and renderWithRecord() helpers from test-utils.tsx. See Admin Testing.
Borrower Portal¶
Uses renderWithProviders() helper from test-utils.tsx. See Portal Testing.
Frontend Test Conventions¶
- Use Testing Library queries (
getByRole,getByText,getByLabelText) - Prefer
userEventoverfireEventfor user interactions - Mock API calls at the data provider or fetch level
- Test behavior, not implementation details
See Also¶
- Admin Testing --- Admin-specific test utilities
- Portal Testing --- Portal-specific test utilities
- Code Conventions --- Coding standards