Skip to content

Instantly share code, notes, and snippets.

@simon-weber
Last active November 24, 2018 21:52
Show Gist options
  • Save simon-weber/9956782 to your computer and use it in GitHub Desktop.
Save simon-weber/9956782 to your computer and use it in GitHub Desktop.
Custom tooling to ease VCR.py management.
import vcrutils
VCR_CASSETTE_PATH = APPROOT + '/venmo_tests/cassettes/' # eg
MAKE_EXTERNAL_REQUESTS = os.environ.get('MAKE_EXTERNAL_REQUESTS') == 'TRUE'
@dual_decorator # convert a paramaterized decorator for no-arg use (https://gist.github.com/simon-weber/9956622).
def external_call(*args, **kwargs):
"""Enable vcrpy to store/mock http requests.
The most basic use looks like::
@external_call
def test_foo(self):
# urllib2, urllib3, and requests will be recorded/mocked.
...
By default, the matching strategy is very restrictive.
To customize it, this decorator's params are passed to vcr.VCR().
For example, to customize the matching strategy, do::
@external_call(match_on=['url', 'host'])
def test_foo(self):
...
If it's easier to match requests by introducing subcassettes,
the decorator can provide a context manager::
@external_call(use_namespaces=True)
def test_foo(self, vcr_namespace): # this argument must be present
# do some work with the base cassette
with vcr_namespace('do_other_work'):
# this uses a separate cassette namespaced under the parent
# we're now using the base cassette again
To force decorated tests to make external requests, set
the MAKE_EXTERNAL_REQUESTS envvar to TRUE.
Class method tests are also supported.
"""
use_namespaces = kwargs.pop('use_namespaces', False)
vcr_args = args
vcr_kwargs = kwargs
default_vcr_kwargs = {
'cassette_library_dir': VCR_CASSETTE_PATH,
'record_mode': 'none',
'match_on': ['url',
'method',
'body',
'path',
'host']
}
default_vcr_kwargs.update(vcr_kwargs)
match_on = default_vcr_kwargs.pop('match_on')
def decorator(f, vcr_args=vcr_args, vcr_kwargs=default_vcr_kwargs,
match_on=match_on, use_namespaces=use_namespaces):
# this is used as a nose attribute:
# http://nose.readthedocs.org/en/latest/plugins/attrib.html
f.external_api = True
@wraps(f)
def wrapper(*args, **kwargs):
my_vcr = vcrutils.get_vcr(*vcr_args, **vcr_kwargs)
cassette_filename = vcrutils.get_filename_from_method(f, args[0])
if use_namespaces:
kwargs['vcr_namespace'] = vcrutils.get_namespace_cm(
my_vcr, cassette_filename, MAKE_EXTERNAL_REQUESTS)
if MAKE_EXTERNAL_REQUESTS:
return f(*args, **kwargs)
else:
with my_vcr.use_cassette(cassette_filename, match_on=match_on):
return f(*args, **kwargs)
return wrapper
return decorator
from contextlib import contextmanager
import inspect
import vcr
def get_vcr(*args, **kwargs):
"""Return a VCR, with our custom matchers registered.
Params are passed to VCR init."""
v = vcr.VCR(*args, **kwargs)
# register custom matchers here
return v
def get_filename_from_method(func, receiver):
"""Return an unambigious filename built from a test method invocation.
The method is assumed to be declared inside venmo_tests.
:attr func: the method's function object.
:attr receiver: the first argument to the method, i.e. self or cls.
"""
# Omit the module path above and including venmo_tests.
# This can include eg a jenkins workspace dir, which
# would make naming inconsistent. Eg::
# before - <jenkins workspace>.venmo_tests.testfile.test_foo
# after - testfile.test_foo
mod_name = func.__module__
mod_name = mod_name[mod_name.index('venmo_tests') + len('venmo_tests') + 1:]
if inspect.isclass(receiver):
class_name = receiver.__name__
else:
class_name = receiver.__class__.__name__
return "%s.%s.%s.yaml" % (mod_name, class_name, func.__name__)
def _get_subcassette_filename(name, parent_filename):
"""Return a cassette namespaced by a parent cassette filename.
For example::
>>> _get_subcassette_filename('foo', 'mytests.test_bar.yaml')
'mytests.test_bar.foo.yaml'
"""
parent_components = parent_filename.split('.')
parent_components.insert(len(parent_components) - 1, name)
return '.'.join(parent_components)
def get_namespace_cm(my_vcr, parent_filename, make_external_requests):
"""Return a context manager that uses a cassette namespaced under the parent.
The context manager takes two arguments:
* name: a string that names the cassette.
* match_on: (optional), passed to use_cassette to override the default.
"""
@contextmanager
def namespace_cm(name, match_on=None,
my_vr=my_vcr, parent_filename=parent_filename,
make_external_requests=make_external_requests):
if make_external_requests:
yield
else:
kwargs = {
'path': _get_subcassette_filename(name, parent_filename),
'match_on': match_on
}
if match_on is None:
# vcr doesn't use a sentinel for match_on;
# it just shouldn't be present to default it.
del kwargs['match_on']
with my_vcr.use_cassette(**kwargs):
yield
return namespace_cm
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment