Last active
November 24, 2018 21:52
-
-
Save simon-weber/9956782 to your computer and use it in GitHub Desktop.
Custom tooling to ease VCR.py management.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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