├── .gitignore ├── .travis.yml ├── LICENSE ├── README.rst ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── test_testcase.py ├── tox.ini └── vcr_unittest ├── __init__.py └── testcase.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | - "3.6" 8 | - "pypy3.5" 9 | 10 | # Enable 3.7 without globally enabling sudo and dist: xenial for other build jobs 11 | # https://github.com/travis-ci/travis-ci/issues/9815 12 | matrix: 13 | include: 14 | - python: 3.7 15 | dist: xenial 16 | sudo: true 17 | 18 | env: 19 | global: 20 | - COVERAGE_CMD="coverage run --append --source vcr_unittest,tests -m" 21 | - COVERAGE_DEP=coverage 22 | 23 | install: 24 | # install tox-travis explicitly after tox to avoid 25 | # pkg_resources.VersionConflict, see 26 | # https://travis-ci.org/scampersand/bienvenue/jobs/196462565 27 | # https://github.com/ryanhiebert/tox-travis/issues/26 28 | - pip install tox 29 | - pip install tox-travis 30 | - pip install codecov 31 | 32 | script: 33 | - tox 34 | 35 | after_script: 36 | - codecov 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2018 Aron Griffis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | vcrpy-unittest 3 | ============== 4 | 5 | |PyPI| |Build Status| |Coverage Report| |Python Versions| |Gitter| 6 | 7 | This package provides ``VCRTestCase`` for simple integration between 8 | `VCR.py`_ and Python's venerable unittest_. 9 | 10 | Installation 11 | ------------ 12 | 13 | Install from PyPI_: 14 | 15 | .. code:: sh 16 | 17 | pip install vcrpy-unittest 18 | 19 | Usage 20 | ----- 21 | 22 | Inherit from ``VCRTestCase`` for automatic recording and playback of HTTP 23 | interactions. 24 | 25 | .. code:: python 26 | 27 | from vcr_unittest import VCRTestCase 28 | import requests 29 | 30 | class MyTestCase(VCRTestCase): 31 | def test_something(self): 32 | response = requests.get('http://example.com') 33 | 34 | Similar to how VCR.py returns the cassette from the context manager, 35 | ``VCRTestCase`` makes the cassette available as ``self.cassette``: 36 | 37 | .. code:: python 38 | 39 | self.assertEqual(len(self.cassette), 1) 40 | self.assertEqual(self.cassette.requests[0].uri, 'http://example.com') 41 | 42 | By default cassettes will be placed in the ``cassettes`` subdirectory next to the 43 | test, named according to the test class and method. For example, the above test 44 | would read from and write to ``cassettes/MyTestCase.test_something.yaml`` 45 | 46 | The configuration can be modified by overriding methods on your subclass: 47 | ``_get_vcr_kwargs``, ``_get_cassette_library_dir`` and ``_get_cassette_name``. 48 | To modify the ``VCR`` object after instantiation, for example to add a matcher, 49 | you can hook on ``_get_vcr``, for example: 50 | 51 | .. code:: python 52 | 53 | class MyTestCase(VCRTestCase): 54 | def _get_vcr(self, **kwargs): 55 | myvcr = super(MyTestCase, self)._get_vcr(**kwargs) 56 | myvcr.register_matcher('mymatcher', mymatcher) 57 | myvcr.match_on = ['mymatcher'] 58 | return myvcr 59 | 60 | See 61 | `the source 62 | `__ 63 | for the default implementations of these methods, and `VCR.py`_ for more 64 | information. 65 | 66 | If you implement a ``setUp`` method on your test class then make sure to call the parent version ``super().setUp()`` in your own in order to continue getting the cassettes produced. 67 | 68 | VCRMixin 69 | ~~~~~~~~ 70 | 71 | In case inheriting from ``VCRTestCase`` is difficult because of an existing 72 | class hierarchy containing tests in the base classes, inherit from ``VCRMixin`` 73 | instead. 74 | 75 | .. code:: python 76 | 77 | from vcr_unittest import VCRMixin 78 | import requests 79 | import unittest 80 | 81 | class MyTestMixin(VCRMixin): 82 | def test_something(self): 83 | response = requests.get(self.url) 84 | 85 | class MyTestCase(MyTestMixin, unittest.TestCase): 86 | url = 'http://example.com' 87 | 88 | License 89 | ------- 90 | 91 | This library uses the MIT license, which is the same as VCR.py. See `LICENSE.txt 92 | `__ for more 93 | details. 94 | 95 | Acknowledgements 96 | ---------------- 97 | 98 | Thanks to `@kevin1024`_ for `VCR.py`_, and to `@IvanMalison`_ for his 99 | constructive critique on this package. Also thanks to `@nedbat`_ for his `post 100 | regarding unittest and context managers 101 | `__, 102 | and to `@davepeck`_ for `httreplay `__ 103 | which served me well for so long. 104 | 105 | .. _PyPI: https://pypi.python.org/pypi/vcrpy-unittest 106 | .. _VCR.py: https://github.com/kevin1024/vcrpy 107 | .. _unittest: https://docs.python.org/2/library/unittest.html 108 | 109 | .. _@kevin1024: https://github.com/kevin1024 110 | .. _@IvanMalison: https://github.com/IvanMalison 111 | .. _@nedbat: https://github.com/nedbat 112 | .. _@davepeck: https://github.com/davepeck 113 | 114 | .. |Build Status| image:: https://img.shields.io/travis/agriffis/vcrpy-unittest/master.svg?style=plastic 115 | :target: https://travis-ci.org/agriffis/vcrpy-unittest?branch=master 116 | 117 | .. |Coverage Report| image:: https://img.shields.io/codecov/c/github/agriffis/vcrpy-unittest/master.svg?style=plastic 118 | :target: https://codecov.io/gh/agriffis/vcrpy-unittest/branch/master 119 | 120 | .. |PyPI| image:: https://img.shields.io/pypi/v/vcrpy-unittest.svg?style=plastic 121 | :target: PyPI_ 122 | 123 | .. |Python Versions| image:: https://img.shields.io/pypi/pyversions/vcrpy-unittest.svg?style=plastic 124 | :target: PyPI_ 125 | 126 | .. |Gitter| image:: https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-green.svg?style=plastic 127 | :target: https://gitter.im/kevin1024/vcrpy 128 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is for development and testing only. 2 | # Runtime requirements are in setup.py install_requirements. 3 | 4 | vcrpy 5 | pytest 6 | mock 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # https://github.com/pypa/sampleproject/blob/master/setup.cfg 2 | 3 | [bdist_wheel] 4 | # This flag says that the code is written to work on both Python 2 and Python 5 | # 3. If at all possible, it is good practice to do this. If you cannot, you 6 | # will need to generate wheels for each Python version that you support. 7 | universal=1 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from setuptools import setup, find_packages 4 | from codecs import open 5 | import os 6 | 7 | here = os.path.dirname(__file__) 8 | with open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: 9 | long_description = f.read() 10 | 11 | setup( 12 | name='vcrpy-unittest', 13 | version='0.1.7', 14 | description='Python unittest integration for vcr.py', 15 | long_description=long_description, 16 | url='https://github.com/agriffis/vcrpy-unittest', 17 | author='Aron Griffis', 18 | author_email='aron@scampersand.com', 19 | license='MIT', 20 | 21 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 22 | classifiers=[ 23 | 'Development Status :: 5 - Production/Stable', 24 | 'Intended Audience :: Developers', 25 | 'Topic :: Software Development :: Build Tools', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Programming Language :: Python', 28 | 'Programming Language :: Python :: 2', 29 | 'Programming Language :: Python :: 2.7', 30 | 'Programming Language :: Python :: 3', 31 | 'Programming Language :: Python :: 3.4', 32 | 'Programming Language :: Python :: 3.5', 33 | 'Programming Language :: Python :: 3.6', 34 | 'Programming Language :: Python :: 3.7', 35 | ], 36 | keywords='vcrpy vcr.py unittest testing mock http'.split(), 37 | packages=find_packages(exclude=['tests']), 38 | install_requires=['vcrpy'], 39 | ) 40 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agriffis/vcrpy-unittest/a2fd7625fde1ea15c8982759b07007aef40424b3/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_testcase.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import os 4 | from mock import MagicMock as Mock 5 | from unittest import defaultTestLoader, TextTestRunner 6 | from vcr_unittest import VCRTestCase 7 | 8 | try: 9 | from urllib2 import urlopen 10 | except ImportError: 11 | from urllib.request import urlopen 12 | 13 | 14 | def test_defaults(): 15 | 16 | class MyTest(VCRTestCase): 17 | def test_foo(self): 18 | pass 19 | 20 | test = run_testcase(MyTest)[0][0] 21 | expected_path = os.path.join(os.path.dirname(__file__), 'cassettes') 22 | expected_name = 'MyTest.test_foo.yaml' 23 | assert os.path.dirname(test.cassette._path) == expected_path 24 | assert os.path.basename(test.cassette._path) == expected_name 25 | 26 | 27 | def test_disabled(): 28 | 29 | # Baseline vcr_enabled = True 30 | class MyTest(VCRTestCase): 31 | def test_foo(self): 32 | pass 33 | test = run_testcase(MyTest)[0][0] 34 | assert hasattr(test, 'cassette') 35 | 36 | # Test vcr_enabled = False 37 | class MyTest(VCRTestCase): 38 | vcr_enabled = False 39 | def test_foo(self): 40 | pass 41 | test = run_testcase(MyTest)[0][0] 42 | assert not hasattr(test, 'cassette') 43 | 44 | 45 | def test_cassette_library_dir(): 46 | 47 | class MyTest(VCRTestCase): 48 | def test_foo(self): 49 | pass 50 | def _get_cassette_library_dir(self): 51 | return '/testing' 52 | 53 | test = run_testcase(MyTest)[0][0] 54 | assert test.cassette._path.startswith('/testing/') 55 | 56 | 57 | def test_cassette_name(): 58 | 59 | class MyTest(VCRTestCase): 60 | def test_foo(self): 61 | pass 62 | def _get_cassette_name(self): 63 | return 'my-custom-name' 64 | 65 | test = run_testcase(MyTest)[0][0] 66 | assert os.path.basename(test.cassette._path) == 'my-custom-name' 67 | 68 | 69 | def test_vcr_kwargs_overridden(): 70 | 71 | class MyTest(VCRTestCase): 72 | def test_foo(self): 73 | pass 74 | def _get_vcr_kwargs(self): 75 | kwargs = super(MyTest, self)._get_vcr_kwargs() 76 | kwargs['record_mode'] = 'new_episodes' 77 | return kwargs 78 | 79 | test = run_testcase(MyTest)[0][0] 80 | assert test.cassette.record_mode == 'new_episodes' 81 | 82 | 83 | def test_vcr_kwargs_passed(): 84 | 85 | class MyTest(VCRTestCase): 86 | def test_foo(self): 87 | pass 88 | def _get_vcr_kwargs(self): 89 | return super(MyTest, self)._get_vcr_kwargs( 90 | record_mode='new_episodes', 91 | ) 92 | 93 | test = run_testcase(MyTest)[0][0] 94 | assert test.cassette.record_mode == 'new_episodes' 95 | 96 | 97 | def test_vcr_kwargs_cassette_dir(): 98 | 99 | # Test that _get_cassette_library_dir applies if cassette_library_dir 100 | # is absent from vcr kwargs. 101 | class MyTest(VCRTestCase): 102 | def test_foo(self): 103 | pass 104 | def _get_vcr_kwargs(self): 105 | return dict( 106 | record_mode='new_episodes', 107 | ) 108 | _get_cassette_library_dir = Mock(return_value='/testing') 109 | test = run_testcase(MyTest)[0][0] 110 | assert test.cassette._path.startswith('/testing/') 111 | assert test._get_cassette_library_dir.call_count == 1 112 | 113 | # Test that _get_cassette_library_dir is ignored if cassette_library_dir 114 | # is present in vcr kwargs. 115 | class MyTest(VCRTestCase): 116 | def test_foo(self): 117 | pass 118 | def _get_vcr_kwargs(self): 119 | return dict( 120 | cassette_library_dir='/testing', 121 | ) 122 | _get_cassette_library_dir = Mock(return_value='/ignored') 123 | test = run_testcase(MyTest)[0][0] 124 | assert test.cassette._path.startswith('/testing/') 125 | assert test._get_cassette_library_dir.call_count == 0 126 | 127 | 128 | def test_get_vcr_with_matcher(tmpdir): 129 | cassette_dir = tmpdir.mkdir('cassettes') 130 | assert len(cassette_dir.listdir()) == 0 131 | 132 | mock_matcher = Mock(return_value=True) 133 | 134 | class MyTest(VCRTestCase): 135 | def test_foo(self): 136 | self.response = urlopen('http://example.com').read() 137 | def _get_vcr(self): 138 | myvcr = super(MyTest, self)._get_vcr() 139 | myvcr.register_matcher('mymatcher', mock_matcher) 140 | myvcr.match_on = ['mymatcher'] 141 | return myvcr 142 | def _get_cassette_library_dir(self): 143 | return str(cassette_dir) 144 | 145 | # First run to fill cassette. 146 | test = run_testcase(MyTest)[0][0] 147 | assert len(test.cassette.requests) == 1 148 | assert not mock_matcher.called # nothing in cassette 149 | 150 | # Second run to call matcher. 151 | test = run_testcase(MyTest)[0][0] 152 | assert len(test.cassette.requests) == 1 153 | assert mock_matcher.called 154 | assert repr(mock_matcher.mock_calls[0]) == 'call(, )' 155 | 156 | 157 | def test_testcase_playback(tmpdir): 158 | cassette_dir = tmpdir.mkdir('cassettes') 159 | assert len(cassette_dir.listdir()) == 0 160 | 161 | # First test actually reads from the web. 162 | 163 | class MyTest(VCRTestCase): 164 | def test_foo(self): 165 | self.response = urlopen('http://example.com').read() 166 | def _get_cassette_library_dir(self): 167 | return str(cassette_dir) 168 | 169 | test = run_testcase(MyTest)[0][0] 170 | assert b'illustrative examples' in test.response 171 | assert len(test.cassette.requests) == 1 172 | assert test.cassette.play_count == 0 173 | 174 | # Second test reads from cassette. 175 | 176 | test2 = run_testcase(MyTest)[0][0] 177 | assert test.cassette is not test2.cassette 178 | assert b'illustrative examples' in test.response 179 | assert len(test2.cassette.requests) == 1 180 | assert test2.cassette.play_count == 1 181 | 182 | 183 | def run_testcase(testcase_class): 184 | """Run all the tests in a TestCase and return them.""" 185 | suite = defaultTestLoader.loadTestsFromTestCase(testcase_class) 186 | tests = list(suite._tests) 187 | result = TextTestRunner().run(suite) 188 | return tests, result 189 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,34,35,36,37},pypy{,3.5} 3 | 4 | [testenv] 5 | commands = {env:COVERAGE_CMD:} py.test 6 | deps = -rrequirements.txt 7 | {env:COVERAGE_DEP:} 8 | -------------------------------------------------------------------------------- /vcr_unittest/__init__.py: -------------------------------------------------------------------------------- 1 | from .testcase import VCRMixin, VCRTestCase 2 | -------------------------------------------------------------------------------- /vcr_unittest/testcase.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import inspect 4 | import logging 5 | import os 6 | import unittest 7 | import vcr 8 | 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class VCRMixin(object): 14 | """A TestCase mixin that provides VCR integration.""" 15 | vcr_enabled = True 16 | 17 | def setUp(self): 18 | super(VCRMixin, self).setUp() 19 | if self.vcr_enabled: 20 | kwargs = self._get_vcr_kwargs() 21 | myvcr = self._get_vcr(**kwargs) 22 | cm = myvcr.use_cassette(self._get_cassette_name()) 23 | self.cassette = cm.__enter__() 24 | self.addCleanup(cm.__exit__, None, None, None) 25 | 26 | def _get_vcr(self, **kwargs): 27 | if 'cassette_library_dir' not in kwargs: 28 | kwargs['cassette_library_dir'] = self._get_cassette_library_dir() 29 | return vcr.VCR(**kwargs) 30 | 31 | def _get_vcr_kwargs(self, **kwargs): 32 | return kwargs 33 | 34 | def _get_cassette_library_dir(self): 35 | testdir = os.path.dirname(inspect.getfile(self.__class__)) 36 | return os.path.join(testdir, 'cassettes') 37 | 38 | def _get_cassette_name(self): 39 | return '{0}.{1}.yaml'.format(self.__class__.__name__, 40 | self._testMethodName) 41 | 42 | 43 | class VCRTestCase(VCRMixin, unittest.TestCase): 44 | pass 45 | --------------------------------------------------------------------------------