Skip to main content

PyPI Coverage Python versions Django versions OpenAPI versions

Django Contract Tester

This is a test utility to validate DRF (Django REST Framework) and Django Ninja test requests & responses against OpenAPI versions 2.x and 3.x schemas.

It has built-in support for OpenAPIversion 2.0, 3.0.x and 3.1.x, both yaml or json schema files.

Installation

pip install django-contract-tester

Django 6.0 requires Python 3.12 or newer.

Usage

Instantiate one or more instances of SchemaTester:

from openapi_tester import SchemaTester

schema_tester = SchemaTester()

If you are using either drf-yasg or drf-spectacular this will be auto-detected, and the schema will be loaded by the SchemaTester automatically.

If you are using schema files, you will need to pass the file path:

from openapi_tester import SchemaTester

# path should be a string
schema_tester = SchemaTester(schema_file_path="./schemas/publishedSpecs.yaml")

Once you've instantiated a tester, you can use it to test responses and request bodies:

from openapi_tester.schema_tester import SchemaTester

schema_tester = SchemaTester()


def test_response_documentation(client):
    response = client.get('api/v1/test/1')
    assert response.status_code == 200
    schema_tester.validate_response(response=response)


def test_request_documentation(client):
    response = client.get('api/v1/test/1')
    assert response.status_code == 200
    schema_tester.validate_request(response=response)

If you are using the Django testing framework, you can create a base APITestCase that incorporates schema validation:

from rest_framework.response import Response
from rest_framework.test import APITestCase

from openapi_tester.schema_tester import SchemaTester

schema_tester = SchemaTester()


class BaseAPITestCase(APITestCase):
    """ Base test class for api views including schema validation """

    @staticmethod
    def assertResponse(response: Response, **kwargs) -> None:
        """ helper to run validate_response and pass kwargs to it """
        schema_tester.validate_response(response=response, **kwargs)

Then use it in a test file:

from shared.testing import BaseAPITestCase


class MyAPITests(BaseAPITestCase):
    def test_some_view(self):
        response = self.client.get("...")
        self.assertResponse(response)

Options

You can pass options either globally, when instantiating a SchemaTester, or locally, when invoking validate_response:

from openapi_tester import SchemaTester, is_camel_case
from tests.utils import my_uuid_4_validator

schema_test_with_case_validation = SchemaTester(
    case_tester=is_camel_case,
    ignore_case=["IP"],
    validators=[my_uuid_4_validator]
)

Or

from openapi_tester import SchemaTester, is_camel_case
from tests.utils import my_uuid_4_validator

schema_tester = SchemaTester()


def my_test(client):
    response = client.get('api/v1/test/1')
    assert response.status_code == 200
    schema_tester.validate_response(
        response=response,
        case_tester=is_camel_case,
        ignore_case=["IP"],
        validators=[my_uuid_4_validator]
    )

case_tester

The case tester argument takes a callable that is used to validate the key case of both schemas and responses. If nothing is passed, case validation is skipped.

The library currently has 4 built-in case testers:

  • is_pascal_case
  • is_snake_case
  • is_camel_case
  • is_kebab_case

You can use one of these, or your own.

ignore_case

List of keys to ignore when testing key case. This setting only applies when case_tester is not None.

validators

List of custom validators. A validator is a function that receives two parameters: schema_section and data, and returns either an error message or None, e.g.:

from typing import Any, Optional
from uuid import UUID


def my_uuid_4_validator(schema_section: dict, data: Any) -> Optional[str]:
    schema_format = schema_section.get("format")
    if schema_format == "uuid4":
        try:
            result = UUID(data, version=4)
            if not str(result) == str(data):
                return f"Expected uuid4, but received {data}"
        except ValueError:
            return f"Expected uuid4, but received {data}"
    return None

field_key_map

You can pass an optional dictionary that maps custom url parameter names into values, for situations where this cannot be inferred by the DRF EndpointEnumerator. A concrete use case for this option is when the django i18n locale prefixes.

from openapi_tester import SchemaTester

schema_tester = SchemaTester(field_key_map={
  "language": "en",
})

Schema Validation

