Skip to content

API Action Tester

ApiActionTester

Class helper for testing api.

Source code in saritasa_drf_tools/testing/api_action_tester.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
class ApiActionTester[
    DjangoModel: django.db.models.Model,
    DjangoUserModel: django.contrib.auth.models.AbstractBaseUser,
    RestAPIView: rest_framework.generics.GenericAPIView,
]:
    """Class helper for testing api."""

    url_basename: str
    type factory_type = type[
        factory_boy.django.DjangoModelFactory[DjangoModel]  # type: ignore
    ]
    factory: factory_type
    type model_type = type[DjangoModel]  # type: ignore
    model: model_type
    type user_model_type = type[DjangoUserModel]  # type: ignore
    user_model: user_model_type
    type api_view_type = type[RestAPIView]  # type: ignore
    api_view: api_view_type
    capture_on_commit: pytest_django.DjangoCaptureOnCommitCallbacks = (
        django.test.TestCase.captureOnCommitCallbacks
    )

    @classmethod
    def init_subclass[
        DjangoModelInit: django.db.models.Model,
        DjangoUserModelInit: django.contrib.auth.models.AbstractBaseUser,
        RestAPIViewInit: rest_framework.generics.GenericAPIView,
    ](
        cls: type[
            """ApiActionTester[
                typing.Any,
                typing.Any,
                typing.Any
            ]
            """
        ],
        factory: type[factory_boy.django.DjangoModelFactory[DjangoModelInit]],
        model: type[DjangoModelInit],
        user_model: type[DjangoUserModelInit],
        api_view: type[RestAPIViewInit],
        url_basename: str,
        mixins: collections.abc.Sequence[type] = (),
    ) -> type[
        """ApiActionTester[
            DjangoModelInit,
            DjangoUserModelInit,
            RestAPIViewInit
        ]
        """
    ]:
        """Init subclass.

        A more simple and parametrized way to create sub class. It allows for
        more generics specification

        """
        cls_factory = factory
        cls_model = model
        cls_user_model = user_model
        cls_api_view = api_view

        class Tester(
            *mixins,  # type: ignore
            cls[  # type: ignore
                model,  # type: ignore
                user_model,  # type: ignore
                api_view,  # type: ignore
            ],
            url_basename=url_basename,
        ):
            factory = cls_factory
            model = cls_model
            user_model = cls_user_model
            api_view = cls_api_view

        return Tester

    def __init_subclass__(
        cls,
        url_basename: str | None = None,
    ) -> None:
        """Set up api action tester class."""
        if not url_basename:
            return
        cls.url_basename = url_basename

    def lazy_url(
        self,
        action: str = "",
        **kwargs,
    ) -> str:
        """Get lazy url to action."""
        viewname = "-".join(filter(None, (self.url_basename, action)))
        return django.urls.reverse_lazy(
            viewname=viewname,
            kwargs=kwargs,
        )

    def get_serializer(
        self,
        action: str,
    ) -> type[rest_framework.serializers.Serializer]:
        """Get serializer for api view."""
        return self.api_view(action=action).get_serializer_class()

    def serialize_data(
        self,
        action: str,
        data: DjangoModel | dict[str, typing.Any],
        many: bool = False,
    ) -> (
        rest_framework.serializers.ReturnDict
        | rest_framework.serializers.ReturnList
    ):
        """Serialize data by using view's action serializer."""
        return self.get_serializer(action=action)(
            instance=data,
            many=many,
        ).data

    def extract_errors_from_response(
        self,
        response: rest_framework.response.Response,
        field: str,
    ) -> list[str]:
        """Extract errors from response."""
        assert response.data  # noqa: S101
        assert field in response.data, response.data  # noqa: S101
        return response.data[field]

    def check_errors_from_response(
        self,
        response: rest_framework.response.Response,
        field: str,
        expected_errors: collections.abc.Sequence[str],
    ) -> None:
        """Extract errors and check that expected errors are present."""
        errors = set(
            self.extract_errors_from_response(
                response=response,
                field=field,
            ),
        )
        assert errors == set(expected_errors), errors ^ set(expected_errors)  # noqa: S101

    def invoke_factory(self, **kwargs) -> DjangoModel:
        """Generate instance."""
        return self.factory.create(**kwargs)

    def invoke_factory_build(self, **kwargs) -> DjangoModel:
        """Build instance."""
        return self.factory.build(**kwargs)

    def invoke_factory_batch(
        self,
        size: int = 5,
        **kwargs,
    ) -> list[DjangoModel]:
        """Generate instances."""
        return self.factory.create_batch(
            size=size,
            **kwargs,
        )  # type: ignore

    def get_api_client(self) -> rest_framework.test.APIClient:
        """Get api client for requests."""
        return rest_framework.test.APIClient()

    def make_request(
        self,
        method: http.HTTPMethod,
        path: str,
        data: dict[str, typing.Any] | list[typing.Any] | None = None,
        expected_status: int | None = None,
        api_client: rest_framework.test.APIClient | None = None,
        user: DjangoModel | None = None,
        capture_on_commit: bool = False,
        **kwargs,
    ) -> rest_framework.response.Response:
        """Make api request."""
        api_client = api_client or self.get_api_client()
        if user:
            api_client.force_authenticate(user)
        with self.capture_on_commit(execute=capture_on_commit):
            response: rest_framework.response.Response = getattr(
                api_client,
                method.lower(),
            )(
                path=path,
                data=data,
                **kwargs,
            )
        if not expected_status and method == http.HTTPMethod.POST.lower():
            expected_status = rest_framework.status.HTTP_201_CREATED
        if not expected_status and method == http.HTTPMethod.DELETE.lower():
            expected_status = rest_framework.status.HTTP_204_NO_CONTENT
        if not expected_status:
            expected_status = rest_framework.status.HTTP_200_OK
        assert response.status_code == expected_status, (  # noqa: S101
            response.status_code,
            response.data,
        )
        return response

    @pytest.fixture
    def instance_kwargs(self) -> dict[str, typing.Any]:
        """Get kwargs for instance generation."""
        return {}

    @pytest.fixture
    def instance(
        self,
        instance_kwargs: dict[str, typing.Any],
    ) -> DjangoModel:
        """Generate instance."""
        return self.factory(**instance_kwargs)  # type: ignore

    @pytest.fixture
    def instance_batch_kwargs(self) -> dict[str, typing.Any]:
        """Get kwargs for instance batch generation."""
        return {}

    @pytest.fixture
    def instance_batch(
        self,
        instance_batch_kwargs: dict[str, typing.Any],
    ) -> list[DjangoModel]:
        """Create instances batch for testing."""
        instance_batch_kwargs.setdefault("size", 5)
        return self.factory.create_batch(**instance_batch_kwargs)

