Skip to content

Test-Driven Development

Use TDD to keep API behavior stable as features evolve.

Prerequisites

  • API Class is scaffolded and routed.
  • Expected response codes are defined using response_kit (see Responses).

Implementation

When you Create an API using the command python mindoff.py create, django-mindoff generates test scaffolding automatically:

  1. API behavior test: apps/<app_name>/tests/test_apis/test_<api_name>.py
  2. Router/version test: apps/<app_name>/tests/test_views.py

Use this scaffolding as your baseline and expand test coverage as endpoint logic evolves.

What the CLI Generates for Each API

python mindoff.py create (option 2 for api) wires test artifacts together:

  1. Creates/updates API tests in apps/<app_name>/tests/test_apis/test_<api_name>.py.
  2. Creates/updates router tests in apps/<app_name>/tests/test_views.py.
  3. Keeps test names aligned with generated route names in urls.py.

Where Tests Live

Mindoff API tests are organized by purpose:

  • Endpoint behavior tests in apps/<app_name>/tests/test_apis/
  • Router/version tests in apps/<app_name>/tests/test_views.py

Router tests are usually maintenance-free unless you customize version routing. Most of your TDD work happens inside test_apis/test_<api_name>.py.

1. Mindoff API Test Class

The generated class extends MindoffTestCase and is configured with core test inputs: identity, request parameters, and assertions. These inputs control API route identification, request setup, and response validation.

After you create an API, your apps/<app_name>/tests/test_apis/test_<api_name>.py file will include a class like this with the class name and api_url_name already filled in for your API. Override only the attributes and add test methods as needed for your endpoint behavior.

import pytest
from django_mindoff.components.tdd_kit import MindoffTestCase
from typing import Literal


@pytest.mark.django_db(transaction=True)
class TestSampleAPIView(MindoffTestCase):
    api_url_name = "{{API_URL_NAME}}"

    def test_acceptance_api_success(self):
        user: callable | None = self.mo_mock_user()
        payload: dict | list | None = None
        url_kwargs: dict | None = {"version": 1}
        query_params: dict | None = None
        headers: dict | None = None
        expected_status_code: int = 200
        expected_response_type: Literal["json", "plain", "html", "binary", "others"] = (
            "json"
        )

        response = self.mo_mock_call_api(
            self.api_url_name,
            user=user,
            payload=payload,
            url_kwargs=url_kwargs,
            query_params=query_params,
            headers=headers,
            is_queue_response=True,
        )
        self.mo_assert_api_response(
            api_url_name=self.api_url_name,
            response=response,
            expected_status_code=expected_status_code,
            expected_response_type=expected_response_type,
        )

Core Test Inputs

Input Purpose Typical values
api_url_name Route identifier used by test helpers. <app>__<api>
payload Request body for POST/PUT APIs. dict
query_params Query string values for GET or filters. dict or None
headers Request headers, auth, custom metadata. dict or None
url_kwargs URL kwargs such as version segments. {"version": 1}
expected_status_code HTTP status assertion target. 200, 400, 401
expected_response_type Response envelope type assertion. json, plain, html, binary, others
is_queue_response Queue-mode acknowledgment vs direct response. True or False

For direct APIs, set is_queue_response=False. For queue-mode APIs, set is_queue_response=True.

2. Example Usage

The key is consistent use of the same api_url_name in both mo_mock_call_api and mo_assert_api_response.

@pytest.mark.django_db(transaction=True)
class TestCreateOrderAPIView(MindoffTestCase):
    api_url_name = "orders__create_order" # Same as API Class

    def test_acceptance_api_success(self):
        user = self.mo_mock_user()
        payload = {
            "customer_id": "cst_123",
            "items": [
                {"sku": "SKU-001", "qty": 2},
                {"sku": "SKU-002", "qty": 1},
            ],
        }
        url_kwargs = {"version": 1}
        expected_status_code = 200
        expected_response_type = "json"

        response = self.mo_mock_call_api(
            self.api_url_name,
            user=user,
            payload=payload,
            url_kwargs=url_kwargs,
            is_queue_response=False,
        )
        self.mo_assert_api_response(
            api_url_name=self.api_url_name,
            response=response,
            expected_status_code=expected_status_code,
            expected_response_type=expected_response_type,
        )

3. Running Tests

From the project root:

pytest

Run only one API test file while iterating:

pytest apps/<app_name>/tests/test_apis/test_<api_name>.py -q

Run only router tests:

pytest apps/<app_name>/tests/test_views.py -q

Core Concepts

MindoffTestCase provides reusable helpers to keep tests readable and fast, part of which is used by the Mindoff API Test Classes

1. Asserts

Provide shared django.test.SimpleTestCase assertion helpers.

Usage:

self.asserts.assertEqual(actual, expected)
self.asserts.assertIn("ok", value)

Possible responses:

  • Returns SimpleTestCase assertion helper instance.

2. Mock App

Create isolated temporary Django apps for test runtime.

Usage:

app_name = self.mo_mock_app()
app_name, temp_dir = self.mo_mock_app(is_return_path=True)

Parameters:

  • app_name (str | None, default=None): Optional app label. Auto-generated when omitted.
  • is_return_path (bool, default=False): When True, returns both app name and temp directory path.

Possible responses:

  • Returns str app name when is_return_path=False.
  • Returns (str, pathlib.Path) when is_return_path=True.

Notes:

  • Registers app in INSTALLED_APPS and injects a temporary URLConf.
  • Automatically cleans modules, URL cache, and temp files at teardown.