When the SchemaTester loads a schema, it parses it using an OpenAPI spec validator. This validates the schema. In case of issues with the schema itself, the validator will raise the appropriate error.

Django testing client

The library includes an OpenAPIClient, which extends Django REST framework's APIClient class. If you wish to validate each request and response against OpenAPI schema when writing unit tests - OpenAPIClient is what you need!

To use OpenAPIClient simply pass SchemaTester instance that should be used to validate requests and responses and then use it like regular Django testing client:

schema_tester = SchemaTester()
client = OpenAPIClient(schema_tester=schema_tester)
response = client.get('/api/v1/tests/123/')

To force all developers working on the project to use OpenAPIClient simply override the client fixture (when using pytest with pytest-django):

from pytest_django.lazy_django import skip_if_no_django

from openapi_tester.schema_tester import SchemaTester


@pytest.fixture
def schema_tester():
    return SchemaTester()


@pytest.fixture
def client(schema_tester):
    skip_if_no_django()

    from openapi_tester.clients import OpenAPIClient

    return OpenAPIClient(schema_tester=schema_tester)

If you are using plain Django test framework, we suggest to create custom test case implementation and use it instead of original Django one:

import functools

from django.test.testcases import SimpleTestCase
from openapi_tester.clients import OpenAPIClient
from openapi_tester.schema_tester import SchemaTester

schema_tester = SchemaTester()


class MySimpleTestCase(SimpleTestCase):
    client_class = OpenAPIClient
    # or use `functools.partial` when you want to provide custom
    # ``SchemaTester`` instance:
    # client_class = functools.partial(OpenAPIClient, schema_tester=schema_tester)

This will ensure you all newly implemented views will be validated against the OpenAPI schema.

It is worth noting that for the case of clients the request validation is only performed for successful response scenarios. This is to avoid having the package interfering with your functional negative test case suite. You can still use schema_tester.validate_request method separately for the negative cases in which you would like to validate the request as well.

Django Ninja Test Client

In case you are using Django Ninja and its corresponding test client, you can use the OpenAPINinjaClient, which extends from it, in the same way as the OpenAPIClient:

schema_tester = SchemaTester()
client = OpenAPINinjaClient(
        router_or_app=router,
        schema_tester=schema_tester,
    )
response = client.get('/api/v1/tests/123/')

Given that the Django Ninja test client works separately from the django url resolver, you can pass the path_prefix argument to the OpenAPINinjaClient to specify the prefix of the path that should be used to look into the OpenAPI schema.

client = OpenAPINinjaClient(
        router_or_app=router,
        path_prefix='/api/v1',
        schema_tester=schema_tester,
    )

Configuration

This package supports configuration through dedicated configuration files, allowing you to set validation behavior globally for your project. This allows you to disable/enable specific validations, which can be useful in case you have certain designs that are still in progress, or can't be changed for some specific reason

Methods

You can configure the library using either:

  1. .django-contract-tester
  2. pyproject.toml

If both files exist, .django-contract-tester takes precedence over configuration in pyproject.toml.

Structure

.django-contract-tester

Create a .django-contract-tester file in your project root with the following INI structure:

[django-contract-tester]
ignore_case = ID, API, URL

[django-contract-tester:validation]
request = true
response = true
types = true
formats = true
query_parameters = true
request_for_non_successful_responses = false
disabled_types = integer, array
disabled_formats = date-time, email
disabled_constraints = enum, pattern, minLength

pyproject.toml

Add under [tool.django-contract-tester] section:

  • ignore_case: List of keys to ignore when testing key case (only applies when case_tester is set)

Under [tool.django-contract-tester.validation], you can control various aspects of validation:

Master Switches (enable/disable entire validation categories):

  • request (default: true): Enable/disable request body validation
  • request_for_non_successful_responses (default: false): Enable request validation for non-successful responses (4xx, 5xx)
  • response (default: true): Enable/disable response body validation
  • types (default: true): Enable/disable all schema type validations (string, integer, array, etc.)
  • formats (default: true): Enable/disable all schema format validations (date-time, uuid, email, etc.)
  • query_parameters (default: true): Enable/disable query parameter validation

