Coverage for fastapi_restly / testing / _client.py: 90%
52 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-24 11:13 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-24 11:13 +0000
1import json
3import httpx
4from fastapi.testclient import TestClient
6# httpx accepts either a string or a `httpx.URL` for request URLs. The base
7# class' `URLTypes` alias is private, so we replicate the public union here.
8URLTypes = httpx.URL | str
11class RestlyTestClient(TestClient):
12 """Custom TestClient that automatically checks response codes and provides clear error messages."""
14 def assert_status(self, response: httpx.Response, expected_code: int | None = None):
15 """Check if the response status code matches the expected code."""
16 __tracebackhide__ = True
18 status_code = response.status_code
20 if expected_code is not None and status_code == expected_code:
21 return # All good
23 if expected_code is None and status_code < 400:
24 return # Also fine
26 # Raise AssertionError with detailed error message
27 try:
28 response_content = response.json()
29 except (ValueError, TypeError, json.JSONDecodeError):
30 response_content = response.content.decode(errors="ignore")
32 content_str_raw = str(response_content)
33 if len(content_str_raw) > 1000: 33 ↛ 34line 33 didn't jump to line 34 because the condition on line 33 was never true
34 content_str_raw = content_str_raw[:1000] + "...(truncated)"
35 content_str = f"Response JSON: {content_str_raw}"
37 # Safe method/URL extraction
38 try:
39 method = response.request.method.upper()
40 url = str(response.request.url)
41 request_info = f"{method} {url}"
42 except Exception:
43 request_info = "(request info unavailable)"
45 raise AssertionError(
46 f"Expected {request_info} to return {expected_code}, got {status_code}\n"
47 f"{content_str}"
48 )
50 def get(
51 self, url: URLTypes, *, assert_status_code: int | None = 200, **kwargs
52 ) -> httpx.Response:
53 """Make a GET request. Asserts the response status code matches `assert_status_code` (default: 200).
54 Pass `assert_status_code=None` to skip the assertion."""
55 __tracebackhide__ = True
56 response = super().get(url, **kwargs)
57 self.assert_status(response, assert_status_code)
58 return response
60 def post(
61 self, url: URLTypes, *, assert_status_code: int | None = 201, **kwargs
62 ) -> httpx.Response:
63 """Make a POST request. Asserts the response status code matches `assert_status_code` (default: 201).
64 Pass `assert_status_code=None` to skip the assertion."""
65 __tracebackhide__ = True
66 response = super().post(url, **kwargs)
67 self.assert_status(response, assert_status_code)
68 return response
70 def put(
71 self, url: URLTypes, *, assert_status_code: int | None = 200, **kwargs
72 ) -> httpx.Response:
73 """Make a PUT request. Asserts the response status code matches `assert_status_code` (default: 200).
74 Pass `assert_status_code=None` to skip the assertion."""
75 __tracebackhide__ = True
76 response = super().put(url, **kwargs)
77 self.assert_status(response, assert_status_code)
78 return response
80 def patch(
81 self, url: URLTypes, *, assert_status_code: int | None = 200, **kwargs
82 ) -> httpx.Response:
83 """Make a PATCH request. Asserts the response status code matches `assert_status_code` (default: 200).
84 Pass `assert_status_code=None` to skip the assertion."""
85 __tracebackhide__ = True
86 response = super().patch(url, **kwargs)
87 self.assert_status(response, assert_status_code)
88 return response
90 def delete(
91 self, url: URLTypes, *, assert_status_code: int | None = 204, **kwargs
92 ) -> httpx.Response:
93 """Make a DELETE request. Asserts the response status code matches `assert_status_code` (default: 204).
94 Pass `assert_status_code=None` to skip the assertion."""
95 __tracebackhide__ = True
96 response = super().delete(url, **kwargs)
97 self.assert_status(response, assert_status_code)
98 return response