diff --git a/docs/mocker.rst b/docs/mocker.rst index 699b08c..383773c 100644 --- a/docs/mocker.rst +++ b/docs/mocker.rst @@ -47,6 +47,37 @@ If the position of the mock is likely to conflict with other arguments you can p >>> test_kw_function() 'resp' +Class Decorator +=============== + +Mocker can also be used to decorate a whole class. It works exactly like in case of decorating a normal function. +When used in this way they wrap every test method on the class. The mocker recognise methods that start with *test* as being test methods. +This is the same way that the `unittest.TestLoader` finds test methods by default. +It is possible that you want to use a different prefix for your tests. You can inform the mocker of the different prefix by setting `requests_mock.Mocker.TEST_PREFIX`: + +.. doctest:: + + >>> requests_mock.Mocker.TEST_PREFIX = 'foo' + >>> + >>> @requests_mock.Mocker() + ... class Thing(object): + ... def foo_one(self, m): + ... m.register_uri('GET', 'http://test.com', text='resp') + ... return requests.get('http://test.com').text + ... def foo_two(self, m): + ... m.register_uri('GET', 'http://test.com', text='resp') + ... return requests.get('http://test.com').text + ... + >>> + >>> Thing().foo_one() + 'resp' + >>> Thing().foo_two() + 'resp' + + +This behavior mimics how patchers from `mock` library works. + + Methods ======= diff --git a/requests_mock/mocker.py b/requests_mock/mocker.py index 581d029..c23db25 100644 --- a/requests_mock/mocker.py +++ b/requests_mock/mocker.py @@ -123,6 +123,10 @@ class MockerCore(object): class Mocker(MockerCore): """The standard entry point for mock Adapter loading. """ + + #: Defines with what should method name begin to be patched + TEST_PREFIX = 'test' + def __init__(self, **kwargs): """Create a new mocker adapter. @@ -141,7 +145,26 @@ class Mocker(MockerCore): def __exit__(self, type, value, traceback): self.stop() - def __call__(self, func): + def __call__(self, obj): + if isinstance(obj, type): + return self.decorate_class(obj) + + return self.decorate_callable(obj) + + def copy(self): + """Returns an exact copy of current mock + """ + m = Mocker( + kw=self._kw, + real_http=self._real_http + ) + return m + + def decorate_callable(self, func): + """Decorates a callable + + :param callable func: callable to decorate + """ @functools.wraps(func) def inner(*args, **kwargs): with self as m: @@ -155,5 +178,25 @@ class Mocker(MockerCore): return inner + def decorate_class(self, klass): + """Decorates methods in a class with request_mock + + Method will be decorated only if it name begins with `TEST_PREFIX` + + :param object klass: class which methods will be decorated + """ + for attr_name in dir(klass): + if not attr_name.startswith(self.TEST_PREFIX): + continue + + attr = getattr(klass, attr_name) + if not hasattr(attr, '__call__'): + continue + + m = self.copy() + setattr(klass, attr_name, m(attr)) + + return klass + mock = Mocker diff --git a/requests_mock/tests/test_mocker.py b/requests_mock/tests/test_mocker.py index 3f928df..2011c49 100644 --- a/requests_mock/tests/test_mocker.py +++ b/requests_mock/tests/test_mocker.py @@ -88,6 +88,70 @@ class MockerTests(base.TestCase): inner() self.assertMockStopped() + def test_with_class_decorator(self): + outer = self + + @requests_mock.mock() + class Decorated(object): + + def test_will_be_decorated(self, m): + outer.assertMockStarted() + outer._do_test(m) + + def will_not_be_decorated(self): + outer.assertMockStopped() + + decorated_class = Decorated() + + self.assertMockStopped() + decorated_class.test_will_be_decorated() + self.assertMockStopped() + decorated_class.will_not_be_decorated() + self.assertMockStopped() + + def test_with_class_decorator_and_custom_kw(self): + outer = self + + @requests_mock.mock(kw='custom_m') + class Decorated(object): + + def test_will_be_decorated(self, **kwargs): + outer.assertMockStarted() + outer._do_test(kwargs['custom_m']) + + def will_not_be_decorated(self): + outer.assertMockStopped() + + decorated_class = Decorated() + + self.assertMockStopped() + decorated_class.test_will_be_decorated() + self.assertMockStopped() + decorated_class.will_not_be_decorated() + self.assertMockStopped() + + @mock.patch.object(requests_mock.mock, 'TEST_PREFIX', 'foo') + def test_with_class_decorator_and_custom_test_prefix(self): + outer = self + + @requests_mock.mock() + class Decorated(object): + + def foo_will_be_decorated(self, m): + outer.assertMockStarted() + outer._do_test(m) + + def will_not_be_decorated(self): + outer.assertMockStopped() + + decorated_class = Decorated() + + self.assertMockStopped() + decorated_class.foo_will_be_decorated() + self.assertMockStopped() + decorated_class.will_not_be_decorated() + self.assertMockStopped() + @requests_mock.mock() def test_query_string(self, m): url = 'http://test.url/path' @@ -114,6 +178,13 @@ class MockerTests(base.TestCase): self.assertEqual(m.request_history, matcher.request_history) self.assertIs(m.last_request, matcher.last_request) + def test_copy(self): + mocker = requests_mock.mock(kw='foo', real_http=True) + copy_of_mocker = mocker.copy() + self.assertIsNot(copy_of_mocker, mocker) + self.assertEqual(copy_of_mocker._kw, mocker._kw) + self.assertEqual(copy_of_mocker._real_http, mocker._real_http) + class MockerHttpMethodsTests(base.TestCase):