Granular Controls (disable specific types/formats/constraints):

  • disabled_types: List of OpenAPI types to skip validating (e.g., ["integer", "array"])
  • disabled_formats: List of OpenAPI formats to skip validating (e.g., ["date-time", "email"])
  • disabled_constraints: List of OpenAPI constraint keywords to skip validating (e.g., ["enum", "pattern", "minLength"])

Examples

.django-contract-tester

[django-contract-tester]
# Keys to ignore during case validation
ignore_case = ID, API, URL

[django-contract-tester:validation]
# Master switches
request = true
response = true
types = true
formats = true
query_parameters = true

# Disable validation for specific types
disabled_types = integer

# Disable validation for specific formats
disabled_formats = date-time, email

# Disable specific constraint validations
disabled_constraints = enum, pattern, minLength, maxLength, minimum, maximum, multipleOf

pyproject.toml

[tool.django-contract-tester]
# Keys to ignore during case validation
ignore_case = ["ID", "API", "URL"]

[tool.django-contract-tester.validation]
# Master switches
request = true
response = true
types = true
formats = true
query_parameters = true

# Disable validation for specific types
disabled_types = ["integer"]

# Disable validation for specific formats
disabled_formats = ["date-time", "email"]

# Disable specific constraint validations
disabled_constraints = [
    "enum",
    "pattern",
    "minLength",
    "maxLength",
    "minimum",
    "maximum",
    "multipleOf"
]

Disabling Validation for Endpoints or Paths

You can also exclude specific endpoints or entire API paths from validation by adding them to the excluded_endpoints option under the [tool.django-contract-tester.validation] section of your pyproject.toml, or to the corresponding section in a .django-contract-tester INI file. For example, if you have endpoints that are being built, changing rapidly, or not yet present in your OpenAPI specs.

.django-contract-tester

[django-contract-tester:validation]
excluded_endpoints = GET /api/pets/*, POST /api/orders, /api/health
# Exclude all GET requests to subpaths of /api/pets/, e.g. GET /api/pets/{id}
# Exclude the POST /api/orders endpoint
# Exclude all methods for /api/health

pyproject.toml

[tool.django-contract-tester.validation]
excluded_endpoints = [
    "GET /api/pets/*",      # Exclude all GET requests to subpaths of /api/pets/, e.g. "GET /api/pets/{id}"
    "POST /api/orders",     # Exclude the POST /api/orders endpoint
    "/api/health",          # Exclude all methods for /api/health
]

Known Issues

  • We are using prance as a schema resolver, and it has some issues with the resolution of (very) complex OpenAPI 2.0 schemas.

Contributing

Contributions are welcome. Please see the contributing guide

Metadata

Release files for django-contract-tester 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for django-contract-tester 2.0.0
File Size Uploaded
django_contract_tester-2.0.0.tar.gz 28.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-contract-tester 2.0.0
File Interpreter ABI Platform
django_contract_tester-2.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 58.6 kB

Release files / django_contract_tester-2.0.0.tar.gz

Download URL django_contract_tester-2.0.0.tar.gz
Size 28.1 kB
Tags Source
SHA-256 checksum
How to use checksums
9289436a10cfcbedf8b7a970f8245447b8c76dddc6b3226164bc9e0ecca9198a
BLAKE2b-256 checksum
How to use checksums
64f33345edd41a51fa94c8e626e604048b702aa27ddf038490c8da16093b58bc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / django_contract_tester-2.0.0-py3-none-any.whl

Download URL django_contract_tester-2.0.0-py3-none-any.whl
Size 30.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
10d69af9c432fcd2d6faca7acab482d188d60dad772263f2bf79556e49361a58
BLAKE2b-256 checksum
How to use checksums
5c6fdba63adf5b9de98497571a51ea35101badc113c34a860db69971e90d8a6e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 release files

1.8.2

2 release files

1.8.1

2 release files

1.8.0

2 release files

1.7.0

2 release files

1.6.1

2 release files

1.6.0

2 release files

1.5.3

2 release files

1.5.2

2 release files

1.5.0

2 release files

1.4.5

2 release files

1.4.4

2 release files

1.4.3

2 release files

1.4.1

2 release files

1.4.0

2 release files

1.3.2

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page