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:
- API behavior test:
apps/<app_name>/tests/test_apis/test_<api_name>.py - 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:
- Creates/updates API tests in
apps/<app_name>/tests/test_apis/test_<api_name>.py. - Creates/updates router tests in
apps/<app_name>/tests/test_views.py. - 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:
Run only one API test file while iterating:
Run only router tests:
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:
Possible responses:
- Returns
SimpleTestCaseassertion helper instance.
2. Mock App¶
Create isolated temporary Django apps for test runtime.
Usage:
Parameters:
app_name(str | None, default=None): Optional app label. Auto-generated when omitted.is_return_path(bool, default=False): WhenTrue, returns both app name and temp directory path.
Possible responses:
- Returns
strapp name whenis_return_path=False. - Returns
(str, pathlib.Path)whenis_return_path=True.
Notes:
- Registers app in
INSTALLED_APPSand 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 withModelwhen 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:
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:
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:
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(usequery_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
Nonewhen assertions pass.
8. Mock User¶
Create or reuse user fixtures for authenticated API tests.
Usage:
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
usernamealready exists. - Returns newly created user otherwise.
MindoffRouterTestCase is available for explicit router/version assertions in test_views.py.
Practical TDD Loop¶
- Write one behavior-focused test in
test_<api_name>.py. - Arrange fixtures with
mo_mock_user()and model helpers as needed. - Call the endpoint using
mo_mock_call_api(...). - Assert envelope and status with
mo_assert_api_response(...). - Add endpoint-specific assertions for response
data, side effects, and error branches. - Use
pytest.mark.parametrizefor similar scenarios to avoid repetitive test code and keep test files concise. - Repeat for edge cases: invalid payload, missing auth, and boundary limits.
Troubleshooting¶
NoReverseMatchforapi_url_name
Confirmapi_url_namematches the route name inapps/<app_name>/urls.py.- Test expects direct response but receives queue payload
Verifyis_queue_responsematches APIprocess_mode. - Failing auth/permission assertions
Ensure test user and headers align with APIauthentication_classesandpermission_classes. - Version routing failures in
test_views.py
CheckVERSION_MAPentries inapps/<app_name>/views.py.