getsentry / responses

A utility for mocking out the Python Requests library.
Apache License 2.0
4.14k stars 354 forks source link

Using nested mocks - URL's from parent Mock not available in child Mock #645

Closed bblommers closed 1 year ago

bblommers commented 1 year ago

Describe the bug

Hi team!

I have a scenario where I start a RequestsMock, and then start another while the first one is still active. Mocked URL's from the first (parent) mock are not available in the child mock.

Is this a bug, or a side-effect of how the library is setup?

Additional context

No response

Version of responses

0.22.0

Steps to Reproduce

def test_nested_mocks():
    url_1 = "http://google.com"
    a = responses.RequestsMock()
    a.start()
    a.add(responses.GET, url_1, json={"foo": "bar_a"})

    b = responses.RequestsMock()
    b.start()

    # This call fails when b is active
    # if `b.start()` is commented out, it does work
    assert requests.get(url_1).json() == {"foo": "bar_a"}

    b.stop()

    assert requests.get(url_1).json() == {"foo": "bar_a"}
    a.stop()

Expected Result

Test passes

Actual Result

An error message: Connection refused by Responses - the call doesn't match any registered mock.

beliaev-maksim commented 1 year ago

@bblommers I think we solved similar issue in the past

in this case it is nested decorator. You register response per decorator, then upper decorator has mock registered, while second does not have.

for example this code will break again, because now A is lacking registered mock

def test_nested_mocks():
    url_1 = "http://google.com"
    a = responses.RequestsMock()
    a.start()

    b = responses.RequestsMock()
    b.start()
    b.add(responses.GET, url_1, json={"foo": "bar_a"})

    # This call fails when b is active
    # if `b.start()` is commented out, it does work
    assert requests.get(url_1).json() == {"foo": "bar_a"}

    b.stop()

    assert requests.get(url_1).json() == {"foo": "bar_a"}
    a.stop()
bblommers commented 1 year ago

The example I posted is simplified - the actual use case is that a user starts the first mock, and a third-party library (Moto) starts the second mock.

So when starting the second mock, we don't actually know whether the first mock exists, what URL's are registered, etc.

beliaev-maksim commented 1 year ago

@bblommers let me give insight how responses works and we can think what we can do for Moto

the difference from standard mocks is that we do not expose any adapter or session to outside. In background we patch requests adapter and override send request (basically we intercept it)

that means, that when you do requests.get(url_1) you do not do responses.request.get(), it is actual requests.get(url_1). I try to give an idea that when we are called in responses, we do not know about other mock. We just see whatever registered in out context loop and try to reuse it, there is no reference from requests to a particular instance of the mock

what are users try to achieve ?

can we educate users who want to use both responses and moto to follow only this concept:

def test_nested_mocks():
    url_1 = "http://google.com"

    b = responses.RequestsMock()
    b.start()
    b.add(responses.GET, url_1, json={"foo": "bar_b"})

    with responses.RequestsMock() as a:
        a.add(responses.GET, url_1, json={"foo": "bar_a"})
        assert requests.get(url_1).json() == {"foo": "bar_a"}

    assert requests.get(url_1).json() == {"foo": "bar_b"}
    b.stop()
beliaev-maksim commented 1 year ago

solved in "moto" thread based on PR #648