3. Mock Model

Create dynamic Django models/tables for integration-style tests.

Usage:

OrderModel = self.mo_mock_model(
    model_name="OrderModel",
    table_name="orders",
    fields={"total": models.FloatField(default=0)},
)

Parameters:

  • model_name (str | None, default=None): Optional PascalCase model name. Must end with Model when provided.
  • app_name (str | None, default=None): Existing app label. Resolved/created dynamically when omitted.
  • table_name (str | None, default=None): Optional snake_case table suffix.
  • foreign_keys (list[tuple[str, str] | tuple[str, str, str]], default=[]): FK specifications: (target_app, target_model) or (target_app, target_model, "required"|"optional").
  • fields (dict, default={}): Extra model fields to add.
  • base_model (default=models.Model): Base class for generated model.

Possible responses:

  • Returns generated Django model class.

Notes:

  • Creates DB table on setup and drops it during teardown.

4. Mock Model Frms

Build model-to-Polars DataFrame fixtures from bakery-generated rows.

Usage:

model_frms = self.mo_mock_model_frms(
    [OrderModel, ItemModel],
    counts=[3, 5],
)

Parameters:

  • models (list[type]): Ordered model classes.
  • counts (list[int], default=[]): Row counts per model index.
  • exclude_columns (list[list[str]], default=[]): Per-model columns to remove.
  • modify (list[dict], default=[]): Per-model row mutation map: {row_index: {"column": value}}.
  • is_fk_as_id (bool, default=True): Convert FK values to FK ids.
  • is_enforce_db_column (bool, default=True): Use DB column names as DataFrame columns.
  • is_uuid_hex (bool, default=True): Generate UUID values as hex.

Possible responses:

  • Returns dict[type, polars.DataFrame].

5. Update Mock Model Frms

Update existing model DataFrame fixtures with fresh generated values.

Usage:

updated = self.mo_update_mock_model_frms(
    model_frms,
    keep_columns=[["id"]],
)

Parameters:

  • df_dict (dict[type, polars.DataFrame]): Existing model-frame map.
  • counts (list[int], default=[]): Generation counts per model index.
  • exclude_columns (list[list[str]], default=[]): Columns to drop.
  • modify (list[dict], default=[]): Per-model row mutation map.
  • keep_columns (list[list[str]], default=[]): Per-model columns preserved from replacement. PK/FK columns are always protected.
  • is_uuid_hex (bool, default=True): Generate UUID values as hex.

Possible responses:

  • Returns updated dict[type, polars.DataFrame].

6. Mock Call Api

Invoke APIs by URL name with automatic method/headers handling.

Usage:

response = self.mo_mock_call_api(
    "orders__create_order",
    payload={"item": "A"},
)

Parameters:

  • api_url_name (str): Named URL route to call.
  • user (default=None): Auth user to force-authenticate.
  • headers (dict | None, default=None): Extra request headers.
  • payload (list | dict | None, default=None): Request JSON body.
  • url_kwargs (dict | None, default=None): URL kwargs for reverse lookup.
  • query_params (dict | None, default=None): Query-string params.
  • **extra: Passed through to Django test client call.

Possible responses:

  • Returns raw Django/DRF response object.

Notes:

  • HTTP method is resolved from API class method.
  • GET/DELETE requests reject payload (use query_params).
  • Queue-mode APIs are forced to direct mode for deterministic tests.

7. Assert Api Response

Assert standardized API response contract and content type.

Usage:

self.mo_assert_api_response(
    api_url_name="orders__create_order",
    response=response,
    expected_response_type="json",
    expected_status_code=200,
)

Parameters:

  • api_url_name (str): URL name used in assertion messages.
  • response: Raw Django/DRF response object.
  • expected_response_type (Literal["json","plain","html","binary","others"], default="json"): Expected response media type category.
  • expected_status_code (int, default=200): Expected HTTP status code.

Possible responses:

  • Raises assertion error on status/content-type mismatch.
  • Returns None when assertions pass.

8. Mock User

Create or reuse user fixtures for authenticated API tests.

Usage:

user = self.mo_mock_user(username="demo", password="secret123")

Parameters:

  • username (default=None): Username to reuse/create.
  • password (str, default="password123"): Raw password to set.
  • **extra_fields: Additional user model fields for bakery creation.

Possible responses:

  • Returns existing user when username already exists.
  • Returns newly created user otherwise.

MindoffRouterTestCase is available for explicit router/version assertions in test_views.py.

Practical TDD Loop

  1. Write one behavior-focused test in test_<api_name>.py.
  2. Arrange fixtures with mo_mock_user() and model helpers as needed.
  3. Call the endpoint using mo_mock_call_api(...).
  4. Assert envelope and status with mo_assert_api_response(...).
  5. Add endpoint-specific assertions for response data, side effects, and error branches.
  6. Use pytest.mark.parametrize for similar scenarios to avoid repetitive test code and keep test files concise.
  7. Repeat for edge cases: invalid payload, missing auth, and boundary limits.

Troubleshooting

  • NoReverseMatch for api_url_name
    Confirm api_url_name matches the route name in apps/<app_name>/urls.py.
  • Test expects direct response but receives queue payload
    Verify is_queue_response matches API process_mode.
  • Failing auth/permission assertions
    Ensure test user and headers align with API authentication_classes and permission_classes.
  • Version routing failures in test_views.py
    Check VERSION_MAP entries in apps/<app_name>/views.py.