Pytest Testing Conventions and Fixtures in the Plane Django API: A Complete Guide

The Plane Django API uses pytest-django with session-scoped fixtures defined in conftest.py to provide database isolation, authenticated API clients, and mocked external services across unit, contract, and integration test layers.

The open-source Plane project (makeplane/plane) implements a robust testing strategy for its Django backend using pytest. Understanding these pytest testing conventions and fixtures is essential for contributors working on the API layer, as they ensure isolated, deterministic tests that run efficiently in Docker-based CI pipelines.

Test Organization Structure

The Plane repository organizes tests into three distinct layers under apps/api/plane/tests/:

  • unit/ – Pure unit tests that avoid database interactions and external services.
  • contract/ – API contract tests that exercise HTTP endpoints using the Django test server.
  • integration/ – Higher-level integration tests covering complex workflows.

This separation ensures fast feedback from unit tests while maintaining confidence in API contracts through dedicated HTTP tests.

Pytest Configuration and Test Runner

Configuration lives in apps/api/pytest.ini, which configures the Django settings module and enables pytest-django markers. The test runner executes through Docker Compose:

docker compose -f docker-compose-test.yml pytest

The pytest.ini file points to the test settings module and configures markers used throughout the suite.

Core Fixtures in conftest.py

The apps/api/plane/tests/conftest.py file defines session-scoped and function-scoped fixtures that provide the testing infrastructure.

Database and Live Server Setup

The django_db_setup fixture (imported from pytest_django.fixtures) initializes the test database once per session. The live_server fixture runs a transient Django server on a random port for contract tests:


# apps/api/plane/tests/conftest.py

import pytest
from rest_framework.test import APIClient

@pytest.fixture(scope="session")
def django_db_setup():
    """Initialize the Django test DB (executed once per session)."""
    # pytest-django handles DB creation automatically.

    pass

@pytest.fixture
def api_client(live_server):
    """Return an APIClient that talks to the live Django test server."""
    client = APIClient()
    client.defaults["SERVER_NAME"] = live_server.host
    client.defaults["SERVER_PORT"] = live_server.port
    return client

API Client and Authentication Fixtures

Authentication fixtures generate JWT tokens and provide ready-to-use headers:


# apps/api/plane/tests/conftest.py

from plane.tests.factories import UserFactory

@pytest.fixture
def create_user():
    """Create and return a regular user."""
    def _create(**kwargs):
        return UserFactory(**kwargs)
    return _create

@pytest.fixture
def admin_user():
    """Create a super-user."""
    return UserFactory(is_staff=True, is_superuser=True)

@pytest.fixture
def auth_headers(create_user):
    """Headers containing a JWT for a freshly created user."""
    user = create_user()
    # Token generation lives in plane.utils.auth

    token = user.get_jwt()
    return {"Authorization": f"Bearer {token}"}

Factory-Based Model Fixtures

Plane uses factory-boy for model factories. The UserFactory and ProjectFactory create persisted model instances without manual boilerplate:


# apps/api/plane/tests/conftest.py

from plane.tests.factories import UserFactory, ProjectFactory

@pytest.fixture
def user_factory():
    """Expose UserFactory as a fixture."""
    return UserFactory

@pytest.fixture
def project_factory():
    """Expose ProjectFactory as a fixture."""
    return ProjectFactory

Mocking External Services

External dependencies are isolated in apps/api/plane/tests/conftest_external.py. These fixtures patch Redis, Elasticsearch, Celery, and S3 with MagicMock objects:


# apps/api/plane/tests/conftest_external.py

import pytest
from unittest.mock import MagicMock

@pytest.fixture
def label_service_mock():
    """Mock the label service to avoid external calls."""
    mock = MagicMock()
    mock.create.return_value = {"id": "lbl_1", "name": "Bug"}
    return mock

Tests import these mocks to verify interactions without network overhead.

Writing Contract Tests with Fixtures

Contract tests combine the api_client and auth_headers fixtures to verify API behavior. In apps/api/plane/tests/contract/api/test_projects.py:

def test_create_project(api_client, auth_headers):
    payload = {"name": "New Project", "description": "Demo"}
    
    response = api_client.post(
        "/api/projects/",
        data=payload,
        format="json",
        **auth_headers,
    )
    
    assert response.status_code == 201
    assert response.data["name"] == "New Project"

The api_client provides a live server-aware HTTP client, while auth_headers injects a valid JWT. The database transaction rolls back automatically after the test completes.

Parametrized Test Patterns

Plane uses @pytest.mark.parametrize for testing multiple scenarios without duplicate code:

@pytest.mark.parametrize(
    "status,expected", [("open", 200), ("archived", 403)]
)
def test_project_access(api_client, auth_headers, create_user, status, expected):
    user = create_user()
    proj = ProjectFactory(owner=user, is_archived=(status == "archived"))
    resp = api_client.get(f"/api/projects/{proj.id}/", **auth_headers)
    assert resp.status_code == expected

Mocking Example in Label Tests

When testing services that interact with external storage, use the mock fixtures:


# apps/api/plane/tests/contract/api/test_labels.py

def test_create_label(label_service_mock, api_client, auth_headers):
    label_service_mock.create.return_value = {"id": "lbl_1", "name": "Bug"}
    
    payload = {"name": "Bug", "color": "#ff0000"}
    resp = api_client.post("/api/labels/", payload, **auth_headers)
    
    assert resp.status_code == 201
    label_service_mock.create.assert_called_once_with(payload)

Summary

  • Test organization splits tests into unit/, contract/, and integration/ directories under apps/api/plane/tests/.
  • Session-scoped fixtures in conftest.py provide django_db_setup, live_server, and api_client for efficient test isolation.
  • Authentication fixtures like auth_headers and create_user handle JWT generation and user creation via factory-boy.
  • External mocks in conftest_external.py prevent tests from hitting Redis, Elasticsearch, Celery, or S3.
  • Contract tests use the api_client fixture to exercise live HTTP endpoints with automatic database rollback.

Frequently Asked Questions

How does Plane handle database isolation in pytest?

Plane relies on pytest-django's transactional fixtures. The django_db_setup fixture creates the test database once per session, while individual tests use the db fixture (or fixtures that depend on it) to wrap each test in a transaction that rolls back after completion. This ensures test isolation without recreating the database for every test.

What is the difference between unit and contract tests in Plane?

Unit tests in apps/api/plane/tests/unit/ verify individual model methods or utility functions without database access, as seen in apps/api/plane/tests/unit/models/test_issue_comment_modal.py. Contract tests in apps/api/plane/tests/contract/api/ use the api_client and live_server fixtures to make actual HTTP requests against the Django application, verifying that API endpoints return the expected status codes and response structures.

How do I authenticate API requests in Plane's pytest suite?

Import the auth_headers fixture into your test function. This fixture creates a user via create_user (which uses UserFactory), generates a JWT token using the get_jwt() method from plane.utils.auth, and returns a dictionary with the Authorization: Bearer <token> header. Pass this to api_client requests using the unpacking operator **auth_headers.

Where are external services like Redis and Celery mocked?

External service mocks reside in apps/api/plane/tests/conftest_external.py. These fixtures use unittest.mock.MagicMock to patch service clients before they are imported by the application code, ensuring that tests never attempt real network connections to Redis, Elasticsearch, Celery workers, or S3 storage.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →