snok / drf-openapi-tester

Test utility for validating OpenAPI documentation
https://github.com/snok/django-openapi-tester
BSD 3-Clause "New" or "Revised" License
119 stars 23 forks source link

custom testing API client implementation #261

Closed skarzi closed 2 years ago

skarzi commented 2 years ago

Hi :wave:

First of all big thanks for developing this great package! I have been using drf-openapi-tester in a few Django-based projects and it works like a charm, however, usually, I'm adding the same code to every project - rest_framework.client.APIClient subclass that simply validates response against OpenAPI description using SchemaTester. What do you think about adding such a client to drf-openapi-tester package?

sondrelg commented 2 years ago

Hi!

That sounds interesting. How would it look?

skarzi commented 2 years ago

Something like this works for me nicely:

from typing import Optional

from django.conf import settings

from openapi_tester.schema_tester import SchemaTester
from rest_framework.response import Response
from rest_framework.test import APIClient

class OpenAPIClient(APIClient):
    """``APIClient`` validating responses against OpenAPI description."""

    def __init__(
        self,
        *args,
        openapi_description_tester: Optional[SchemaTester] = None,
        **kwargs,
    ) -> None:
        """Initialize ``OpenAPIClient`` instance."""
        super().__init__(*args, **kwargs)
        self.openapi_description_tester = (
            openapi_description_tester
            or self._default_openapi_description_tester_factory()
        )

    def request(self, **kwargs) -> Response:  # type: ignore[override]
        """Validate fetched response against given OpenAPI description."""
        response = super().request(**kwargs)
        self.openapi_description_tester.validate_response(response)
        return response

    def _default_openapi_description_tester_factory(self) -> SchemaTester:
        """Initialize default ``SchemaTester`` instance."""
        return SchemaTester(
            schema_file_path=settings.OPENAPI_DESCRIPTION_FILEPATH,
        )

Of course, we can make it more generic by requiring passing openapi_description_tester when initializing this client.

sondrelg commented 2 years ago

Just so I completely understand then, using this in tests would look something like this?

schema_tester = SchemaTester()
client = OpenAPIClient(tester=schema_tester)
response = client.request('GET', '/api/v1/test')

instead of

schema_tester = SchemaTester()
response = client.get('/api/v1/test')  # 'client' if included as a fixture in pytest tests, 'self.client' in TestCase
schema_tester.validate_response(response=response)

This seems equally verbose. Are there specific cases where you would save lines of code, or make things less complex by (re-)using the client instead of the schema tester instance directly? 🙂

skarzi commented 2 years ago

Yes, however, you can still use client.get(...) etc, because these methods use request under the hood. Additionally, you can enforce all developers working on the project to use OpenAPIClient by simply overriding the client fixture (or defining and using a new, custom fixture e.g. openapi_client), then you will be sure all newly implemented views will be validated against the OpenAPI description. So then it will look like a regular view test:

response = client.get('api/v1/test')

or when using openapi_client fixture instead of overriding client:

response = openapi_client.get('api/v1/test')
sondrelg commented 2 years ago

Neat! I'd be happy to accept a PR for this then 🙂

One tiny nitpick for the implementation: I think we should try to use "openapi schema" instead of "openapi description" to be consistent with how we've otherwise referenced it in the package.

I think documenting exactly what you wrote above is also very valuable if you're up for adding that to the readme 👍

skarzi commented 2 years ago

great, so I will prepare PR in the following days! Sure I will use "schema" instead of "description". BTW I'm using "description" mostly because of this glossary.