Skip to content

Testing Guide

Testing a unified application requires validating three distinct layers: your reactive Reflex state event handlers, your Django database models, and the request-level middleware that bridges the two.

With standard tools like pytest, pytest-django, and pytest-asyncio, you can write robust, highly performant automated tests. This guide explains how to isolate async operations, mock user sessions, test permission pipelines, and set up continuous integration.


1. Preparing the Test Environment

To prevent Django from throwing AppRegistryNotReady exceptions, you must define your environment settings before any tests or models are imported.

Step 1: Create conftest.py

Place a conftest.py configuration in your project's testing directory (or project root). Assign DJANGO_SETTINGS_MODULE explicitly:

# tests/conftest.py
import os
import pytest

# Enforce settings module resolution before importing any Django components
os.environ["DJANGO_SETTINGS_MODULE"] = "my_project.settings"

import django
django.setup()

@pytest.fixture(autouse=True)
def enable_db_access_for_all_tests(db):
    """Automatically grant database access to all tests in the suite."""
    pass

Step 2: Configure Pytest

In your pyproject.toml or pytest.ini, configure pytest-asyncio to run in automatic mode:

# pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
django_find_project = true
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"

2. Unit Testing State Event Handlers

Since ModelState and ModelCRUDView are standard Reflex states, you can instantiate them in unit tests and invoke their event handlers directly. Because event handlers inside reflex-django utilize Django's async ORM, your unit tests must run within an asynchronous test runner.

# tests/test_catalog.py
import pytest
from shop.models import Product
from shop.state import ProductState

@pytest.mark.django_db
async def test_create_product_success():
    # 1. Instantiate the State class
    state = ProductState()

    # 2. Simulate user typing inputs
    state.name = "Premium Keyboard"
    state.price = "120.00"
    state.sku = "ELEC-KEY-01"
    state.is_active = True

    # 3. Invoke the saving event handler asynchronously
    await state.save()

    # 4. Assert reactive state variables updated correctly
    assert state.error == ""
    assert state.editing_id == -1  # Reset to -1 on successful creation

    # 5. Verify the record was successfully written to the database
    from shop.models import Product
    db_product = await Product.objects.aget(sku="ELEC-KEY-01")
    assert db_product.name == "Premium Keyboard"
    assert db_product.price == 120.00

3. Mocking Request Contexts & Session Authentication

If your state handlers evaluate permissions, reference self.request.user, or restrict views based on active sessions, you must mock the request pipeline. reflex-django exposes internal hooks (begin_event_request and end_event_request) to mock context parameters:

# tests/test_security.py
import pytest
from django.contrib.auth import get_user_model
from reflex_django.middleware import begin_event_request, end_event_request
from shop.state import ProductState

User = get_user_model()

@pytest.fixture
async def authenticated_user():
    """Create a standard authenticated test user."""
    return await User.objects.acreate(
        username="store_manager",
        email="manager@shop.com"
    )

@pytest.mark.django_db
async def test_restricted_action_requires_login(authenticated_user):
    state = ProductState()

    # 1. Mock request metadata (cookies, headers, route)
    mock_router_data = {
        "headers": {"cookie": "sessionid=test_session_id"},
        "pathname": "/inventory",
    }

    # 2. Bind request context simulating active user session
    begin_event_request(
        state=state,
        user=authenticated_user,
        router_data=mock_router_data
    )

    try:
        # 3. Execute state actions that require request context
        assert state.request.user.is_authenticated
        assert state.request.user.username == "store_manager"

        state.name = "Manager Item"
        state.sku = "MGR-001"
        await state.save()

        assert state.error == ""

    finally:
        # 4. Always tear down the request context to clean threadvars
        end_event_request(state)

4. Testing User-Scoped Constraints & Validation

If you use UserScopedMixin to scope records to their creators, verify that users are strictly blocked from loading or deleting records owned by other users.

# tests/test_scoping.py
import pytest
from django.contrib.auth import get_user_model
from reflex_django.middleware import begin_event_request, end_event_request
from blog.models import BlogPost
from blog.states import PostsState

User = get_user_model()

@pytest.mark.django_db
async def test_user_cannot_load_foreign_post():
    # 1. Create two separate users and an article
    owner = await User.objects.acreate(username="owner")
    attacker = await User.objects.acreate(username="attacker")

    private_post = await BlogPost.objects.acreate(
        title="Owner Secrets",
        slug="secrets",
        author=owner
    )

    # 2. Instantiate and bind context simulating the attacker
    state = PostsState()
    begin_event_request(state=state, user=attacker)

    try:
        # 3. Attacker attempts to load the private post for editing
        await state.load(private_post.pk)

        # 4. Assert that the operation was blocked and form inputs remained empty
        assert state.title == ""
        assert "not found" in state.posts_error.lower()

    finally:
        end_event_request(state)

5. Continuous Integration (CI) Workflow

To run your tests automatically on every code push or pull request, add the following workflow file to your repository:

# .github/workflows/test.yml
name: Testing Suite

on:
  push:
    branches: [ main, master ]
  pull_request:
    branches: [ main, master ]

jobs:
  pytest:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Code
        uses: actions/checkout@v4

      - name: Install uv Package Manager
        uses: astral-sh/setup-uv@v4
        with:
          enable-cache: true

      - name: Setup Python Environment
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install Project Dependencies
        run: uv sync --all-extras --dev

      - name: Execute Tests via Pytest
        run: uv run pytest --maxfail=3 --tb=short

6. Troubleshooting Common Testing Issues

Symptom Cause Solution
AppRegistryNotReady Django settings were imported after models or execution began. Ensure DJANGO_SETTINGS_MODULE is set at the absolute beginning of your conftest.py file.
SynchronousOnlyOperation A standard database query was executed within an async test runner. Prefix database queries with await and use async variants (acreate, aget, asave, etc.).
State variables do not reset between tests. The state instance persists across test cases. Re-instantiate the state class (state = MyState()) inside every individual test function.
self.request.user is AnonymousUser The event request context was not bound. Use the begin_event_request helper within your test block to bind a simulated user context.
fixture loop scope mismatch Pytest-asyncio is attempting to use a mismatched event loop scope. Configure asyncio_default_fixture_loop_scope = "function" inside your pyproject.toml settings.

Navigation: ← Command Line Interface | Next: Deployment Guide →