[![PyPI](https://img.shields.io/pypi/v/django-contract-tester.svg)](https://pypi.org/project/django-contract-tester/)
[![Coverage](https://codecov.io/gh/maticardenas/django-contract-tester/graph/badge.svg)](https://app.codecov.io/gh/maticardenas/django-contract-tester)
[![Python versions](https://img.shields.io/badge/Python-3.8%2B-blue)](https://pypi.org/project/django-contract-tester/)
[![Django versions](https://img.shields.io/badge/Django-3.2%2B-blue)](https://pypi.org/project/django-contract-tester/)
# 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 `OpenAPI`version `2.0`, `3.0.x` and `3.1.x`, both `yaml` or `json` schema files.
## Installation
```shell script
pip install django-contract-tester
```
## Usage
Instantiate one or more instances of `SchemaTester`:
```python
from openapi_tester import SchemaTester
schema_tester = SchemaTester()
```
If you are using either [drf-yasg](https://github.com/axnsan12/drf-yasg)
or [drf-spectacular](https://github.com/tfranzel/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:
```python
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:
```python
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:
```python
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:
```python
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`:
```python
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
```python
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.:
```python
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](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#language-prefix-in-url-patterns).
```python
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](https://github.com/p1c2u/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](https://www.django-rest-framework.org/api-guide/testing/#apiclient).
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:
```python
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`):
```python
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:
```python
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.
### Django Ninja Test Client
In case you are using `Django Ninja` and its corresponding [test client](https://github.com/vitalik/django-ninja/blob/master/ninja/testing/client.py#L159), you can use the `OpenAPINinjaClient`, which extends from it, in the same way as the `OpenAPIClient`:
```python
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.
```python
client = OpenAPINinjaClient(
router_or_app=router,
path_prefix='/api/v1',
schema_tester=schema_tester,
)
```
## Known Issues
* We are using [prance](https://github.com/jfinkhaeuser/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](https://github.com/maticardenas/django-contract-tester/blob/master/CONTRIBUTING.md)
Raw data
{
"_id": null,
"home_page": "https://github.com/maticardenas/django-contract-tester",
"name": "django-contract-tester",
"maintainer": null,
"docs_url": null,
"requires_python": "<4.0,>=3.8",
"maintainer_email": null,
"keywords": "openapi, swagger, api, testing, schema, django, drf",
"author": "Mat\u00edas C\u00e1rdenas",
"author_email": "cardenasmatias.1990@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/f6/33/d5ce870a64cd198f8a67b41b5d81cdf91246607fef5dd23e8a5578d34916/django_contract_tester-1.5.0.tar.gz",
"platform": null,
"description": "[![PyPI](https://img.shields.io/pypi/v/django-contract-tester.svg)](https://pypi.org/project/django-contract-tester/)\n[![Coverage](https://codecov.io/gh/maticardenas/django-contract-tester/graph/badge.svg)](https://app.codecov.io/gh/maticardenas/django-contract-tester)\n[![Python versions](https://img.shields.io/badge/Python-3.8%2B-blue)](https://pypi.org/project/django-contract-tester/)\n[![Django versions](https://img.shields.io/badge/Django-3.2%2B-blue)](https://pypi.org/project/django-contract-tester/)\n\n\n# Django Contract Tester\n\nThis 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.\n\nIt has built-in support for `OpenAPI`version `2.0`, `3.0.x` and `3.1.x`, both `yaml` or `json` schema files.\n\n## Installation\n\n```shell script\npip install django-contract-tester\n```\n\n## Usage\n\nInstantiate one or more instances of `SchemaTester`:\n\n```python\nfrom openapi_tester import SchemaTester\n\nschema_tester = SchemaTester()\n```\n\nIf you are using either [drf-yasg](https://github.com/axnsan12/drf-yasg)\nor [drf-spectacular](https://github.com/tfranzel/drf-spectacular) this will be auto-detected, and the schema will be\nloaded by the `SchemaTester` automatically.\n\nIf you are using schema files, you will need to pass the file path:\n\n```python\nfrom openapi_tester import SchemaTester\n\n# path should be a string\nschema_tester = SchemaTester(schema_file_path=\"./schemas/publishedSpecs.yaml\")\n```\n\nOnce you've instantiated a tester, you can use it to test responses and request bodies:\n\n```python\nfrom openapi_tester.schema_tester import SchemaTester\n\nschema_tester = SchemaTester()\n\n\ndef test_response_documentation(client):\n response = client.get('api/v1/test/1')\n assert response.status_code == 200\n schema_tester.validate_response(response=response)\n\n\ndef test_request_documentation(client):\n response = client.get('api/v1/test/1')\n assert response.status_code == 200\n schema_tester.validate_request(response=response)\n```\n\nIf you are using the Django testing framework, you can create a base `APITestCase` that incorporates schema validation:\n\n```python\nfrom rest_framework.response import Response\nfrom rest_framework.test import APITestCase\n\nfrom openapi_tester.schema_tester import SchemaTester\n\nschema_tester = SchemaTester()\n\n\nclass BaseAPITestCase(APITestCase):\n \"\"\" Base test class for api views including schema validation \"\"\"\n\n @staticmethod\n def assertResponse(response: Response, **kwargs) -> None:\n \"\"\" helper to run validate_response and pass kwargs to it \"\"\"\n schema_tester.validate_response(response=response, **kwargs)\n```\n\nThen use it in a test file:\n\n```python\nfrom shared.testing import BaseAPITestCase\n\n\nclass MyAPITests(BaseAPITestCase):\n def test_some_view(self):\n response = self.client.get(\"...\")\n self.assertResponse(response)\n```\n\n## Options\n\nYou can pass options either globally, when instantiating a `SchemaTester`, or locally, when\ninvoking `validate_response`:\n\n```python\nfrom openapi_tester import SchemaTester, is_camel_case\nfrom tests.utils import my_uuid_4_validator\n\nschema_test_with_case_validation = SchemaTester(\n case_tester=is_camel_case,\n ignore_case=[\"IP\"],\n validators=[my_uuid_4_validator]\n)\n\n```\n\nOr\n\n```python\nfrom openapi_tester import SchemaTester, is_camel_case\nfrom tests.utils import my_uuid_4_validator\n\nschema_tester = SchemaTester()\n\n\ndef my_test(client):\n response = client.get('api/v1/test/1')\n assert response.status_code == 200\n schema_tester.validate_response(\n response=response,\n case_tester=is_camel_case,\n ignore_case=[\"IP\"],\n validators=[my_uuid_4_validator]\n )\n```\n\n### case_tester\n\nThe case tester argument takes a callable that is used to validate the key case of both schemas and responses. If\nnothing is passed, case validation is skipped.\n\nThe library currently has 4 built-in case testers:\n\n- `is_pascal_case`\n- `is_snake_case`\n- `is_camel_case`\n- `is_kebab_case`\n\nYou can use one of these, or your own.\n\n### ignore_case\n\nList of keys to ignore when testing key case. This setting only applies when case_tester is not `None`.\n\n### validators\n\nList of custom validators. A validator is a function that receives two parameters: schema_section and data, and returns\neither an error message or `None`, e.g.:\n\n```python\nfrom typing import Any, Optional\nfrom uuid import UUID\n\n\ndef my_uuid_4_validator(schema_section: dict, data: Any) -> Optional[str]:\n schema_format = schema_section.get(\"format\")\n if schema_format == \"uuid4\":\n try:\n result = UUID(data, version=4)\n if not str(result) == str(data):\n return f\"Expected uuid4, but received {data}\"\n except ValueError:\n return f\"Expected uuid4, but received {data}\"\n return None\n```\n\n### field_key_map\n\nYou can pass an optional dictionary that maps custom url parameter names into values, for situations where this cannot be\ninferred by the DRF `EndpointEnumerator`. A concrete use case for this option is when\nthe [django i18n locale prefixes](https://docs.djangoproject.com/en/3.1/topics/i18n/translation/#language-prefix-in-url-patterns).\n\n```python\nfrom openapi_tester import SchemaTester\n\nschema_tester = SchemaTester(field_key_map={\n \"language\": \"en\",\n})\n```\n\n## Schema Validation\n\nWhen the SchemaTester loads a schema, it parses it using an\n[OpenAPI spec validator](https://github.com/p1c2u/openapi-spec-validator). This validates the schema.\nIn case of issues with the schema itself, the validator will raise the appropriate error.\n\n## Django testing client\n\nThe library includes an `OpenAPIClient`, which extends Django REST framework's\n[`APIClient` class](https://www.django-rest-framework.org/api-guide/testing/#apiclient).\nIf you wish to validate each request and response against OpenAPI schema when writing\nunit tests - `OpenAPIClient` is what you need!\n\nTo use `OpenAPIClient` simply pass `SchemaTester` instance that should be used\nto validate requests and responses and then use it like regular Django testing client:\n\n```python\nschema_tester = SchemaTester()\nclient = OpenAPIClient(schema_tester=schema_tester)\nresponse = client.get('/api/v1/tests/123/')\n```\n\nTo force all developers working on the project to use `OpenAPIClient` simply\noverride the `client` fixture (when using `pytest` with `pytest-django`):\n\n```python\nfrom pytest_django.lazy_django import skip_if_no_django\n\nfrom openapi_tester.schema_tester import SchemaTester\n\n\n@pytest.fixture\ndef schema_tester():\n return SchemaTester()\n\n\n@pytest.fixture\ndef client(schema_tester):\n skip_if_no_django()\n\n from openapi_tester.clients import OpenAPIClient\n\n return OpenAPIClient(schema_tester=schema_tester)\n```\n\nIf you are using plain Django test framework, we suggest to create custom\ntest case implementation and use it instead of original Django one:\n\n```python\nimport functools\n\nfrom django.test.testcases import SimpleTestCase\nfrom openapi_tester.clients import OpenAPIClient\nfrom openapi_tester.schema_tester import SchemaTester\n\nschema_tester = SchemaTester()\n\n\nclass MySimpleTestCase(SimpleTestCase):\n client_class = OpenAPIClient\n # or use `functools.partial` when you want to provide custom\n # ``SchemaTester`` instance:\n # client_class = functools.partial(OpenAPIClient, schema_tester=schema_tester)\n```\n\nThis will ensure you all newly implemented views will be validated against\nthe OpenAPI schema.\n\n\n### Django Ninja Test Client\n\nIn case you are using `Django Ninja` and its corresponding [test client](https://github.com/vitalik/django-ninja/blob/master/ninja/testing/client.py#L159), you can use the `OpenAPINinjaClient`, which extends from it, in the same way as the `OpenAPIClient`:\n\n```python\nschema_tester = SchemaTester()\nclient = OpenAPINinjaClient(\n router_or_app=router,\n schema_tester=schema_tester,\n )\nresponse = client.get('/api/v1/tests/123/')\n```\n\nGiven 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.\n\n```python\nclient = OpenAPINinjaClient(\n router_or_app=router,\n path_prefix='/api/v1',\n schema_tester=schema_tester,\n )\n```\n\n## Known Issues\n\n* We are using [prance](https://github.com/jfinkhaeuser/prance) as a schema resolver, and it has some issues with the\n resolution of (very) complex OpenAPI 2.0 schemas.\n\n## Contributing\n\nContributions are welcome. Please see the [contributing guide](https://github.com/maticardenas/django-contract-tester/blob/master/CONTRIBUTING.md)\n",
"bugtrack_url": null,
"license": "BSD-4-Clause",
"summary": "Test utility for validating OpenAPI response documentation",
"version": "1.5.0",
"project_urls": {
"Documentation": "https://github.com/maticardenas/django-contract-tester",
"Homepage": "https://github.com/maticardenas/django-contract-tester",
"Repository": "https://github.com/maticardenas/django-contract-tester"
},
"split_keywords": [
"openapi",
" swagger",
" api",
" testing",
" schema",
" django",
" drf"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "509825c7a5ddee60c8f5f519bc37dc4a6b7f487821c4e84ef53574cea5317772",
"md5": "edcd1bf73fb855f97255e0c6a2dd05d2",
"sha256": "8a54cc11d9a38678e984b6518a0bbff6407b030ec6eeb58f5d9f21d4ed218f3b"
},
"downloads": -1,
"filename": "django_contract_tester-1.5.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "edcd1bf73fb855f97255e0c6a2dd05d2",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": "<4.0,>=3.8",
"size": 24291,
"upload_time": "2024-10-19T10:33:58",
"upload_time_iso_8601": "2024-10-19T10:33:58.226486Z",
"url": "https://files.pythonhosted.org/packages/50/98/25c7a5ddee60c8f5f519bc37dc4a6b7f487821c4e84ef53574cea5317772/django_contract_tester-1.5.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "f633d5ce870a64cd198f8a67b41b5d81cdf91246607fef5dd23e8a5578d34916",
"md5": "09ad153820b22406feaa663df1457eb8",
"sha256": "e39881f0d0df16091591b7f7d20a78724c2bc0532f95a6cb7dd368ae4b8cc0bf"
},
"downloads": -1,
"filename": "django_contract_tester-1.5.0.tar.gz",
"has_sig": false,
"md5_digest": "09ad153820b22406feaa663df1457eb8",
"packagetype": "sdist",
"python_version": "source",
"requires_python": "<4.0,>=3.8",
"size": 22757,
"upload_time": "2024-10-19T10:34:00",
"upload_time_iso_8601": "2024-10-19T10:34:00.029299Z",
"url": "https://files.pythonhosted.org/packages/f6/33/d5ce870a64cd198f8a67b41b5d81cdf91246607fef5dd23e8a5578d34916/django_contract_tester-1.5.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-10-19 10:34:00",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "maticardenas",
"github_project": "django-contract-tester",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "django-contract-tester"
}