__init_subclass__(url_basename=None)

Set up api action tester class.

Source code in saritasa_drf_tools/testing/api_action_tester.py
 96
 97
 98
 99
100
101
102
103
def __init_subclass__(
    cls,
    url_basename: str | None = None,
) -> None:
    """Set up api action tester class."""
    if not url_basename:
        return
    cls.url_basename = url_basename

check_errors_from_response(response, field, expected_errors)

Extract errors and check that expected errors are present.

Source code in saritasa_drf_tools/testing/api_action_tester.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def check_errors_from_response(
    self,
    response: rest_framework.response.Response,
    field: str,
    expected_errors: collections.abc.Sequence[str],
) -> None:
    """Extract errors and check that expected errors are present."""
    errors = set(
        self.extract_errors_from_response(
            response=response,
            field=field,
        ),
    )
    assert errors == set(expected_errors), errors ^ set(expected_errors)  # noqa: S101

extract_errors_from_response(response, field)

Extract errors from response.

Source code in saritasa_drf_tools/testing/api_action_tester.py
139
140
141
142
143
144
145
146
147
def extract_errors_from_response(
    self,
    response: rest_framework.response.Response,
    field: str,
) -> list[str]:
    """Extract errors from response."""
    assert response.data  # noqa: S101
    assert field in response.data, response.data  # noqa: S101
    return response.data[field]

get_api_client()

Get api client for requests.

Source code in saritasa_drf_tools/testing/api_action_tester.py
183
184
185
def get_api_client(self) -> rest_framework.test.APIClient:
    """Get api client for requests."""
    return rest_framework.test.APIClient()

get_serializer(action)

Get serializer for api view.

Source code in saritasa_drf_tools/testing/api_action_tester.py
117
118
119
120
121
122
def get_serializer(
    self,
    action: str,
) -> type[rest_framework.serializers.Serializer]:
    """Get serializer for api view."""
    return self.api_view(action=action).get_serializer_class()

init_subclass(factory, model, user_model, api_view, url_basename, mixins=()) classmethod

Init subclass.

A more simple and parametrized way to create sub class. It allows for more generics specification

Source code in saritasa_drf_tools/testing/api_action_tester.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@classmethod
def init_subclass[
    DjangoModelInit: django.db.models.Model,
    DjangoUserModelInit: django.contrib.auth.models.AbstractBaseUser,
    RestAPIViewInit: rest_framework.generics.GenericAPIView,
](
    cls: type[
        """ApiActionTester[
            typing.Any,
            typing.Any,
            typing.Any
        ]
        """
    ],
    factory: type[factory_boy.django.DjangoModelFactory[DjangoModelInit]],
    model: type[DjangoModelInit],
    user_model: type[DjangoUserModelInit],
    api_view: type[RestAPIViewInit],
    url_basename: str,
    mixins: collections.abc.Sequence[type] = (),
) -> type[
    """ApiActionTester[
        DjangoModelInit,
        DjangoUserModelInit,
        RestAPIViewInit
    ]
    """
]:
    """Init subclass.

    A more simple and parametrized way to create sub class. It allows for
    more generics specification

    """
    cls_factory = factory
    cls_model = model
    cls_user_model = user_model
    cls_api_view = api_view

    class Tester(
        *mixins,  # type: ignore
        cls[  # type: ignore
            model,  # type: ignore
            user_model,  # type: ignore
            api_view,  # type: ignore
        ],
        url_basename=url_basename,
    ):
        factory = cls_factory
        model = cls_model
        user_model = cls_user_model
        api_view = cls_api_view

    return Tester

