-
-
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.
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 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 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
""" | |
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'), | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Worth mentioning there's an official (and much improved) version of this here, for those who stumble across this 1st (like me).