Skip to content

Commit

Permalink
Merge pull request #712 from kevin1024/integrate-vcrpy-unittest
Browse files Browse the repository at this point in the history
Integrate vcrpy-unittest (alternative to #709)
  • Loading branch information
hartwork authored Jun 8, 2023
2 parents 96a6e91 + d613a81 commit f4316d2
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 5 deletions.
2 changes: 1 addition & 1 deletion docs/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Finally, register your class with VCR to use your new serializer.
import vcr
class BogoSerializer(object):
class BogoSerializer:
"""
Must implement serialize() and deserialize() methods
"""
Expand Down
70 changes: 67 additions & 3 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,73 @@ 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
<https://github.com/agriffis/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
<https://github.com/kevin1024/vcrpy/blob/master/vcr/unittest.py>`__
for the default implementations of these methods.

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
------------------
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_register_persister.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from vcr.persisters.filesystem import FilesystemPersister


class CustomFilesystemPersister(object):
class CustomFilesystemPersister:
"""Behaves just like default FilesystemPersister but adds .test extension
to the cassette file"""

Expand Down
195 changes: 195 additions & 0 deletions tests/unit/test_unittest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import os
from unittest import TextTestRunner, defaultTestLoader
from unittest.mock import MagicMock
from urllib.request import urlopen

from vcr.unittest import VCRTestCase


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()._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()._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 = MagicMock(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 = MagicMock(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 = MagicMock(return_value=True, __name__="MockMatcher")

class MyTest(VCRTestCase):
def test_foo(self):
self.response = urlopen("http://example.com").read()

def _get_vcr(self):
myvcr = super()._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(<Request (GET) http://example.com>, <Request (GET) http://example.com>)"
)


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
39 changes: 39 additions & 0 deletions vcr/unittest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import inspect
import os
import unittest

from .config import VCR


class VCRMixin:
"""A TestCase mixin that provides VCR integration."""

vcr_enabled = True

def setUp(self):
super().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(**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

0 comments on commit f4316d2

Please sign in to comment.