instance(instance_kwargs)

Generate instance.

Source code in saritasa_drf_tools/testing/api_action_tester.py
228
229
230
231
232
233
234
@pytest.fixture
def instance(
    self,
    instance_kwargs: dict[str, typing.Any],
) -> DjangoModel:
    """Generate instance."""
    return self.factory(**instance_kwargs)  # type: ignore

instance_batch(instance_batch_kwargs)

Create instances batch for testing.

Source code in saritasa_drf_tools/testing/api_action_tester.py
241
242
243
244
245
246
247
248
@pytest.fixture
def instance_batch(
    self,
    instance_batch_kwargs: dict[str, typing.Any],
) -> list[DjangoModel]:
    """Create instances batch for testing."""
    instance_batch_kwargs.setdefault("size", 5)
    return self.factory.create_batch(**instance_batch_kwargs)

instance_batch_kwargs()

Get kwargs for instance batch generation.

Source code in saritasa_drf_tools/testing/api_action_tester.py
236
237
238
239
@pytest.fixture
def instance_batch_kwargs(self) -> dict[str, typing.Any]:
    """Get kwargs for instance batch generation."""
    return {}

instance_kwargs()

Get kwargs for instance generation.

Source code in saritasa_drf_tools/testing/api_action_tester.py
223
224
225
226
@pytest.fixture
def instance_kwargs(self) -> dict[str, typing.Any]:
    """Get kwargs for instance generation."""
    return {}

invoke_factory(**kwargs)

Generate instance.

Source code in saritasa_drf_tools/testing/api_action_tester.py
164
165
166
def invoke_factory(self, **kwargs) -> DjangoModel:
    """Generate instance."""
    return self.factory.create(**kwargs)

invoke_factory_batch(size=5, **kwargs)

Generate instances.

Source code in saritasa_drf_tools/testing/api_action_tester.py
172
173
174
175
176
177
178
179
180
181
def invoke_factory_batch(
    self,
    size: int = 5,
    **kwargs,
) -> list[DjangoModel]:
    """Generate instances."""
    return self.factory.create_batch(
        size=size,
        **kwargs,
    )  # type: ignore

invoke_factory_build(**kwargs)

Build instance.

Source code in saritasa_drf_tools/testing/api_action_tester.py
168
169
170
def invoke_factory_build(self, **kwargs) -> DjangoModel:
    """Build instance."""
    return self.factory.build(**kwargs)

lazy_url(action='', **kwargs)

Get lazy url to action.

Source code in saritasa_drf_tools/testing/api_action_tester.py
105
106
107
108
109
110
111
112
113
114
115
def lazy_url(
    self,
    action: str = "",
    **kwargs,
) -> str:
    """Get lazy url to action."""
    viewname = "-".join(filter(None, (self.url_basename, action)))
    return django.urls.reverse_lazy(
        viewname=viewname,
        kwargs=kwargs,
    )

make_request(method, path, data=None, expected_status=None, api_client=None, user=None, capture_on_commit=False, **kwargs)

Make api request.

Source code in saritasa_drf_tools/testing/api_action_tester.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def make_request(
    self,
    method: http.HTTPMethod,
    path: str,
    data: dict[str, typing.Any] | list[typing.Any] | None = None,
    expected_status: int | None = None,
    api_client: rest_framework.test.APIClient | None = None,
    user: DjangoModel | None = None,
    capture_on_commit: bool = False,
    **kwargs,
) -> rest_framework.response.Response:
    """Make api request."""
    api_client = api_client or self.get_api_client()
    if user:
        api_client.force_authenticate(user)
    with self.capture_on_commit(execute=capture_on_commit):
        response: rest_framework.response.Response = getattr(
            api_client,
            method.lower(),
        )(
            path=path,
            data=data,
            **kwargs,
        )
    if not expected_status and method == http.HTTPMethod.POST.lower():
        expected_status = rest_framework.status.HTTP_201_CREATED
    if not expected_status and method == http.HTTPMethod.DELETE.lower():
        expected_status = rest_framework.status.HTTP_204_NO_CONTENT
    if not expected_status:
        expected_status = rest_framework.status.HTTP_200_OK
    assert response.status_code == expected_status, (  # noqa: S101
        response.status_code,
        response.data,
    )
    return response

serialize_data(action, data, many=False)

Serialize data by using view's action serializer.

Source code in saritasa_drf_tools/testing/api_action_tester.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def serialize_data(
    self,
    action: str,
    data: DjangoModel | dict[str, typing.Any],
    many: bool = False,
) -> (
    rest_framework.serializers.ReturnDict
    | rest_framework.serializers.ReturnList
):
    """Serialize data by using view's action serializer."""
    return self.get_serializer(action=action)(
        instance=data,
        many=many,
    ).data