Closed gswilcox01 closed 4 months ago
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()
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.
for now we are not planning to change the behavior
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):
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:
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.
Code for the 2 new decorators is here if your are interested: