diff --git a/CHANGES b/CHANGES index 3113c85e..626fed61 100644 --- a/CHANGES +++ b/CHANGES @@ -1,6 +1,7 @@ 0.24.0 ------ +* Added `BaseResponse.calls` to access calls data of a separate mocked request. See #664 * Added `real_adapter_send` parameter to `RequestsMock` that will allow users to set through which function they would like to send real requests * Added support for re.Pattern based header matching. diff --git a/README.rst b/README.rst index 92e00400..3ce35b51 100644 --- a/README.rst +++ b/README.rst @@ -1079,6 +1079,59 @@ Assert that the request was called exactly n times. responses.assert_call_count("http://www.example.com?hello=world", 1) is True +Assert Request Calls data +------------------ + +``Request`` object has ``calls`` list which elements correspond to ``Call`` objects +in the global list of ``Registry``. This can be useful when the order of requests is not +guaranteed, but you need to check their correctness, for example in multithreaded +applications. + +.. code-block:: python + + import concurrent.futures + import responses + import requests + + + @responses.activate + def test_assert_calls_on_resp(): + rsp1 = responses.patch("http://www.foo.bar/1/", status=200) + rsp2 = responses.patch("http://www.foo.bar/2/", status=400) + rsp3 = responses.patch("http://www.foo.bar/3/", status=200) + + def update_user(uid, is_active): + url = f"http://www.foo.bar/{uid}/" + response = requests.patch(url, json={"is_active": is_active}) + return response + + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + future_to_uid = { + executor.submit(update_user, uid, is_active): uid + for (uid, is_active) in [("3", True), ("2", True), ("1", False)] + } + for future in concurrent.futures.as_completed(future_to_uid): + uid = future_to_uid[future] + response = future.result() + print(f"{uid} updated with {response.status_code} status code") + + assert len(responses.calls) == 3 # total calls count + + assert rsp1.call_count == 1 + assert rsp1.calls[0] in responses.calls + assert rsp1.calls[0].response.status_code == 200 + assert json.loads(rsp1.calls[0].request.body) == {"is_active": False} + + assert rsp2.call_count == 1 + assert rsp2.calls[0] in responses.calls + assert rsp2.calls[0].response.status_code == 400 + assert json.loads(rsp2.calls[0].request.body) == {"is_active": True} + + assert rsp3.call_count == 1 + assert rsp3.calls[0] in responses.calls + assert rsp3.calls[0].response.status_code == 200 + assert json.loads(rsp3.calls[0].request.body) == {"is_active": True} + Multiple Responses ------------------ diff --git a/responses/__init__.py b/responses/__init__.py index 7394a152..4d442662 100644 --- a/responses/__init__.py +++ b/responses/__init__.py @@ -245,6 +245,9 @@ def __getitem__(self, idx: Union[int, slice]) -> Union[Call, List[Call]]: def add(self, request: "PreparedRequest", response: "_Body") -> None: self._calls.append(Call(request, response)) + def add_call(self, call: Call) -> None: + self._calls.append(call) + def reset(self) -> None: self._calls = [] @@ -384,7 +387,7 @@ def __init__( ) self.match: "_MatcherIterable" = match - self.call_count: int = 0 + self._calls: CallList = CallList() self.passthrough = passthrough def __eq__(self, other: Any) -> bool: @@ -500,6 +503,14 @@ def matches(self, request: "PreparedRequest") -> Tuple[bool, str]: return True, "" + @property + def call_count(self) -> int: + return len(self._calls) + + @property + def calls(self) -> CallList: + return self._calls + def _form_response( body: Union[BufferedReader, BytesIO], @@ -1048,14 +1059,16 @@ def _on_request( request, match.get_response(request) ) except BaseException as response: - match.call_count += 1 - self._calls.add(request, response) + call = Call(request, response) + self._calls.add_call(call) + match.calls.add_call(call) raise if resp_callback: response = resp_callback(response) # type: ignore[misc] - match.call_count += 1 - self._calls.add(request, response) # type: ignore[misc] + call = Call(request, response) # type: ignore[misc] + self._calls.add_call(call) + match.calls.add_call(call) retries = retries or adapter.max_retries # first validate that current request is eligible to be retried. diff --git a/responses/tests/test_responses.py b/responses/tests/test_responses.py index 2962b552..7d3af31e 100644 --- a/responses/tests/test_responses.py +++ b/responses/tests/test_responses.py @@ -2035,6 +2035,36 @@ def run(): assert_reset() +def test_response_calls_and_registry_calls_are_equal(): + @responses.activate + def run(): + rsp1 = responses.add(responses.GET, "http://www.example.com") + rsp2 = responses.add(responses.GET, "http://www.example.com/1") + rsp3 = responses.add( + responses.GET, "http://www.example.com/2" + ) # won't be requested + + requests.get("http://www.example.com") + requests.get("http://www.example.com/1") + requests.get("http://www.example.com") + + assert len(responses.calls) == len(rsp1.calls) + len(rsp2.calls) + len( + rsp3.calls + ) + assert rsp1.call_count == 2 + assert len(rsp1.calls) == 2 + assert rsp1.calls[0] is responses.calls[0] + assert rsp1.calls[1] is responses.calls[2] + assert rsp2.call_count == 1 + assert len(rsp2.calls) == 1 + assert rsp2.calls[0] is responses.calls[1] + assert rsp3.call_count == 0 + assert len(rsp3.calls) == 0 + + run() + assert_reset() + + def test_fail_request_error(): """ Validate that exception is raised if request URL/Method/kwargs don't match