From c13f33b1e0e0192592ff993e5a21acbb34c68035 Mon Sep 17 00:00:00 2001 From: Sebastian Pipping Date: Fri, 2 Jun 2023 16:25:42 +0200 Subject: [PATCH] Add unmodified vcrpy-unittest code Source commit is a2fd7625fde1ea15c8982759b07007aef40424b3. License is MIT just like vcrpy. --- docs/usage.rst | 69 +++++++++- vcr/unittest/__init__.py | 1 + vcr/unittest/testcase.py | 44 +++++++ vcr/unittest/tests/__init__.py | 0 vcr/unittest/tests/test_testcase.py | 188 ++++++++++++++++++++++++++++ 5 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 vcr/unittest/__init__.py create mode 100644 vcr/unittest/testcase.py create mode 100644 vcr/unittest/tests/__init__.py create mode 100644 vcr/unittest/tests/test_testcase.py diff --git a/docs/usage.rst b/docs/usage.rst index fc4c9e1..6d8f4ab 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -92,9 +92,72 @@ all Unittest Integration -------------------- -While it's possible to use the context manager or decorator forms with unittest, -there's also a ``VCRTestCase`` provided separately by `vcrpy-unittest -`__. +Inherit from ``VCRTestCase`` for automatic recording and playback of HTTP +interactions. + +.. code:: python + + from vcr_unittest import VCRTestCase + import requests + + class MyTestCase(VCRTestCase): + def test_something(self): + response = requests.get('http://example.com') + +Similar to how VCR.py returns the cassette from the context manager, +``VCRTestCase`` makes the cassette available as ``self.cassette``: + +.. code:: python + + self.assertEqual(len(self.cassette), 1) + self.assertEqual(self.cassette.requests[0].uri, 'http://example.com') + +By default cassettes will be placed in the ``cassettes`` subdirectory next to the +test, named according to the test class and method. For example, the above test +would read from and write to ``cassettes/MyTestCase.test_something.yaml`` + +The configuration can be modified by overriding methods on your subclass: +``_get_vcr_kwargs``, ``_get_cassette_library_dir`` and ``_get_cassette_name``. +To modify the ``VCR`` object after instantiation, for example to add a matcher, +you can hook on ``_get_vcr``, for example: + +.. code:: python + + class MyTestCase(VCRTestCase): + def _get_vcr(self, **kwargs): + myvcr = super(MyTestCase, self)._get_vcr(**kwargs) + myvcr.register_matcher('mymatcher', mymatcher) + myvcr.match_on = ['mymatcher'] + return myvcr + +See +`the source +`__ +for the default implementations of these methods, and `VCR.py`_ for more +information. + +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. + +VCRMixin +~~~~~~~~ + +In case inheriting from ``VCRTestCase`` is difficult because of an existing +class hierarchy containing tests in the base classes, inherit from ``VCRMixin`` +instead. + +.. code:: python + + from vcr_unittest import VCRMixin + import requests + import unittest + + class MyTestMixin(VCRMixin): + def test_something(self): + response = requests.get(self.url) + + class MyTestCase(MyTestMixin, unittest.TestCase): + url = 'http://example.com' + Pytest Integration ------------------ diff --git a/vcr/unittest/__init__.py b/vcr/unittest/__init__.py new file mode 100644 index 0000000..617ff7e --- /dev/null +++ b/vcr/unittest/__init__.py @@ -0,0 +1 @@ +from .testcase import VCRMixin, VCRTestCase diff --git a/vcr/unittest/testcase.py b/vcr/unittest/testcase.py new file mode 100644 index 0000000..094ee80 --- /dev/null +++ b/vcr/unittest/testcase.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, unicode_literals + +import inspect +import logging +import os +import unittest +import vcr + + +logger = logging.getLogger(__name__) + + +class VCRMixin(object): + """A TestCase mixin that provides VCR integration.""" + vcr_enabled = True + + def setUp(self): + super(VCRMixin, self).setUp() + if self.vcr_enabled: + kwargs = self._get_vcr_kwargs() + myvcr = self._get_vcr(**kwargs) + cm = myvcr.use_cassette(self._get_cassette_name()) + self.cassette = cm.__enter__() + self.addCleanup(cm.__exit__, None, None, None) + + def _get_vcr(self, **kwargs): + if 'cassette_library_dir' not in kwargs: + kwargs['cassette_library_dir'] = self._get_cassette_library_dir() + return vcr.VCR(**kwargs) + + def _get_vcr_kwargs(self, **kwargs): + return kwargs + + def _get_cassette_library_dir(self): + testdir = os.path.dirname(inspect.getfile(self.__class__)) + return os.path.join(testdir, 'cassettes') + + def _get_cassette_name(self): + return '{0}.{1}.yaml'.format(self.__class__.__name__, + self._testMethodName) + + +class VCRTestCase(VCRMixin, unittest.TestCase): + pass diff --git a/vcr/unittest/tests/__init__.py b/vcr/unittest/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vcr/unittest/tests/test_testcase.py b/vcr/unittest/tests/test_testcase.py new file mode 100644 index 0000000..ec3ff75 --- /dev/null +++ b/vcr/unittest/tests/test_testcase.py @@ -0,0 +1,188 @@ +from __future__ import absolute_import, unicode_literals + +import os +from mock import MagicMock as Mock +from unittest import defaultTestLoader, TextTestRunner +from vcr_unittest import VCRTestCase + +try: + from urllib2 import urlopen +except ImportError: + from urllib.request import urlopen + + +def test_defaults(): + + class MyTest(VCRTestCase): + def test_foo(self): + pass + + test = run_testcase(MyTest)[0][0] + expected_path = os.path.join(os.path.dirname(__file__), 'cassettes') + expected_name = 'MyTest.test_foo.yaml' + assert os.path.dirname(test.cassette._path) == expected_path + assert os.path.basename(test.cassette._path) == expected_name + + +def test_disabled(): + + # Baseline vcr_enabled = True + class MyTest(VCRTestCase): + def test_foo(self): + pass + test = run_testcase(MyTest)[0][0] + assert hasattr(test, 'cassette') + + # Test vcr_enabled = False + class MyTest(VCRTestCase): + vcr_enabled = False + def test_foo(self): + pass + test = run_testcase(MyTest)[0][0] + assert not hasattr(test, 'cassette') + + +def test_cassette_library_dir(): + + class MyTest(VCRTestCase): + def test_foo(self): + pass + def _get_cassette_library_dir(self): + return '/testing' + + test = run_testcase(MyTest)[0][0] + assert test.cassette._path.startswith('/testing/') + + +def test_cassette_name(): + + class MyTest(VCRTestCase): + def test_foo(self): + pass + def _get_cassette_name(self): + return 'my-custom-name' + + test = run_testcase(MyTest)[0][0] + assert os.path.basename(test.cassette._path) == 'my-custom-name' + + +def test_vcr_kwargs_overridden(): + + class MyTest(VCRTestCase): + def test_foo(self): + pass + def _get_vcr_kwargs(self): + kwargs = super(MyTest, self)._get_vcr_kwargs() + kwargs['record_mode'] = 'new_episodes' + return kwargs + + test = run_testcase(MyTest)[0][0] + assert test.cassette.record_mode == 'new_episodes' + + +def test_vcr_kwargs_passed(): + + class MyTest(VCRTestCase): + def test_foo(self): + pass + def _get_vcr_kwargs(self): + return super(MyTest, self)._get_vcr_kwargs( + record_mode='new_episodes', + ) + + test = run_testcase(MyTest)[0][0] + assert test.cassette.record_mode == 'new_episodes' + + +def test_vcr_kwargs_cassette_dir(): + + # Test that _get_cassette_library_dir applies if cassette_library_dir + # is absent from vcr kwargs. + class MyTest(VCRTestCase): + def test_foo(self): + pass + def _get_vcr_kwargs(self): + return dict( + record_mode='new_episodes', + ) + _get_cassette_library_dir = Mock(return_value='/testing') + test = run_testcase(MyTest)[0][0] + assert test.cassette._path.startswith('/testing/') + assert test._get_cassette_library_dir.call_count == 1 + + # Test that _get_cassette_library_dir is ignored if cassette_library_dir + # is present in vcr kwargs. + class MyTest(VCRTestCase): + def test_foo(self): + pass + def _get_vcr_kwargs(self): + return dict( + cassette_library_dir='/testing', + ) + _get_cassette_library_dir = Mock(return_value='/ignored') + test = run_testcase(MyTest)[0][0] + assert test.cassette._path.startswith('/testing/') + assert test._get_cassette_library_dir.call_count == 0 + + +def test_get_vcr_with_matcher(tmpdir): + cassette_dir = tmpdir.mkdir('cassettes') + assert len(cassette_dir.listdir()) == 0 + + mock_matcher = Mock(return_value=True) + + class MyTest(VCRTestCase): + def test_foo(self): + self.response = urlopen('http://example.com').read() + def _get_vcr(self): + myvcr = super(MyTest, self)._get_vcr() + myvcr.register_matcher('mymatcher', mock_matcher) + myvcr.match_on = ['mymatcher'] + return myvcr + def _get_cassette_library_dir(self): + return str(cassette_dir) + + # First run to fill cassette. + test = run_testcase(MyTest)[0][0] + assert len(test.cassette.requests) == 1 + assert not mock_matcher.called # nothing in cassette + + # Second run to call matcher. + test = run_testcase(MyTest)[0][0] + assert len(test.cassette.requests) == 1 + assert mock_matcher.called + assert repr(mock_matcher.mock_calls[0]) == 'call(, )' + + +def test_testcase_playback(tmpdir): + cassette_dir = tmpdir.mkdir('cassettes') + assert len(cassette_dir.listdir()) == 0 + + # First test actually reads from the web. + + class MyTest(VCRTestCase): + def test_foo(self): + self.response = urlopen('http://example.com').read() + def _get_cassette_library_dir(self): + return str(cassette_dir) + + test = run_testcase(MyTest)[0][0] + assert b'illustrative examples' in test.response + assert len(test.cassette.requests) == 1 + assert test.cassette.play_count == 0 + + # Second test reads from cassette. + + test2 = run_testcase(MyTest)[0][0] + assert test.cassette is not test2.cassette + assert b'illustrative examples' in test.response + assert len(test2.cassette.requests) == 1 + assert test2.cassette.play_count == 1 + + +def run_testcase(testcase_class): + """Run all the tests in a TestCase and return them.""" + suite = defaultTestLoader.loadTestsFromTestCase(testcase_class) + tests = list(suite._tests) + result = TextTestRunner().run(suite) + return tests, result