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

Easier use of recorder beta feature? #696

Open
gswilcox01 opened this issue Dec 3, 2023 · 2 comments
Open

Easier use of recorder beta feature? #696

gswilcox01 opened this issue Dec 3, 2023 · 2 comments

Comments

@gswilcox01
Copy link

I'm kind of a python noob. But I thought i'd see if you are interested in me contributing this back. Or maybe you could correct my understanding/show me what i missed.

I initially went down this path where i followed the docs/example to record/activate, and created a helper function to make a directory & calculate a filename based on the test name (1 file for each test):

def yaml_filename(test_name):
    directory = os.path.splitext(__file__)[0] + "_files"
    os.makedirs(directory, exist_ok=True)

    filename = test_name + ".yaml"
    return os.path.join(directory, filename)

# Record OOTB setup, see: https://github.com/getsentry/responses#record-responses-to-files
# @_recorder.record(file_path=yaml_filename("test_responses_recorder"))
# Replay.1 OOTB setup, see: https://github.com/getsentry/responses#replay-responses-populate-registry-from-files
@responses.activate
def test_responses_recorder(runner, greetings_with_2res, two_users):
    # Replay.2 OOTB setup, see: https://github.com/getsentry/responses#replay-responses-populate-registry-from-files
    responses._add_from_file(file_path=yaml_filename("test_responses_recorder"))

It felt kind of annoying to have to repeat the test_name as a string (and make sure i don't copy/paste wrong), and also to comment/uncomment multiple lines to activate and load the file. So i made 2 simple decorators that did all of this for me.

My tests now look like this:

# @activate_recorder()
@activate_responses()
def test_get(runner, greetings_with_2res, two_users):
    pass

And for a single test_get.py module with 4 test functions in it, after recording i wind up with this "test_get_files" directory created & 4 output yaml files in it.

http/
	test_get_files/
		test_get.yaml
		test_get_401.yaml
		test_quiet_get.yaml
		test_various_gets.yaml
	test_get.py

Code for the 2 new decorators is here if your are interested:

def default_filename(func):
    module = inspect.getmodule(func)

    directory = os.path.splitext(module.__file__)[0] + "_files"
    os.makedirs(directory, exist_ok=True)

    filename = func.__name__ + ".yaml"
    return os.path.join(directory, filename)


def activate_responses(file_path=None):
    def outer_decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal file_path
            if file_path is None:
                file_path = default_filename(func)

            with responses.RequestsMock() as rsp:
                rsp._add_from_file(file_path=file_path)
                func(*args, **kwargs)

        return wrapper

    return outer_decorator


def activate_recorder(file_path=None):
    def outer_decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal file_path
            if file_path is None:
                file_path = default_filename(func)

            recorder = Recorder()
            with recorder:
                result = func(*args, **kwargs)
                recorder.dump_to_file(
                    file_path=file_path, registered=recorder.get_registry().registered
                )
                return result

        return wrapper

    return outer_decorator
@olivierdalang
Copy link

olivierdalang commented Feb 6, 2024

Hey ! Thanks @gswilcox01 for sharing your setup.

I definitely agree that providing facilities and documenting how back and forths between record/activate is supposed to be done would help a lot in using this otherwise great library.

Here I went one small step further and combined both decorators in a third one that conditionally switches between recorder and apply, based on a configuration (in my case a django setting but could just as well be an env var). I also added a try...finally clause in the recorder decorator so that it still saves values if an exception is hit to facilitate debugging/iterations (working around #705). Here's what it looks like:

import functools
import inspect
import pathlib

import responses
import responses._recorder


def make_filename(func):
    module = inspect.getmodule(func)
    # FIXME: include test case class name to avoid clashes
    directory = pathlib.Path(module.__file__).parent.joinpath("_testing_results")
    directory.mkdir(exist_ok=True)
    return directory.joinpath(f"{func.__name__}.yaml")


def activate_responses():
    def outer_decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            with responses.RequestsMock() as rsp:
                rsp._add_from_file(file_path=make_filename(func))
                return func(*args, **kwargs)

        return wrapper

    return outer_decorator


def activate_recorder():
    def outer_decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            recorder = responses._recorder.Recorder()
            with recorder:
                try:
                    result = func(*args, **kwargs)
                finally:
                    recorder.dump_to_file(
                        file_path=make_filename(func),
                        registered=recorder.get_registry().registered,
                    )
                return result

        return wrapper

    return outer_decorator


def mock_responses(update_results=False):
    """Decorator to record then mock requests made with the requests module.

    When update_results is True, will store requests to a yaml file. When it
    is false, it will retrieve the results, allowing to run tests offline.

    Usage:
        import requests
        from mdmodelpoc.testing.requests import mock_responses
        from django.conf import settings

        class MyTestCase(TestCase):
            @mock_responses(update_results=settings.TESTS_UPDATE_STORED_RESULTS)
            def test_mytest(self):
                request.get("https://example.com)
                ...
    """
    if update_results:
        return activate_recorder()
    else:
        return activate_responses()

@olivierdalang
Copy link

Another aspect that would facilitate this workflow is handling domain that are environment dependent. It's very common to have code like

def get_data():
    # a  service function that returns data
    return requests.get(f"{os.environ['DATA_ENDPOINT']}/mydata.json")

The issue is that this may not match the queries when run in different environments (e.g. I record requests in my local dev env to get test data from a local server, then want to retrieve responses in CI that has a different upstreams setting).

I worked around this by adding an aliases parameter to the decorator above that abstracts away the actual hostname. IMO that would be a very good addition the the API to facilitate usage of the recorder feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Archived in project
Development

No branches or pull requests

4 participants