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

Commit 2e37b9c

Browse files
authored
Merge pull request #262 from skarzi/issue-261
feat: Add custom django testing client
2 parents 7949267 + 6491cde commit 2e37b9c

File tree

4 files changed

+215
-7
lines changed

4 files changed

+215
-7
lines changed

README.md

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,10 @@ def test_response_documentation(client):
6363
If you are using the Django testing framework, you can create a base APITestCase that incorporates schema validation:
6464

6565
```python
66-
from openapi_tester.schema_tester import SchemaTester
67-
from rest_framework.test import APITestCase
6866
from rest_framework.response import Response
67+
from rest_framework.test import APITestCase
68+
69+
from openapi_tester.schema_tester import SchemaTester
6970

7071
schema_tester = SchemaTester()
7172

@@ -189,6 +190,68 @@ When the SchemaTester loads a schema, it runs it through
189190
specification compliance issues. In case of issues with the schema itself, the validator will raise the appropriate
190191
error.
191192

193+
## Django testing client
194+
195+
The library includes an `OpenAPIClient`, which extends Django REST framework's
196+
[`APIClient` class](https://www.django-rest-framework.org/api-guide/testing/#apiclient).
197+
If you wish to validate each response against OpenAPI schema when writing
198+
unit tests - `OpenAPIClient` is what you need!
199+
200+
To use `OpenAPIClient` simply pass `SchemaTester` instance that should be used
201+
to validate responses and then use it like regular Django testing client:
202+
203+
```python
204+
schema_tester = SchemaTester()
205+
client = OpenAPIClient(schema_tester=schema_tester)
206+
response = client.get('/api/v1/tests/123/')
207+
```
208+
209+
To force all developers working on the project to use `OpenAPIClient` simply
210+
override the `client` fixture (when using `pytest` with `pytest-django`):
211+
212+
```python
213+
from pytest_django.lazy_django import skip_if_no_django
214+
215+
from openapi_tester.schema_tester import SchemaTester
216+
217+
218+
@pytest.fixture
219+
def schema_tester():
220+
return SchemaTester()
221+
222+
223+
@pytest.fixture
224+
def client(schema_tester):
225+
skip_if_no_django()
226+
227+
from openapi_tester.clients import OpenAPIClient
228+
229+
return OpenAPIClient(schema_tester=schema_tester)
230+
```
231+
232+
If you are using plain Django test framework, we suggest to create custom
233+
test case implementation and use it instead of original Django one:
234+
235+
```python
236+
import functools
237+
238+
from django.test.testcases import SimpleTestCase
239+
from openapi_tester.clients import OpenAPIClient
240+
from openapi_tester.schema_tester import SchemaTester
241+
242+
schema_tester = SchemaTester()
243+
244+
245+
class MySimpleTestCase(SimpleTestCase):
246+
client_class = OpenAPIClient
247+
# or use `functools.partial` when you want to provide custom
248+
# ``SchemaTester`` instance:
249+
# client_class = functools.partial(OpenAPIClient, schema_tester=schema_tester)
250+
```
251+
252+
This will ensure you all newly implemented views will be validated against
253+
the OpenAPI schema.
254+
192255
## Known Issues
193256

194257
* 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: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
from .schema_tester import SchemaTester
9+
10+
if TYPE_CHECKING:
11+
from rest_framework.response import Response
12+
13+
14+
class OpenAPIClient(APIClient):
15+
"""``APIClient`` validating responses against OpenAPI schema."""
16+
17+
def __init__(
18+
self,
19+
*args,
20+
schema_tester: SchemaTester | None = None,
21+
**kwargs,
22+
) -> None:
23+
"""Initialize ``OpenAPIClient`` instance."""
24+
super().__init__(*args, **kwargs)
25+
self.schema_tester = schema_tester or self._schema_tester_factory()
26+
27+
def request(self, **kwargs) -> Response: # type: ignore[override]
28+
"""Validate fetched response against given OpenAPI schema."""
29+
response = super().request(**kwargs)
30+
self.schema_tester.validate_response(response)
31+
return response
32+
33+
@staticmethod
34+
def _schema_tester_factory() -> SchemaTester:
35+
"""Factory of default ``SchemaTester`` instances."""
36+
return SchemaTester()

tests/test_clients.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import functools
2+
import json
3+
4+
import pytest
5+
from django.test.testcases import SimpleTestCase
6+
from rest_framework import status
7+
8+
from openapi_tester.clients import OpenAPIClient
9+
from openapi_tester.exceptions import UndocumentedSchemaSectionError
10+
from openapi_tester.schema_tester import SchemaTester
11+
12+
13+
@pytest.fixture()
14+
def openapi_client(settings) -> OpenAPIClient:
15+
"""Sample ``OpenAPIClient`` instance to use in tests."""
16+
# use `drf-yasg` schema loader in tests
17+
settings.INSTALLED_APPS = [app for app in settings.INSTALLED_APPS if app != "drf_spectacular"]
18+
return OpenAPIClient()
19+
20+
21+
def test_init_schema_tester_passed():
22+
"""Ensure passed ``SchemaTester`` instance is used."""
23+
schema_tester = SchemaTester()
24+
25+
client = OpenAPIClient(schema_tester=schema_tester)
26+
27+
assert client.schema_tester is schema_tester
28+
29+
30+
@pytest.mark.parametrize(
31+
("generic_kwargs", "expected_status_code"),
32+
[
33+
(
34+
{"method": "GET", "path": "/api/v1/cars/correct"},
35+
status.HTTP_200_OK,
36+
),
37+
(
38+
{
39+
"method": "POST",
40+
"path": "/api/v1/vehicles",
41+
"data": json.dumps({"vehicle_type": "suv"}),
42+
"content_type": "application/json",
43+
},
44+
status.HTTP_201_CREATED,
45+
),
46+
],
47+
)
48+
def test_request(openapi_client, generic_kwargs, expected_status_code):
49+
"""Ensure ``SchemaTester`` doesn't raise exception when response valid."""
50+
response = openapi_client.generic(**generic_kwargs)
51+
52+
assert response.status_code == expected_status_code
53+
54+
55+
@pytest.mark.parametrize(
56+
("generic_kwargs", "raises_kwargs"),
57+
[
58+
(
59+
{
60+
"method": "POST",
61+
"path": "/api/v1/vehicles",
62+
"data": json.dumps({"vehicle_type": ("1" * 50)}),
63+
"content_type": "application/json",
64+
},
65+
{
66+
"expected_exception": UndocumentedSchemaSectionError,
67+
"match": "Undocumented status code: 400",
68+
},
69+
),
70+
(
71+
{"method": "PUT", "path": "/api/v1/animals"},
72+
{
73+
"expected_exception": UndocumentedSchemaSectionError,
74+
"match": "Undocumented method: put",
75+
},
76+
),
77+
],
78+
)
79+
def test_request_invalid_response(
80+
openapi_client,
81+
generic_kwargs,
82+
raises_kwargs,
83+
):
84+
"""Ensure ``SchemaTester`` raises an exception when response is invalid."""
85+
with pytest.raises(**raises_kwargs): # noqa: PT010
86+
openapi_client.generic(**generic_kwargs)
87+
88+
89+
@pytest.mark.parametrize(
90+
"openapi_client_class",
91+
[
92+
OpenAPIClient,
93+
functools.partial(OpenAPIClient, schema_tester=SchemaTester()),
94+
],
95+
)
96+
def test_django_testcase_client_class(openapi_client_class):
97+
"""Ensure example from README.md about Django test case works fine."""
98+
99+
class DummyTestCase(SimpleTestCase):
100+
"""Django ``TestCase`` with ``OpenAPIClient`` client."""
101+
102+
client_class = openapi_client_class
103+
104+
test_case = DummyTestCase()
105+
test_case._pre_setup()
106+
107+
assert isinstance(test_case.client, OpenAPIClient)

0 commit comments

Comments
 (0)