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

Mocking is not completely undone when decorated tests are nested #481

Closed
crazyscientist opened this issue Jan 21, 2022 · 6 comments · Fixed by #500
Closed

Mocking is not completely undone when decorated tests are nested #481

crazyscientist opened this issue Jan 21, 2022 · 6 comments · Fixed by #500
Assignees
Labels

Comments

@crazyscientist
Copy link

First of all, I'd like to point out that I only noticed this problem in a project where HTTP requests are mocked with multiple libraries (I know, bad style).

Environment

  • Ubuntu 20.04
  • Python 3.X, X in (6, 8)
  • responses 0.17.0
  • httpretty 1.1.4

Steps to Reproduce

  1. As mentioned, I ended up with a testsuite that uses two libraries for mocking: responses and httpretty.
  2. In an effort to reduce code duplication in the test suite, I wanted to re-use a test method decorated with responses.activate within another test method, that is also decorated with responses.activate.
  3. Another test method decorated with httpretty.activate will fail, if it is executed after the test including the nesting is executed.

To illustrate this, I have created a gist.

Expected Result

Mocks introduced my the responses package are completely rolled back, even if there is a nesting of mocks.

Actual Result

To illustrate this, I have created a gist:

  • In it's current state (no nesting of tests decorated with responses.activate), all tests in the gist will pass.
  • If the decorator in line 29 is commented in, one test will fail:
