Skip to content
Back to skills

Django Tdd

ASecurity

Use when testing Django applications with pytest - TDD workflow, pytest-django setup, factory_boy and model-bakery fixtures, DRF API testing, mocking and patching, integration tests, or coverage

  • 21 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 2, 2026
testingpythongosqldjangotestingapidatabasebackend

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned October 2, 2026

npx -y skills add CodeAtCode/oss-ai-skills --skill django-tdd --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Django Tdd?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Django Tdd
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/codeatcode-django-tdd/badge)](https://www.skillsdirectory.com/skills/codeatcode-django-tdd)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: django-tdd
description: Use when testing Django applications with pytest - TDD workflow, pytest-django setup, factory_boy and model-bakery fixtures, DRF API testing, mocking and patching, integration tests, or coverage
metadata:
  author: mte90
  version: 2.0.0
  tags:
    - python
    - django
    - testing
    - pytest
    - tdd
---

# Django Testing with TDD

Test-driven development for Django applications using pytest, factory_boy, and Django REST Framework.

## When to Activate

- Writing new Django applications
- Implementing Django REST Framework APIs
- Testing Django models, views, and serializers
- Setting up testing infrastructure for Django projects

## TDD Workflow for Django

### Red-Green-Refactor Cycle

```python
# Step 1: RED - Write failing test
def test_user_creation():
    user = User.objects.create_user(email='test@example.com', password='testpass123')
    assert user.email == 'test@example.com'
    assert user.check_password('testpass123')
    assert not user.is_staff

# Step 2: GREEN - Make test pass
# Create User model or factory

# Step 3: REFACTOR - Improve while keeping tests green
```

## Setup

### pytest Configuration

```ini
# pytest.ini
[pytest]
DJANGO_SETTINGS_MODULE = config.settings.test
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
    --reuse-db
    --nomigrations
    --cov=apps
    --cov-report=html
    --cov-report=term-missing
    --strict-markers
markers =
    slow: marks tests as slow
    integration: marks tests as integration tests
```

### Test Settings

```python
# config/settings/test.py
from .base import *

DEBUG = True
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': ':memory:',
    }
}

# Disable migrations for speed
class DisableMigrations:
    def __contains__(self, item):
        return True

    def __getitem__(self, item):
        return None

MIGRATION_MODULES = DisableMigrations()

# Faster password hashing
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.MD5PasswordHasher',
]

# Email backend
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# Celery always eager
CELERY_TASK_ALWAYS_EAGER = True
CELERY_TASK_EAGER_PROPAGATES = True
```

### conftest.py

```python
# tests/conftest.py
import pytest
from django.utils import timezone
from django.contrib.auth import get_user_model

User = get_user_model()

@pytest.fixture(autouse=True)
def timezone_settings(settings):
    """Ensure consistent timezone."""
    settings.TIME_ZONE = 'UTC'

@pytest.fixture
def user(db):
    """Create a test user."""
    return User.objects.create_user(
        email='test@example.com',
        password='testpass123',
        username='testuser'
    )

@pytest.fixture
def admin_user(db):
    """Create an admin user."""
    return User.objects.create_superuser(
        email='admin@example.com',
        password='adminpass123',
        username='admin'
    )

@pytest.fixture
def authenticated_client(client, user):
    """Return authenticated client."""
    client.force_login(user)
    return client

@pytest.fixture
def api_client():
    """Return DRF API client."""
    from rest_framework.test import APIClient
    return APIClient()

@pytest.fixture
def authenticated_api_client(api_client, user):
    """Return authenticated API client."""
    api_client.force_authenticate(user=user)
    return api_client
```

## Factory Boy Patterns

### Core Patterns with Failure Modes

**Lazy attributes** — computed values that depend on other fields:

```python
slug = factory.LazyAttribute(lambda obj: obj.name.lower().replace(' ', '-'))
```
*Failure:* `LazyAttribute` runs at object creation — if the model's `save()` overrides it, the factory value is silently lost.

**Sequence for unique values**:

```python
email = factory.Sequence(lambda n: f"user{n}@example.com")
```
*Failure:* Sequence state persists across test runs; parallel tests collide on unique constraints.

**SubFactory for relationships**:

```python
category = factory.SubFactory(CategoryFactory)
```
*Failure:* Nested SubFactory creates database hits; tests slow down exponentially with depth.

**django_get_or_create for M2M through models**:

```python
tags = factory.RelatedFactoryList(TagFactory, 'product_tags', _quantity=3)
```
*Failure:* `get_or_create` hides uniqueness violations — the test passes even when the real code would fail on duplicate keys.

**post_generation for M2M relationships**:

```python
@factory.post_generation
def tags(self, create, extracted, **kwargs):
    if not create:
        return
    if extracted:
        for tag in extracted:
            self.tags.add(tag)
```
*Failure:* If `post_generation` is missing or returns early, M2M rows are absent and the test passes for the wrong reason.

**Meta.freeze for immutable models**:

```python
class Meta:
    model = Invoice
    exclude = ('created_at',)

@classmethod
def _generate(cls, create_class, _build, *args, **kwargs):
    obj = super()._generate(create_class, _build, *args, **kwargs)
    obj.created_at = timezone.now()  # Set after save
    return obj
```
*Failure:* A non-frozen factory lets one test's mutation leak into another — `created_at` changes mid-test and assertions fail intermittently.

### Minimal Factory Example

```python
# tests/factories.py
import factory
from factory import fuzzy
from django.contrib.auth import get_user_model

User = get_user_model()

class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User

    email = factory.Sequence(lambda n: f"user{n}@example.com")
    username = factory.Sequence(lambda n: f"user{n}")
    password = factory.PostGenerationMethodCall('set_password', 'testpass123')

class ProductFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Product

    name = factory.Faker('sentence', nb_words=3)
    price = fuzzy.FuzzyDecimal(10.00, 1000.00, 2)
    category = factory.SubFactory(CategoryFactory)

    @factory.post_generation
    def tags(self, create, extracted, **kwargs):
        if not create or not extracted:
            return
        for tag in extracted:
            self.tags.add(tag)
```

### Using Factories

```python
# tests/test_models.py
import pytest
from tests.factories import ProductFactory, UserFactory

def test_product_creation():
    product = ProductFactory(price=100.00, stock=50)
    assert product.price == 100.00
    assert product.stock == 50

def test_multiple_products():
    products = ProductFactory.create_batch(10)
    assert len(products) == 10
```

## Transaction and Isolation Testing

### pytest.mark.django_db vs transaction=True

`@pytest.mark.django_db` wraps each test in a transaction and rolls back at the end. Fast, but:

- **Failure mode:** `TransactionManagementError` when code explicitly commits or uses `transaction.atomic()` incorrectly
- **Failure mode:** Data persists across tests if the transaction doesn't roll back (e.g., unhandled exception before rollback)

`transaction=True` disables transaction wrapping and uses real transactions:

```python
@pytest.mark.django_db(transaction=True)
def test_real_transaction():
    # Actual commit/rollback behavior
    pass
```

- **Failure mode:** Missing data across tests — each test truncates tables, so fixtures from other tests are gone
- **Cost:** ~10x slower due to table truncation between tests

**When to use `transaction=True`:**
- Testing code that relies on `transaction.on_commit()` callbacks
- Testing concurrent database access (multiple threads/processes)
- Testing migration data that must survive a real commit

**When to stick with default:**
- Most unit tests — speed matters
- Tests that only read/write within a single test function

## Testing Pitfalls

### Mocking Pitfalls

**Patching `request.user` on the class instead of instance**

```python
# WRONG — patches the attribute on the class, leaks across tests
@patch('django.contrib.auth.models.User')
def test_view(mock_user):
    ...

# RIGHT — patch on the instance or use client.force_login
def test_view(client, user):
    client.force_login(user)
    ...
```
*Failure:* User state from one test appears in another; authentication checks pass for the wrong user.

**Patching a manager method vs the queryset**

```python
# Model.objects.filter returns a QuerySet that must be evaluated
# WRONG — patching the method but not evaluating
@patch('apps.models.Product.objects.filter')
def test_list_products(mock_filter):
    mock_filter.return_value = [ProductFactory()]  # Returns list, not evaluated QuerySet
    ...

# RIGHT — patch at the point of evaluation or use MagicMock
@patch('apps.models.Product.objects.filter')
def test_list_products(mock_filter):
    mock_filter.return_value = MagicMock(__iter__=Mock(return_value=iter([ProductFactory()])))
    ...
```
*Failure:* QuerySet evaluation returns empty; test passes with no data when real code would return results.

**Patching a decorated view**

```python
# WRONG — decorator wraps the function; patch target is wrapper's module attribute
@patch('apps.views.my_view')
def test_view(mock_view):
    ...

# RIGHT — patch the module-level attribute after decoration
@patch('apps.views.my_view')  # Target the decorated wrapper, not the undecorated name
def test_view(mock_view):
    ...
```
*Failure:* Patch doesn't apply; the real view runs and hits the database.

**Using `override_settings` instead of `mock.patch` for settings evaluated at import time**

```python
# WRONG — settings evaluated at import time, override_settings does nothing
from myapp import CONSTANT  # CONSTANT computed from settings at import

@override_settings(MY_SETTING='new_value')
def test_something():
    assert CONSTANT == 'expected'  # Still old value

# RIGHT — mock the module-level constant or restructure to lazy evaluation
@patch('myapp.CONSTANT', 'new_value')
def test_something(mock_constant):
    ...
```
*Failure:* Settings patch has no effect; test passes with wrong configuration.

**Mocking a third-party HTTP client with un-asserted mock**

```python
# WRONG — mock returns None or empty dict by default
@patch('requests.get')
def test_external_api(mock_get):
    response = make_api_call()  # Uses requests.get internally
    assert response.status == 200  # Fails if mock_get.return_value is None

# RIGHT — configure mock to return realistic response shape
mock_get.return_value = Mock(status_code=200, json=lambda: {'data': 'value'})
```
*Failure:* Mock hides the real response shape; test passes but integration fails with actual API.

### Common Anti-Patterns

**Assert on response status only**

```python
# WRONG — 200 with wrong content passes
response = client.get('/api/items/')
assert response.status_code == 200

# RIGHT — assert on content structure
assert response.status_code == 200
assert response.data['count'] == 10
assert 'results' in response.data
```
*Failure:* Endpoint returns 200 with empty or malformed data; test passes, production breaks.

**`assertTrue(form.is_valid())` without asserting `form.errors`**

```python
# WRONG — form fails for wrong reason, test still passes
assert form.is_valid()

# RIGHT — assert specific errors
assert not form.is_valid()
assert 'email' in form.errors
assert 'Invalid email format' in form.errors['email'][0]
```
*Failure:* Form validates when it shouldn't, or fails for unexpected reasons; test gives false confidence.

**`TestCase` wrapping a test that needs real transactions**

```python
# WRONG — TestCase wraps everything in transaction; real commit behavior hidden
class TestPayment(TestCase):
    def test_payment_commit(self):
        # Code uses transaction.on_commit() — never fires in TestCase
        ...

# RIGHT — use TransactionTestCase or pytest-django with transaction=True
@pytest.mark.django_db(transaction=True)
def test_payment_commit():
    ...
```
*Failure:* Test passes but `on_commit()` callbacks never execute in production; side effects (emails, webhooks) are lost.

**Mocking what you should build**

```python
# WRONG — mock your own models
@patch('apps.models.Order')
def test_checkout(mock_order):
    ...

# RIGHT — use factories for your own models
def test_checkout():
    order = OrderFactory()
    ...
```
*Failure:* Mock doesn't reflect model behavior (validations, managers, relationships); test diverges from reality.

### Django-Specific Gotchas

**Mocking `connections` imported at module scope**

If the code under test does `from django.db import connections` at the top, patch `module_under_test.connections`, not `django.db.connections`. The module bound its own reference at import time.

**`patch.object` cannot mock dunders on instances**

`patch.object(connections, "__getitem__", ...)` silently fails. Mock at class level (`patch.object(type(connections), "__getitem__", ...)`) or substitute a wrapper object.

**Always use timezone-aware datetimes**

Naive datetimes trigger `RuntimeWarning` and fail with `filterwarnings = ["error"]`. Use `django.utils.timezone.now()` or `timezone.make_aware()`.

**`auto_now_add` in tests**

`DateTimeField(auto_now_add=True)` ignores values passed to constructor. Set value after `save()`:

```python
invoice = Invoice(customer=c)
invoice.save()
invoice.issued_at = some_time
invoice.save()
```

## Quick Reference

| Pattern | Usage |
|---------|-------|
| `@pytest.mark.django_db` | Enable database access |
| `client` | Django test client |
| `api_client` | DRF API client |
| `factory.create_batch(n)` | Create multiple objects |
| `patch('module.function')` | Mock external dependencies |
| `override_settings` | Temporarily change settings |
| `force_authenticate()` | Bypass authentication in tests |
| `assertRedirects` | Check for redirects |
| `assertTemplateUsed` | Verify template usage |
| `mail.outbox` | Check sent emails |

## Deep Dives

Load these reference files for detailed coverage of specific testing areas:

- **Model & View Testing** — `references/model-view-testing.md` — Model tests, view tests, authentication checks
- **DRF API Testing** — `references/drf-api-testing.md` — Serializer tests, ViewSet tests, API endpoints
- **Mocking & Integration** — `references/mocking-integration.md` — External service mocking, email testing, full flow tests
- **Ecosystem & Coverage** — `references/ecosystem-coverage.md` — model-bakery, django-test-migrations, django-test-plus, coverage goals

Files in this skill

  • SKILL.md14.1 KB
  • references/drf-api-testing.md5.3 KB
  • references/ecosystem-coverage.md2.1 KB
  • references/mocking-integration.md3.7 KB
  • references/model-view-testing.md4.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…