drf-openapi-tester


Namedrf-openapi-tester JSON
Version 2.1.1 PyPI version JSON
download
home_pagehttps://github.com/snok/drf-openapi-tester
SummaryTest utility for validating OpenAPI response documentation
upload_time2022-06-28 13:35:52
maintainerNa'aman Hirschfeld
docs_urlNone
authorSondre Lillebø Gundersen
requires_python>=3.7,<4.0
licenseBSD-4-Clause
keywords openapi swagger api testing schema django drf
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            [![PyPI](https://img.shields.io/pypi/v/drf-openapi-tester.svg)](https://pypi.org/project/drf-openapi-tester/)
[![Coverage](https://codecov.io/gh/snok/drf-openapi-tester/branch/master/graph/badge.svg)](https://codecov.io/gh/snok/drf-openapi-tester)
[![Python versions](https://img.shields.io/badge/Python-3.7%2B-blue)](https://pypi.org/project/drf-openapi-tester/)
[![Django versions](https://img.shields.io/badge/Django-3.0%2B-blue)](https://pypi.org/project/drf-openapi-tester/)


# DRF OpenAPI Tester

This is a test utility to validate DRF Test Responses against OpenAPI 2 and 3 schema. It has built-in support for:

- OpenAPI 2/3 yaml or json schema files.
- OpenAPI 2 schemas created with [drf-yasg](https://github.com/axnsan12/drf-yasg).
- OpenAPI 3 schemas created with [drf-spectacular](https://github.com/tfranzel/drf-spectacular).

## Installation

```shell script
pip install drf-openapi-tester
```

## Usage

First 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 though, you will need to pass the file path to
the tester:

```python
from openapi_tester import SchemaTester

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


```

Once you instantiate a tester, you can use it to test responses:

```python
from openapi_tester.schema_tester import SchemaTester

# you need to create at least one instance of SchemaTester.
# you can pass kwargs to it
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)
```

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 casings 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 of course pass your own custom case tester.

### ignore_case

List of keys to ignore when testing key casing. 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 cases 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 runs it through
[OpenAPI Spec validator](https://github.com/p1c2u/openapi-spec-validator) which validates that the schema passes without
specification compliance issues. 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 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 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.

## 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. If you encounter
  issues, [please document them here](https://github.com/snok/drf-openapi-tester/issues/205).

## Contributing

Contributions are welcome. Please see the [contributing guide](https://github.com/snok/.github/blob/main/CONTRIBUTING.md)

            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/snok/drf-openapi-tester",
    "name": "drf-openapi-tester",
    "maintainer": "Na'aman Hirschfeld",
    "docs_url": null,
    "requires_python": ">=3.7,<4.0",
    "maintainer_email": "nhirschfeld@gmail.com",
    "keywords": "openapi,swagger,api,testing,schema,django,drf",
    "author": "Sondre Lilleb\u00f8 Gundersen",
    "author_email": "sondrelg@live.no",
    "download_url": "https://files.pythonhosted.org/packages/4e/2f/acb3e19327308e28115e598a28b4c918626f194eb11ff78844c45f581a50/drf-openapi-tester-2.1.1.tar.gz",
    "platform": null,
    "description": "[![PyPI](https://img.shields.io/pypi/v/drf-openapi-tester.svg)](https://pypi.org/project/drf-openapi-tester/)\n[![Coverage](https://codecov.io/gh/snok/drf-openapi-tester/branch/master/graph/badge.svg)](https://codecov.io/gh/snok/drf-openapi-tester)\n[![Python versions](https://img.shields.io/badge/Python-3.7%2B-blue)](https://pypi.org/project/drf-openapi-tester/)\n[![Django versions](https://img.shields.io/badge/Django-3.0%2B-blue)](https://pypi.org/project/drf-openapi-tester/)\n\n\n# DRF OpenAPI Tester\n\nThis is a test utility to validate DRF Test Responses against OpenAPI 2 and 3 schema. It has built-in support for:\n\n- OpenAPI 2/3 yaml or json schema files.\n- OpenAPI 2 schemas created with [drf-yasg](https://github.com/axnsan12/drf-yasg).\n- OpenAPI 3 schemas created with [drf-spectacular](https://github.com/tfranzel/drf-spectacular).\n\n## Installation\n\n```shell script\npip install drf-openapi-tester\n```\n\n## Usage\n\nFirst instantiate one or more instances of SchemaTester:\n\n```python\nfrom openapi_tester import SchemaTester\n\nschema_tester = SchemaTester()\n\n\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. If you are using schema files though, you will need to pass the file path to\nthe tester:\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\n```\n\nOnce you instantiate a tester, you can use it to test responses:\n\n```python\nfrom openapi_tester.schema_tester import SchemaTester\n\n# you need to create at least one instance of SchemaTester.\n# you can pass kwargs to it\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\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 casings 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 of course pass your own custom case tester.\n\n### ignore_case\n\nList of keys to ignore when testing key casing. 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 cases 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 runs it through\n[OpenAPI Spec validator](https://github.com/p1c2u/openapi-spec-validator) which validates that the schema passes without\nspecification compliance issues. In case of issues with the schema itself, the validator will raise the appropriate\nerror.\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 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 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## 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. If you encounter\n  issues, [please document them here](https://github.com/snok/drf-openapi-tester/issues/205).\n\n## Contributing\n\nContributions are welcome. Please see the [contributing guide](https://github.com/snok/.github/blob/main/CONTRIBUTING.md)\n",
    "bugtrack_url": null,
    "license": "BSD-4-Clause",
    "summary": "Test utility for validating OpenAPI response documentation",
    "version": "2.1.1",
    "split_keywords": [
        "openapi",
        "swagger",
        "api",
        "testing",
        "schema",
        "django",
        "drf"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "md5": "2299652c7341e1fdb51dfcdee7ba2f48",
                "sha256": "a5a89b9763e3701dc0d9450a15d3d484d4b2668c676d36582aadf6e1fb1bbe55"
            },
            "downloads": -1,
            "filename": "drf_openapi_tester-2.1.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "2299652c7341e1fdb51dfcdee7ba2f48",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7,<4.0",
            "size": 19401,
            "upload_time": "2022-06-28T13:35:54",
            "upload_time_iso_8601": "2022-06-28T13:35:54.131976Z",
            "url": "https://files.pythonhosted.org/packages/5d/fe/4790b3fbe92bb21d203d7c6ea1d1a736e4ed967ef8c1a363ca3b54db7c4c/drf_openapi_tester-2.1.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "md5": "f3719e170a25c4abffd2e41522382c1b",
                "sha256": "cf1eb227fad547b52ed556e4eb9cc824ef52a930e22dad489237dd7e7bbb0a7c"
            },
            "downloads": -1,
            "filename": "drf-openapi-tester-2.1.1.tar.gz",
            "has_sig": false,
            "md5_digest": "f3719e170a25c4abffd2e41522382c1b",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7,<4.0",
            "size": 19700,
            "upload_time": "2022-06-28T13:35:52",
            "upload_time_iso_8601": "2022-06-28T13:35:52.413184Z",
            "url": "https://files.pythonhosted.org/packages/4e/2f/acb3e19327308e28115e598a28b4c918626f194eb11ff78844c45f581a50/drf-openapi-tester-2.1.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2022-06-28 13:35:52",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "github_user": "snok",
    "github_project": "drf-openapi-tester",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "drf-openapi-tester"
}
        
Elapsed time: 0.38296s