Skip to content

Instantly share code, notes, and snippets.

@simon-weber
Last active October 19, 2015 04:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save simon-weber/b0683e27285dc9ec8efa to your computer and use it in GitHub Desktop.
Save simon-weber/b0683e27285dc9ec8efa to your computer and use it in GitHub Desktop.
A nose plugin that uses VCR.py to detect tests with unmocked http interactions.
from setuptools import setup
setup(
name='nose-enforcevcr',
author='Simon Weber',
version='0.1',
description='Detect un-vcred external calls.',
entry_points={
'nose.plugins.0.10': [
'enforce_vcr = vcrenforce:EnforceVCR',
]
},
py_modules=['vcrenforce'],
)
"""
This nose plugin will detect un-vcred external calls in any tests run.
If any exist, a failing test will be created to output the details.
"""
from collections import defaultdict
from functools import partial
from operator import itemgetter
import sys
from nose.case import Test
from nose.plugins import Plugin
from nose.plugins.xunit import Xunit as XunitPlugin
import vcr
vcr.cassette.save_cassette = lambda *args, **kwargs: None
# These cassettes should never be written out, but we need them to record.
class VcrFailuresTest(object):
# This is just a callable with custom __str__, since
# apparently overriding __str__ on lambdas doesn't do anything.
def __str__(self):
return "Test run contains unmocked tests"
def __call__(self):
pass
id = __str__
class UnmockedReport(Exception):
# We need to defer generation of this output to when all tests are done.
# Because there's no nose hook after tests but before report output,
# we use this object to encapsulate it.
def __init__(self):
self.unmocked_tests = defaultdict(partial(defaultdict, list))
# {'my.module.MyClass': {'my_test': cassette}}
def add(self, test, cassette):
raw = repr(test) # 'Test(<my.module testMethod=test_save_me_user>)'
raw = raw[6:-2]
module_and_class, next_el = raw.split(' ')[:2]
test_name = next_el.split('=')[-1]
self.unmocked_tests[module_and_class][test_name] = cassette
def __str__(self):
output = []
for module_and_class, failures in sorted(self.unmocked_tests.iteritems(),
key=itemgetter(0)):
output.append("- %s:" % module_and_class)
output.append('')
for test_name, cassette in failures.items():
output.append(" %s:" % test_name)
for request in cassette.requests:
output.append(" %s %s" % (request.method, request.url.split('?')[0]))
output.append('')
output.append('')
return '\n'.join(output)
class EnforceVCR(Plugin):
enabled = False
env_opt = 'NOSE_ENFORCE_VCR'
name = 'enforce_vcr'
score = 5000 # msut be higher than 2000 to send results to xunit
def __init__(self):
Plugin.__init__(self)
def options(self, parser, env):
parser.add_option("--enforce-vcr",
action="store_true",
dest="enforce_vcr",
help=("Detect non-vcred tests that make external calls."),
)
def configure(self, options, conf):
self.conf = conf
self.added_failure = False
self.unmocked_report = UnmockedReport()
if options.enforce_vcr:
self.enabled = True
def prepareTestResult(self, result):
# We need access to the result in afterTest, but it's not usually available.
self.result = result
def startTest(self, test):
"""Run this test inside a cassette."""
cassette_name = "%s.yaml" % test
self._cassette_manager = vcr.use_cassette(cassette_name,
serializer='yaml',
record_mode='once',
ignore_localhost=True)
self._cassette = self._cassette_manager.__enter__()
assert not self._cassette.rewound # this cassette shouldn't be from disk
def stopTest(self, test):
"""Exit vcr and note any unmocked interactions."""
self._cassette_manager.__exit__(*sys.exc_info())
if len(self._cassette):
self.unmocked_report.add(test, self._cassette)
if not self.added_failure:
test = Test(VcrFailuresTest())
result = self.unmocked_report
self.result.failures.append((test, result))
self.added_failure = True
def report(self, result):
if not self.added_failure:
return
xunit_plugin = [p for p in self.conf.plugins.plugins if isinstance(p, XunitPlugin)]
if xunit_plugin:
xunit_plugin = xunit_plugin[0]
xunit_plugin.errorlist.append(
'<testcase classname=%(cls)s name=%(name)s time="%(taken).3f">'
'<failure type=%(errtype)s message=%(message)s><![CDATA[%(tb)s]]>'
'</failure></testcase>' %
{'cls': xunit_plugin._quoteattr('EnforceVcrFailure.Unmocked_tests_exist'),
'name': xunit_plugin._quoteattr('see_message_for_details'),
'taken': 0,
'errtype': xunit_plugin._quoteattr('EnforceVcrFailure'),
'message': xunit_plugin._quoteattr(str(self.unmocked_report)),
'tb': xunit_plugin._quoteattr('N/A'),
})
@j-funk
Copy link

j-funk commented Oct 19, 2015

Worth mentioning there's an official (and much improved) version of this here, for those who stumble across this 1st (like me).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment