Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calls info in BaseResponse #664

Merged
merged 17 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
53 changes: 53 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that this example might look very close to what you have in production. But can we unload it a bit and reduce complexity?

we probably still want to show an example of concurrent since this could be one of the main usages, but we do not need all those complicated matrices of settings to show the usage

can you please simplify it to remove additional arguments and have more clear names like
rsp1, rsp2, rsp3

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got you, I'll try to simplify this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

simplified, please take a look

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zeezdev now looks a way better, thank you!

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
------------------

Expand Down
23 changes: 18 additions & 5 deletions responses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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.
Expand Down
30 changes: 30 additions & 0 deletions responses/tests/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that comment is probably true only with the default registry, or?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, this mocked resource (http://www.example.com/2) isn't requested during the test to explain why we get empty rsp3.calls.
I can remove this if it confuses.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is fine to keep it for the testing purpose.

I just mean that if the user applies a different registry or we switch the default registry (which is unlikely), then this comment will not be correct

https://github.com/getsentry/responses#ordered-registry

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but this comment relates to the specific scope of the test. And is it relevant in this case?

If I were to initialize the test with
@responses.activate(registry=OrderedRegistry)

then it would be a different case with different expectations, wouldn’t it?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is more of a note rather than a call for an action :)

I am +1 with the current state of PR

now wait for @markstory to make his round of review


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
Expand Down