diff --git a/ddt.py b/ddt.py index 0754a8a..77f664e 100644 --- a/ddt.py +++ b/ddt.py @@ -11,6 +11,8 @@ import re from functools import wraps +import mock + try: import yaml except ImportError: # pragma: no cover @@ -27,6 +29,7 @@ DATA_ATTR = '%values' # store the data the test must run with FILE_ATTR = '%file_path' # store the path to JSON file UNPACK_ATTR = '%unpack' # remember that we have to unpack values +MOCK_ATTR = '%mockdata' # store mock oriented data try: @@ -52,6 +55,15 @@ def unpack(func): return func +def mockdata(func): + """ + Method decorator to add mock oriented functionality. + + """ + setattr(func, MOCK_ATTR, True) + return func + + def data(*values): """ Method decorator to add to your test methods. @@ -222,6 +234,46 @@ def _add_tests_from_data(cls, name, func, data): add_test(cls, test_name, func, value) +def mockdata_multiple_args(v): + """ + Convert mock `MagicMock` object from mock `patch` from passed arguments. + """ + values = () + + for el in list(v): + if isinstance(el, mock.mock._patch): + mock_obj = el.start() + values += (mock_obj,) + else: + values += (el,) + + return values + + +def mockdata_dict(v): + """ + Set user defined attributes as dict keys to mock `MagicMock` object. + """ + allowed_attributes = ['return_value', 'side_effect'] + + with v['patch'] as mock_object: + for attr in allowed_attributes: + defined_attr = v.get(attr, 'User did not define mock attribute.') + + if defined_attr is not 'User did not define mock attribute.': + setattr(mock_object, attr, defined_attr) + + return (mock_object,) + + +def mockdata_single_arg(arg): + """ + Return mock `MagicMock` object if passed argument is a mock `patch`. + """ + if isinstance(arg, mock.mock._patch): + return arg.start() + + def ddt(cls): """ Class decorator for subclasses of ``unittest.TestCase``. @@ -251,13 +303,31 @@ def ddt(cls): for i, v in enumerate(getattr(func, DATA_ATTR)): test_name = mk_test_name(name, getattr(v, "__name__", v), i) if hasattr(func, UNPACK_ATTR): + if isinstance(v, tuple) or isinstance(v, list): + + if hasattr(func, MOCK_ATTR): + # mock as multiple args + v = mockdata_multiple_args(v) + add_test(cls, test_name, func, *v) else: - # unpack dictionary - add_test(cls, test_name, func, **v) + + if hasattr(func, MOCK_ATTR): + # mock with attr as dict key + v = mockdata_dict(v) + add_test(cls, test_name, func, *v) + + else: + # unpack dictionary + add_test(cls, test_name, func, **v) else: + if hasattr(func, MOCK_ATTR): + # mock as single argument + v = mockdata_single_arg(v) + add_test(cls, test_name, func, v) + delattr(cls, name) elif hasattr(func, FILE_ATTR): file_attr = getattr(func, FILE_ATTR) diff --git a/test/test_functional.py b/test/test_functional.py index 51639b4..a5f65c7 100644 --- a/test/test_functional.py +++ b/test/test_functional.py @@ -4,7 +4,11 @@ import six import mock -from ddt import ddt, data, file_data +from ddt import ( + ddt, data, file_data, unpack, + mockdata, MOCK_ATTR, + mockdata_multiple_args, mockdata_dict, mockdata_single_arg +) from nose.tools import ( assert_true, assert_equal, assert_is_not_none, assert_raises ) @@ -70,6 +74,22 @@ def test_something_again(self, value): return value +@ddt +class MockDataDummy(object): + """ + Dummy class to test the mockdata decorator on + """ + + @data([mock.patch('ddt.ddt'), 5, AttributeError()]) + @unpack + @mockdata + def test_something(self, mock_obj, return_value, side_effect): + mock_obj.return_value = return_value + mock_obj.side_effect = side_effect + + return mock_obj + + def test_data_decorator(): """ Test the ``data`` method decorator @@ -320,3 +340,79 @@ def test_file_data_yaml_dict(self, value): for test in tests: method = getattr(obj, test) assert_raises(ValueError, method) + + +def test_feed_data_mockdata(): + """ + Test that data is fed to the decorated tests + """ + tests = list(filter(_is_test, MockDataDummy.__dict__)) + assert_equal(len(tests), 1) + + obj = MockDataDummy() + method_result = getattr(obj, tests[0])() + + assert_true(isinstance(type(method_result), type(mock.MagicMock))) + assert_equal(5, method_result.return_value) + + assert_equal( + AttributeError().__class__, method_result.side_effect.__class__ + ) + + +def test_mockdata_decorator(): + """ + Test that func decorated by `mockdata` has `MOCK_ATTR` (`%mockdata`). + """ + + def hello(): + pass + + mockdata(hello) + + assert_true(hello.__getattribute__(MOCK_ATTR)) + + +def test_mockdata_single_arg(): + """ + Test returning mock `MagicMock` object if passed argument is a mock `patch` + """ + arg = mock.patch('ddt.ddt') + result = mockdata_single_arg(arg) + assert_true(isinstance(type(result), type(mock.MagicMock))) + + arg = 3 + result = mockdata_single_arg(arg) + assert_equal(None, result) + + +def test_mockdata_dict(): + """ + Test that user defined attributes are set to mock `MagicMock` object. + """ + moke_attr = { + 'patch': mock.patch('ddt.ddt'), + 'return_value': 5, + 'side_effect': AttributeError() + } + + result = mockdata_dict(moke_attr) + mock_obj = result[0] # result is tuple + + assert_true(isinstance(type(mock_obj), type(mock.MagicMock))) + assert_equal(5, mock_obj.return_value) + assert_equal(AttributeError().__class__, mock_obj.side_effect.__class__) + + +def test_mockdata_multiple_args(): + """ + Test that mock `patch` args are converted to mock `MagicMock` objects. + """ + values = [mock.patch('ddt.ddt'), mock.patch('ddt.unpack'), 5, 'Mock'] + + result = list(mockdata_multiple_args(values)) + + assert_true(isinstance(type(result[0]), type(mock.MagicMock))) + assert_true(isinstance(type(result[1]), type(mock.MagicMock))) + assert_equal(values[2], result[2]) + assert_equal(values[3], result[3])