Testing throttling#
We believe that throttling is a part of the business requirements, so it must be tested. To help users with it, we provide several utilities:
For sync controllers:
dmr.test.assert_throttling()For async controllers:
dmr.test.assert_async_throttling()
Testing that an endpoint is throttled usually means driving it to its limit
first. Sending the configured max_requests in a loop is slow for large
rates such as 1000/hour. What do we do instead?
We lower the
max_requestsvalue for the first throttle and increase the rate to be a hour. This way we can reliably and fastly test the expected behaviorNext, we send several requests (
max_requestscontrols this) that will hit the endpoint, assert that the response status matchessuccess_statuscodeLastly, we assert that the final request hits the rate limit, we also assert that the headers match our modified rate limit
Examples:
Default Python’s testing framework:
1from typing import Final
2
3from django.core.cache import cache
4from django.test import TestCase
5from typing_extensions import override
6
7from dmr import Controller
8from dmr.plugins.pydantic import PydanticFastSerializer
9from dmr.test import DMRRequestFactory, assert_throttling
10from dmr.throttling import Rate, SyncThrottle
11from dmr.throttling.backends import SyncDjangoCache
12
13_URL: Final = '/reports/'
14
15
16class ReportsController(Controller[PydanticFastSerializer]):
17 # A large hourly budget: reaching it with real requests would be slow.
18 throttling = (
19 SyncThrottle(
20 1000,
21 Rate.hour,
22 # Tests use `LocMemCache`, so the unsafe cache check is disabled:
23 backend=SyncDjangoCache(allow_unsafe_cache=True),
24 ),
25 )
26
27 def get(self) -> str:
28 return 'inside'
29
30
31# In your test files:
32class TestReportsThrottling(TestCase):
33 @override
34 def setUp(self) -> None:
35 cache.clear() # keep throttle state isolated between tests
36 self.dmr_rf = DMRRequestFactory()
37
38 def test_endpoint_is_throttled(self) -> None:
39 # Lower the throttle, drive a couple of real requests, assert the 429 —
40 # no 1000 requests needed. Returns the rejected response:
41 assert_throttling(
42 ReportsController,
43 lambda: self.dmr_rf.get(_URL),
44 )
And more preferable pytest:
1from collections.abc import Iterator
2from typing import Final
3
4import pytest
5from django.core.cache import cache
6
7from dmr import Controller
8from dmr.plugins.pydantic import PydanticFastSerializer
9from dmr.test import DMRRequestFactory, assert_throttling
10from dmr.throttling import Rate, SyncThrottle
11from dmr.throttling.backends import SyncDjangoCache
12
13_URL: Final = '/reports/'
14
15
16class ReportsController(Controller[PydanticFastSerializer]):
17 # A large hourly budget: reaching it with real requests would be slow.
18 throttling = (
19 SyncThrottle(
20 1000,
21 Rate.hour,
22 # Tests use `LocMemCache`, so the unsafe cache check is disabled:
23 backend=SyncDjangoCache(allow_unsafe_cache=True),
24 ),
25 )
26
27 def get(self) -> str:
28 return 'inside'
29
30
31@pytest.fixture(autouse=True)
32def _clean_throttling_cache() -> Iterator[None]:
33 cache.clear() # keep throttle state isolated between tests
34 yield
35 cache.clear()
36
37
38def test_endpoint_is_throttled(dmr_rf: DMRRequestFactory) -> None:
39 # Lower the throttle, drive a couple of real requests, assert the 429 —
40 # no 1000 requests needed. Returns the rejected response:
41 assert_throttling(
42 ReportsController,
43 lambda: dmr_rf.get(_URL),
44 )
Only the endpoint under test is affected; its throttling is restored afterwards.
There’s also a lower level API:
dmr.test.reduced_throttling()to reduce the throttle number manuallydmr.test.assert_throttled()to assert that the response object is from throttling middleware