======================================================================
ERROR: test_c_httpretty (scratch_4.TestRequest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/httpretty/core.py", line 2075, in wrapper
    return test(*args, **kw)
  File "/home/andi/.config/JetBrains/PyCharm2021.2/scratches/scratch_4.py", line 29, in test_c_httpretty
    self.assertEqual(b"Hello 3", requests.get("http://example.com/3").content)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/requests/api.py", line 75, in get
    return request('get', url, params=params, **kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/requests/api.py", line 61, in request
    return session.request(method=method, url=url, **kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/requests/sessions.py", line 529, in request
    resp = self.send(prep, **send_kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/requests/sessions.py", line 645, in send
    r = adapter.send(request, **kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/responses/__init__.py", line 842, in unbound_on_send
    return self._on_request(adapter, request, *a, **kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/responses/__init__.py", line 821, in _on_request
    raise response
requests.exceptions.ConnectionError: Connection refused by Responses - the call doesn't match any registered mock.

Request: 
- GET http://example.com/3

Available matches:

@beliaev-maksim
Copy link
Collaborator

there are multiple issues that I see

  1. just split two libraries into two different classes, or better switch to responses
  2. why do you call one test from another, this is an architectural mistake. Just create an internal method eg _add_site_responses(), that adds some responses that you need, and call it in test functions as many times you need

I think applying these two design patterns will eliminate the issue

@crazyscientist
Copy link
Author

or better switch to responses

That's the plan, but replacing the mock definitions in over 700 tests takes time.

just split two libraries into two different classes

That has no effect, even if the two use cases occur in different modules. As far as I can tell, it affects all discovered httpretty tests executed after the nested one.

why do you call one test from another, this is an architectural mistake.

That is true, but I was lazy and just wanted to run an already existing test with some additional constraint. So, I just defined the new test case with decorator and called the existing one.

I think applying these two design patterns will eliminate the issue

Yes, there are a few more workarounds I can think of, but I wanted to share my findings nevertheless. Because someone might also consider this unexpected behavior.

For what it's worth, it just occurred to me, that this problem is not limited to the use of two mocking libraries. Even if one wants to perform real requests, those will fail. I have extended the gist and get this new exception:

ERROR: test_d (scratch_4.TestRequest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/andi/.config/JetBrains/PyCharm2021.2/scratches/scratch_4.py", line 32, in test_d
    response = requests.head("https://www.google.com")
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/requests/api.py", line 102, in head
    return request('head', url, **kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/requests/api.py", line 61, in request
    return session.request(method=method, url=url, **kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/requests/sessions.py", line 529, in request
    resp = self.send(prep, **send_kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/requests/sessions.py", line 645, in send
    r = adapter.send(request, **kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/responses/__init__.py", line 842, in unbound_on_send
    return self._on_request(adapter, request, *a, **kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/responses/__init__.py", line 821, in _on_request
    raise response
requests.exceptions.ConnectionError: Connection refused by Responses - the call doesn't match any registered mock.

Request: 
- HEAD https://www.google.com/

Available matches:

I do appreciate your lack of enthusiasm for "fixing" this. My primary goal was to document this.

@beliaev-maksim
Copy link
Collaborator

I do not understand why that fails, there is even no decorator on the method

I think httpretty is doing something on the background and registers all the methods, not sure.

have you raised your concerns in httpretty ?

@crazyscientist
Copy link
Author

have you raised your concerns in httpretty ?

No, because httpretty is completely innocent. The problem can also be illustrated when only using responses: https://gist.github.com/crazyscientist/9882d4dac97e8c2889d88f6ca65c2d64

Please note what is happening in the gist: The alphabetical order of the test cases ensures that a real request is required in a test before and after applying the responses.activate decorators.

In the current state of the gist, test_c produces an error. If you comment out either line 16 or 18, it will pass. The traceback looks like this:

ERROR: test_c (scratch_4.TestRequest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/andi/.config/JetBrains/PyCharm2021.2/scratches/scratch_4.py", line 21, in test_c
    response = requests.head("https://www.google.com")
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/requests/api.py", line 102, in head
    return request('head', url, **kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/requests/api.py", line 61, in request
    return session.request(method=method, url=url, **kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/requests/sessions.py", line 529, in request
    resp = self.send(prep, **send_kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/requests/sessions.py", line 645, in send
    r = adapter.send(request, **kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/responses/__init__.py", line 842, in unbound_on_send
    return self._on_request(adapter, request, *a, **kwargs)
  File "/home/andi/virtualenvs/smash/lib/python3.8/site-packages/responses/__init__.py", line 821, in _on_request
    raise response
requests.exceptions.ConnectionError: Connection refused by Responses - the call doesn't match any registered mock.

Request: 
- HEAD https://www.google.com/

Available matches:


----------------------------------------------------------------------

I do not understand why that fails, there is even no decorator on the method

I have not checked the code, but I believe the problem lies in the method of how mocked objects are stored.

From the gist we know, that the problem only occurs after the nesting of responses.activate, because test_a always gets executed first and passes. And test_c produces an error because there is still a mock active.

I'd like to point out, that this is pure speculation: It looks like mocked objects (i.e. the originals) get stored in a "global variable". By nesting the decorator a mock gets stored in this variable. And when the mocked objects get restored we end up with a mock in the following test case that we do not intend to mock.

I hope I was able to sufficiently explain what I think is happening.

If you are willing to fix this, I would be very grateful. If not, we now have a description including workarounds that should be considered as better practice anyway. 🙂

@sirosen
Copy link

sirosen commented Feb 1, 2022

As someone who has experienced the painful (but ultimately gratifying) transition from httpretty to responses, one idea:

I've managed this transiiton in the past by making a dedicated utility module which encapsulates all use of httpretty, and then a single change to swap the implementation of that module from httpretty to responses. That approach may or may not work well for you, but the interfaces for these two libraries are not terribly different.

As another thought to toss out there, in case it helps: avoid nesting responses.activate calls with a custom decorator:

# caveat emptor: I haven't tested this
_responses_active = False

def reentrant_activate(func):
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        global _responses_active

        if _responses_active:
            return func(*args, **kwargs)
        try:
            _responses_active = True
            return responses.activate(func)(*args, **kwargs)
        finally:
            _responses_active = False

    return wrapped

@beliaev-maksim
Copy link
Collaborator

copy gist here to track the history:

import unittest

import requests
import responses


class TestRequest(unittest.TestCase):
    def test_a(self):
        self.assertEqual(200, requests.get("https://www.google.com").status_code)

    @responses.activate
    def test_b_response_1(self):
        responses.add(responses.GET, "http://example.com/1", body="Hello 1")
        self.assertEqual(b"Hello 1", requests.get("http://example.com/1").content)

    @responses.activate
    def test_b_response_2(self):
        self.test_b_response_1()

    def test_c(self):
        response = requests.head("https://www.google.com")
        self.assertEqual(response.status_code, 200)

markstory pushed a commit that referenced this issue Feb 22, 2022
* clean patcher on exit
* add tests and comments

Closes #481
@beliaev-maksim beliaev-maksim self-assigned this Jun 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants