Skip to content
This repository was archived by the owner on Nov 19, 2023. It is now read-only.

Commit c50498d

Browse files
committed
feat: add custom django testing client
Introduce `OpenAPIClient` that extends `rest_framework.test.APIClient` with responses validation against OpenAPI schema by using `SchemaTester`.
1 parent d9f108d commit c50498d

File tree

4 files changed

+131
-5
lines changed

4 files changed

+131
-5
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,29 @@ When the SchemaTester loads a schema, it runs it through
192192
specification compliance issues. In case of issues with the schema itself, the validator will raise the appropriate
193193
error.
194194

195+
## Django testing client
196+
197+
`OpenAPIClient` extends Django REST framework
198+
[`APIClient` class](https://www.django-rest-framework.org/api-guide/testing/#apiclient).
199+
If you wish to validate each response against OpenAPI schema when writing
200+
unit tests - `OpenAPIClient` is what you need!
201+
202+
To use `OpenAPIClient` simply pass `SchemaTester` instance that should be used
203+
to validate responses and then use it like regular Django testing client
204+
(TIP: add custom fixture if you are using `pytest` to avoid code boilerplate):
205+
206+
```python
207+
schema_tester = SchemaTester()
208+
client = OpenAPIClient(schema_tester=schema_tester)
209+
response = client.get('/api/v1/tests/123/')
210+
```
211+
212+
To enforce all developers working on the project to use `OpenAPIClient` simply
213+
override the `client` fixture (when using `pytest-django`) or provide custom
214+
test cases implementation (when using standard Django `unitest`-based approach)
215+
and then you will be sure all newly implemented views will be validated against
216+
the OpenAPI schema.
217+
195218
## Known Issues
196219

197220
* We are using [prance](https://github.com/jfinkhaeuser/prance) as a schema resolver, and it has some issues with the

openapi_tester/__init__.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
""" DRF OpenAPI Schema Tester """
22
from .case_testers import is_camel_case, is_kebab_case, is_pascal_case, is_snake_case
3+
from .clients import OpenAPIClient
34
from .loaders import BaseSchemaLoader, DrfSpectacularSchemaLoader, DrfYasgSchemaLoader, StaticSchemaLoader
45
from .schema_tester import SchemaTester
56

67
__all__ = [
7-
"is_camel_case",
8-
"is_kebab_case",
9-
"is_pascal_case",
10-
"is_snake_case",
118
"BaseSchemaLoader",
129
"DrfSpectacularSchemaLoader",
1310
"DrfYasgSchemaLoader",
14-
"StaticSchemaLoader",
1511
"SchemaTester",
12+
"StaticSchemaLoader",
13+
"is_camel_case",
14+
"is_kebab_case",
15+
"is_pascal_case",
16+
"is_snake_case",
17+
"OpenAPIClient",
1618
]

openapi_tester/clients.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Subclass of ``APIClient`` using ``SchemaTester`` to validate responses."""
2+
from __future__ import annotations
3+
4+
from typing import TYPE_CHECKING
5+
6+
from rest_framework.test import APIClient
7+
8+
if TYPE_CHECKING:
9+
from rest_framework.response import Response
10+
11+
from .schema_tester import SchemaTester
12+
13+
14+
class OpenAPIClient(APIClient):
15+
"""``APIClient`` validating responses against OpenAPI schema."""
16+
17+
def __init__(self, *args, schema_tester: SchemaTester, **kwargs) -> None:
18+
"""Initialize ``OpenAPIClient`` instance."""
19+
super().__init__(*args, **kwargs)
20+
self.schema_tester = schema_tester
21+
22+
def request(self, **kwargs) -> Response: # type: ignore[override]
23+
"""Validate fetched response against given OpenAPI schema."""
24+
response = super().request(**kwargs)
25+
self.schema_tester.validate_response(response)
26+
return response

tests/test_clients.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import json
2+
3+
import pytest
4+
from rest_framework import status
5+
6+
from openapi_tester.clients import OpenAPIClient
7+
from openapi_tester.exceptions import UndocumentedSchemaSectionError
8+
from openapi_tester.schema_tester import SchemaTester
9+
10+
11+
@pytest.fixture()
12+
def openapi_client(settings) -> OpenAPIClient:
13+
"""Sample ``OpenAPIClient`` instance to use in tests."""
14+
# use `drf-yasg` schema loader in tests
15+
settings.INSTALLED_APPS = [app for app in settings.INSTALLED_APPS if app != "drf_spectacular"]
16+
return OpenAPIClient(schema_tester=SchemaTester())
17+
18+
19+
@pytest.mark.parametrize(
20+
("generic_kwargs", "expected_status_code"),
21+
[
22+
(
23+
{"method": "GET", "path": "/api/v1/cars/correct"},
24+
status.HTTP_200_OK,
25+
),
26+
(
27+
{
28+
"method": "POST",
29+
"path": "/api/v1/vehicles",
30+
"data": json.dumps({"vehicle_type": "suv"}),
31+
"content_type": "application/json",
32+
},
33+
status.HTTP_201_CREATED,
34+
),
35+
],
36+
)
37+
def test_request(openapi_client, generic_kwargs, expected_status_code):
38+
"""Ensure ``SchemaTester`` doesn't raise exception when response valid."""
39+
response = openapi_client.generic(**generic_kwargs)
40+
41+
assert response.status_code == expected_status_code
42+
43+
44+
@pytest.mark.parametrize(
45+
("generic_kwargs", "raises_kwargs"),
46+
[
47+
(
48+
{
49+
"method": "POST",
50+
"path": "/api/v1/vehicles",
51+
"data": json.dumps({"vehicle_type": ("1" * 50)}),
52+
"content_type": "application/json",
53+
},
54+
{
55+
"expected_exception": UndocumentedSchemaSectionError,
56+
"match": "Undocumented status code: 400",
57+
},
58+
),
59+
(
60+
{"method": "PUT", "path": "/api/v1/animals"},
61+
{
62+
"expected_exception": UndocumentedSchemaSectionError,
63+
"match": "Undocumented method: put",
64+
},
65+
),
66+
],
67+
)
68+
def test_request_invalid_response(
69+
openapi_client,
70+
generic_kwargs,
71+
raises_kwargs,
72+
):
73+
"""Ensure ``SchemaTester`` raises an exception when response is invalid."""
74+
with pytest.raises(**raises_kwargs): # noqa: PT010
75+
openapi_client.generic(**generic_kwargs)

0 commit comments

Comments
 (0)