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 OpenAPI
version 2.0
, 3.0.x
and 3.1.x
, both yaml
or json
schema files.
pip install django-contract-tester
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)
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]
)
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.
List of keys to ignore when testing key case. This setting only applies when case_tester is not None
.
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
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",
})
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.
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.
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,
)
- We are using prance as a schema resolver, and it has some issues with the resolution of (very) complex OpenAPI 2.0 schemas.
Contributions are welcome. Please see the contributing guide