diff --git a/.noseids b/.noseids index cf87ab4..271385c 100644 Binary files a/.noseids and b/.noseids differ diff --git a/testdoubles/__init__.py b/testdoubles/__init__.py index 2a664dd..18932f7 100644 --- a/testdoubles/__init__.py +++ b/testdoubles/__init__.py @@ -109,3 +109,7 @@ def default_implementation(*args, **kwargs): obj = type(obj.__name__, obj.__bases__, attrs) return substitute(obj, qualified_name, spec) + +from testdoubles import utils + +__all__ = ('fake', 'utils') \ No newline at end of file diff --git a/testdoubles/fakes/callables.py b/testdoubles/fakes/callables.py index e27d66b..7a47ffd 100644 --- a/testdoubles/fakes/callables.py +++ b/testdoubles/fakes/callables.py @@ -2,9 +2,55 @@ # -*- coding: utf-8 -*- import inspect import sys +from testdoubles.utils import are_argspecs_identical python3 = sys.version_info[0] == 3 + +class CallableInternalAttributesBaseMixin(object): + @property + def __name__(self): + if not self.is_instance_method and not inspect.isfunction(self.live): + return self.live.__class__.__name__ + + return self.live.__name__ + + @property + def __self__(self): + try: + return self.live.__self__ + except AttributeError: + raise AttributeError("'function' object has no attribute '__self__'") + + @property + def __func__(self): + if not self.is_instance_method: + raise AttributeError("'function' object has no attribute '__func__'") + + if self.is_unbound_instance_method: + return None + + return self.fake.__func__ + + @property + def __code__(self): + return self.fake.__code__ + + @property + def __defaults__(self): + return self.fake.__defaults__ + + @property + def __globals__(self): + return self.fake.__globals__ + + def __getattribute__(self, item): + if item == '__doc__': + return self.live.__doc__ + + return super(CallableInternalAttributesBaseMixin, self).__getattribute__(item) + + if python3: class CallableIntrospectionMixin(object): @property @@ -14,10 +60,15 @@ def is_unbound_instance_method(self): return args[0] == 'self' and not inspect.ismethod(self.live) except IndexError: return False + except TypeError: + return False @property def is_instance_method(self): return inspect.ismethod(self.live) or self.is_unbound_instance_method + + class CallableInternalAttributesMixin(CallableInternalAttributesBaseMixin): + pass else: class CallableIntrospectionMixin(object): @property @@ -28,8 +79,38 @@ def is_unbound_instance_method(self): def is_instance_method(self): return inspect.ismethod(self.live) or self.is_unbound_instance_method -class FakeCallable(CallableIntrospectionMixin): - def __init__(self, live): + class CallableInternalAttributesMixin(CallableInternalAttributesBaseMixin): + @property + def im_self(self): + return self.__self__ + + @property + def im_class(self): + return self.__self__.__class__ + + @property + def func_code(self): + return self.__code__ + + @property + def func_doc(self): + return self.__doc__ + + @property + def func_name(self): + return self.live.__name__ + + @property + def func_defaults(self): + return self.__defaults__ + + @property + def func_globals(self): + return self.__globals__ + + +class FakeCallable(CallableIntrospectionMixin, CallableInternalAttributesMixin): + def __init__(self, live, inspect_args=False): if not callable(live): try: raise TypeError('%s is not callable.' % live.__name__) @@ -38,6 +119,15 @@ def __init__(self, live): self._live = live + if inspect_args: + if inspect.isbuiltin(self.live): + raise ValueError('Cannot inspect arguments of a builtin live object.') + + if not are_argspecs_identical(self.live, self.fake): + raise ValueError("The provided live object's arguments %s does not match %s" % ( + inspect.getargspec(self.live), inspect.getargspec(self.fake))) + + @property def live(self): return self._live diff --git a/testdoubles/utils.py b/testdoubles/utils.py new file mode 100644 index 0000000..06c534e --- /dev/null +++ b/testdoubles/utils.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import inspect + + +def get_keyword_arguments(argspec): + return argspec.args[-len(argspec.defaults):] if argspec.defaults else [] + + +def are_arguments_identical(argspec1, argspec2): + kwargs1 = get_keyword_arguments(argspec1) + kwargs2 = get_keyword_arguments(argspec2) + + arguments1 = set(argspec1.args) - set(kwargs1) + arguments2 = set(argspec2.args) - set(kwargs2) + + if len(arguments1) == len(arguments2) and not (argspec1.varargs or argspec2.varargs): + return True + elif any(_ for _ in arguments1) and argspec2.varargs or any(_ for _ in arguments2) and argspec1.varargs: + return True + + return False + + +def are_keyword_arguments_identical(argspec1, argspec2): + kwargs1 = get_keyword_arguments(argspec1) + kwargs2 = get_keyword_arguments(argspec2) + + if kwargs1 == kwargs2 and not (argspec1.keywords or argspec2.keywords): + return True + if any(_ for _ in kwargs1) and argspec2.keywords or any(_ for _ in kwargs2) and argspec1.keywords: + return True + + return False + + +def are_argspecs_identical(callable1, callable2): + argspec1 = inspect.getargspec(callable1) + argspec2 = inspect.getargspec(callable2) + + if argspec1 == argspec2: + return True + else: + return are_arguments_identical(argspec1, argspec2) and are_keyword_arguments_identical(argspec1, argspec2) + +__all__ = ('are_argspecs_identical', ) \ No newline at end of file diff --git a/tests/common/compat.py b/tests/common/compat.py index 8a8a3f7..5dcd122 100644 --- a/tests/common/compat.py +++ b/tests/common/compat.py @@ -1,13 +1,19 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import six +import sys + try: from unittest import mock except ImportError: import mock -try: - import unittest -except ImportError: - import unittest2 as unittest +if not six.PY3 and sys.version_info[1] == 6: + import unittest2 as unittest +else: + try: + import unittest + except ImportError: + import unittest2 as unittest -__all__ = ['mock'] \ No newline at end of file +__all__ = ['mock', 'unittest'] \ No newline at end of file diff --git a/tests/functional/fakes/test_fake_callable.py b/tests/functional/fakes/test_fake_callable.py index 3b96a75..8badb56 100644 --- a/tests/functional/fakes/test_fake_callable.py +++ b/tests/functional/fakes/test_fake_callable.py @@ -1,10 +1,372 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +from tests.common.compat import unittest from nose2.tools import such +import six from testdoubles.fakes.callables import FakeCallable from tests.common.layers import FunctionalTestsLayer from tests.common.compat import mock +with such.A("Fake Function object") as it: + it.uses(FunctionalTestsLayer) + + @it.should("have the same name as the live object") + def test_should_have_the_same_name_as_the_live_object(case): + def foo(): pass + + sut = FakeCallable(foo) + expected = foo.__name__ + + actual = sut.__name__ + + case.assertEqual(actual, expected) + + @it.should("have the same name as the live object when the live object is a callable class instance") + def test_should_have_the_same_name_as_the_live_object_when_the_live_object_is_a_callable_class_instance(case): + class Foo(object): + def __call__(self, *args, **kwargs): + pass + + sut = FakeCallable(Foo()) + expected = Foo.__name__ + + actual = sut.__name__ + + case.assertEqual(actual, expected) + + @it.should("have a reference to the instance if the method is bound") + def test_should_have_a_reference_to_the_instance_if_the_method_is_bound(case): + class Foo(object): + def bar(self): + pass + + live_bound_method = Foo().bar + expected = live_bound_method.__self__ + + sut = FakeCallable(live_bound_method) + + actual = sut.__self__ + + case.assertEqual(actual, expected) + + @it.should("raise an attribute error when accessing __self__ and the method is not a bound or unbound instance method") + def test_should_raise_an_attribute_error_when_accessing_self_and_the_method_is_not_a_bound_or_unbound_instance_method(case): + def foo(): + pass + + live_unbound_method = foo + + with case.assertRaisesRegexp(AttributeError, + r"'function' object has no attribute '__self__'"): + sut = FakeCallable(live_unbound_method) + + _ = sut.__self__ + + @it.should("raise an attribute error when attempting to use the im_self alias") + @unittest.skipUnless(six.PY3, 'Test should only be run under Python 3.x') + def test_should_raise_an_attribute_error_when_attempting_to_use_the_im_self_alias(case): + class Foo(object): + def bar(self): + pass + + live_unbound_method = Foo.bar + + sut = FakeCallable(live_unbound_method) + + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'im_self'"): + + _ = sut.im_self + + @it.should("raise an attribute error when attempting to use the im_class internal attribute") + @unittest.skipUnless(six.PY3, 'Test should only be run under Python 3.x') + def test_should_raise_an_attribute_error_when_attempting_to_use_the_im_class_internal_attribute(case): + class Foo(object): + def bar(self): + pass + + live_unbound_method = Foo.bar + + sut = FakeCallable(live_unbound_method) + + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'im_class'"): + + _ = sut.im_class + + @it.should("raise an attribute error when attempting to use the func_code internal attribute") + @unittest.skipUnless(six.PY3, 'Test should only be run under Python 3.x') + def test_should_raise_an_attribute_error_when_attempting_to_use_the_func_code_internal_attribute(case): + class Foo(object): + def bar(self): + pass + + live_unbound_method = Foo.bar + + sut = FakeCallable(live_unbound_method) + + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'func_code'"): + + _ = sut.func_code + + @it.should("raise an attribute error when attempting to use the func_doc internal attribute") + @unittest.skipUnless(six.PY3, 'Test should only be run under Python 3.x') + def test_should_raise_an_attribute_error_when_attempting_to_use_the_func_doc_internal_attribute(case): + class Foo(object): + def bar(self): + pass + + live_unbound_method = Foo.bar + + sut = FakeCallable(live_unbound_method) + + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'func_doc'"): + + _ = sut.func_doc + + @it.should("raise an attribute error when attempting to use the func_defaults internal attribute") + @unittest.skipUnless(six.PY3, 'Test should only be run under Python 3.x') + def test_should_raise_an_attribute_error_when_attempting_to_use_the_func_defaults_internal_attribute(case): + class Foo(object): + def bar(self): + pass + + live_unbound_method = Foo.bar + + sut = FakeCallable(live_unbound_method) + + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'func_defaults'"): + + _ = sut.func_defaults + + @it.should("raise an attribute error when attempting to use the func_globals internal attribute") + @unittest.skipUnless(six.PY3, 'Test should only be run under Python 3.x') + def test_should_raise_an_attribute_error_when_attempting_to_use_the_func_globals_internal_attribute(case): + class Foo(object): + def bar(self): + pass + + live_unbound_method = Foo.bar + + sut = FakeCallable(live_unbound_method) + + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'func_globals'"): + + _ = sut.func_globals + + @it.should("raise an attribute error when attempting to use the func_name internal attribute") + @unittest.skipUnless(six.PY3, 'Test should only be run under Python 3.x') + def test_should_raise_an_attribute_error_when_attempting_to_use_the_func_name_internal_attribute(case): + class Foo(object): + def bar(self): + pass + + live_unbound_method = Foo.bar + + sut = FakeCallable(live_unbound_method) + + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'func_name'"): + + _ = sut.func_name + + @it.should("have an attribute named im_self that is equal to the __self__ attribute") + @unittest.skipUnless(not six.PY3, 'Test should only be run under Python 2.x') + def test_should_have_an_attribute_named_im_self_that_is_equal_to_the_self_attribute(case): + class Foo(object): + def bar(self): + pass + + live_bound_method = Foo().bar + expected = live_bound_method.__self__ + + sut = FakeCallable(live_bound_method) + + actual = sut.im_self + + case.assertEqual(actual, expected) + + @it.should("have an attribute named im_class that is equal to the __self__ attribute's type") + @unittest.skipUnless(not six.PY3, 'Test should only be run under Python 2.x') + def test_should_have_an_attribute_named_im_self_that_is_equal_to_the_self_attribute_type(case): + class Foo(object): + def bar(self): + pass + + live_bound_method = Foo().bar + expected = live_bound_method.__self__.__class__ + + sut = FakeCallable(live_bound_method) + + actual = sut.im_class + + case.assertEqual(actual, expected) + + @it.should("have an attribute named func_code that is equal to the __code__ attribute") + @unittest.skipUnless(not six.PY3, 'Test should only be run under Python 2.x') + def test_should_have_an_attribute_named_func_code_that_is_equal_to_the_code_attribute(case): + class Foo(object): + def bar(self): + pass + + live_bound_method = Foo().bar + + sut = FakeCallable(live_bound_method) + expected = sut.__code__ + + actual = sut.func_code + + case.assertEqual(actual, expected) + + @it.should("have an attribute named func_doc that is equal to the __doc__ attribute") + @unittest.skipUnless(not six.PY3, 'Test should only be run under Python 2.x') + def test_should_have_an_attribute_named_func_doc_that_is_equal_to_the_doc_attribute(case): + class Foo(object): + def bar(self): + pass + + live_bound_method = Foo().bar + + sut = FakeCallable(live_bound_method) + expected = sut.__doc__ + + actual = sut.func_doc + + case.assertEqual(actual, expected) + + @it.should("have an attribute named func_defaults that is equal to the __defaults__ attribute") + @unittest.skipUnless(not six.PY3, 'Test should only be run under Python 2.x') + def test_should_have_an_attribute_named_func_defaults_that_is_equal_to_the_defaults_attribute(case): + class Foo(object): + def bar(self): + pass + + live_bound_method = Foo().bar + + sut = FakeCallable(live_bound_method) + expected = sut.__defaults__ + + actual = sut.func_defaults + + case.assertEqual(actual, expected) + + @it.should("have an attribute named func_globals that is equal to the __globals__ attribute") + @unittest.skipUnless(not six.PY3, 'Test should only be run under Python 2.x') + def test_should_have_an_attribute_named_func_globals_that_is_equal_to_the_globals_attribute(case): + class Foo(object): + def bar(self): + pass + + live_bound_method = Foo().bar + + sut = FakeCallable(live_bound_method) + expected = sut.__globals__ + + actual = sut.func_globals + + case.assertEqual(actual, expected) + + @it.should("have an attribute named func_name that is equal to the __name__ attribute") + @unittest.skipUnless(not six.PY3, 'Test should only be run under Python 2.x') + def test_should_have_an_attribute_named_func_name_that_is_equal_to_the_name_attribute(case): + class Foo(object): + def bar(self): + pass + + live_bound_method = Foo().bar + + sut = FakeCallable(live_bound_method) + expected = sut.__name__ + + actual = sut.func_name + + case.assertEqual(actual, expected) + + @it.should("have a reference to the fake unbound version of the method if the method is bound") + def test_should_have_a_reference_to_the_fake_unbound_version_of_the_method_if_the_method_is_bound(case): + class Foo(object): + def bar(self): + pass + + live_bound_method = Foo().bar + + sut = FakeCallable(live_bound_method) + expected = sut.fake.__func__ + actual = sut.__func__ + + case.assertEqual(actual, expected) + + @it.should("not have a reference to the fake unbound version of the method if the method is unbound") + def test_should_not_have_a_reference_to_the_fake_unbound_version_of_the_method_if_the_method_is_unbound(case): + class Foo(object): + def bar(self): + pass + + live_unbound_method = Foo.bar + + sut = FakeCallable(live_unbound_method) + expected = None + + actual = sut.__func__ + + case.assertEqual(actual, expected) + + @it.should("raise an attribute error when accessing __func__ and the method is not a bound or unbound instance method") + def test_should_raise_an_attribute_error_when_accessing_func_and_the_method_is_not_a_bound_or_unbound_instance_method(case): + def foo(): + pass + + live_unbound_method = foo + + with case.assertRaisesRegexp(AttributeError, + r"'function' object has no attribute '__func__'"): + sut = FakeCallable(live_unbound_method) + + _ = sut.__func__ + + @it.should("have the same docstring as the live callable") + def test_should_have_the_same_docstring_as_the_live_callable(case): + def foo(): + """Docstring""" + pass + + sut = FakeCallable(foo) + expected = foo.__doc__ + + actual = sut.__doc__ + + case.assertEqual(actual, expected) + + @it.should("have the same code object as the fake callable") + def test_should_have_the_same_docstring_as_the_live_callable(case): + def foo(): + pass + + sut = FakeCallable(foo) + expected = sut.fake.__code__ + + actual = sut.__code__ + + case.assertEqual(actual, expected) + + @it.should("have the same default values for keyword arguments as the fake callable") + def test_should_have_the_same_default_values_for_keyword_arguments_as_the_fake_callable(case): + def foo(): + pass + + sut = FakeCallable(foo) + expected = sut.fake.__defaults__ + + actual = sut.__defaults__ + + case.assertEqual(actual, expected) + + it.createTests(globals()) + with such.A("Fake Function's initialization method") as it: it.uses(FunctionalTestsLayer) @@ -13,6 +375,23 @@ def test_should_raise_a_TypeError_when_the_provided_live_object_is_not_callable( with case.assertRaisesRegexp(TypeError, r"[a-zA-Z1-9_]* is not callable"): FakeCallable(mock.NonCallableMagicMock()) + @it.should("raise a ValueError when the provided live object does not match the argspec") + def test_should_raise_a_ValueError_when_the_provided_live_object_does_not_match_the_argspec(case): + with case.assertRaisesRegexp(ValueError, r"The provided live object's arguments ArgSpec\((?:[a-zA-Z1-9_]+=.+(?:, |(?=\))))+\) does not match ArgSpec\((?:[a-zA-Z1-9_]+=.+(?:, |(?=\))))+\)"): + def foo(): pass + + FakeCallable(foo, inspect_args=True) + + @it.should("not raise a ValueError when arguments inspection is opted out.") + def test_should_not_raise_a_ValueError_when_argument_inspection_is_opted_out(case): + def foo(): pass + + try: + FakeCallable(foo, inspect_args=False) + except ValueError: + case.fail() + + it.createTests(globals()) with such.A("Fake Function's object initialization method") as it: diff --git a/tests/functional/utils/__init__.py b/tests/functional/utils/__init__.py new file mode 100644 index 0000000..a5682fb --- /dev/null +++ b/tests/functional/utils/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/tests/functional/utils/test_are_argspecs_identical.py b/tests/functional/utils/test_are_argspecs_identical.py new file mode 100644 index 0000000..4bb41e8 --- /dev/null +++ b/tests/functional/utils/test_are_argspecs_identical.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import mock +from nose2.tools import such +from testdoubles.utils import are_argspecs_identical +from tests.common.layers import FunctionalTestsLayer + +with such.A('keyword arguments comparison method') as it: + it.uses(FunctionalTestsLayer) + + @it.should("return true if the argspecs are completely identical") + def test_should_return_true_if_the_argspecs_are_completely_identical(case): + def fake_callable1(a, k=mock.DEFAULT): + pass + + def fake_callable2(a, k=mock.DEFAULT): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertTrue(actual) + + @it.should( + 'return true if the argspecs are not completely identical but have the same number of positional arguments') + def test_should_return_true_if_the_argspecs_are_not_completely_identical_but_have_the_same_number_of_positional_arguments(case): + def fake_callable1(a): + pass + + def fake_callable2(b): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertTrue(actual) + + @it.should( + 'return true if the argspecs are not completely identical but the first method has a varargs argument') + def test_should_return_true_if_the_argspecs_are_not_completely_identical_but_the_first_method_has_a_varargs_argument(case): + def fake_callable1(*args): + pass + + def fake_callable2(a): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertTrue(actual) + + @it.should( + 'return true if the argspecs are not completely identical but the second method has a varargs argument') + def test_should_return_true_if_the_argspecs_are_not_completely_identical_but_the_second_method_has_a_varargs_argument(case): + def fake_callable1(a): + pass + + def fake_callable2(*args): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertTrue(actual) + + @it.should("return true if both methods have exactly the same keyword arguments") + def test_should_return_true_if_both_methods_have_exactly_the_same_keyword_arguments(case): + def fake_callable1(a, k=mock.DEFAULT): + pass + + def fake_callable2(b, k=mock.DEFAULT): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertTrue(actual) + + @it.should("return true if the first method has a kwargs argument and the second method has keyword arguments") + def test_should_return_true_if_the_first_method_has_a_kwargs_argument_and_the_second_method_has_keyword_arguments(case): + def fake_callable1(**kwargs): + pass + + def fake_callable2(k=mock.DEFAULT): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertTrue(actual) + + @it.should("return true if the first method has keyword arguments and the second method has a kwargs argument ") + def test_should_return_true_if_the_first_method_has_has_keyword_arguments_and_the_second_method_a_kwargs_argument(case): + def fake_callable1(k=mock.DEFAULT): + pass + + def fake_callable2(**kwargs): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertTrue(actual) + + @it.should( + 'return false if the first method has a varargs argument and the second method has no positional arguments') + def test_should_return_false_if_the_first_method_has_a_varargs_argument_and_the_second_method_has_no_positional_arguments(case): + def fake_callable1(*args): + pass + + def fake_callable2(k=mock.DEFAULT): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertEqual(actual, False) + + @it.should( + 'return false if the first method has no positional arguments and the second method has a varargs argument') + def test_should_return_false_if_the_first_method_has_no_positional_arguments_and_the_second_method_has_a_varargs_argument(case): + def fake_callable1(k=mock.DEFAULT): + pass + + def fake_callable2(*args): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertEqual(actual, False) + + @it.should('return false if the first method has a kwargs argument and the second method has no keyword arguments') + def test_should_return_false_if_the_first_method_has_a_kwargs_argument_and_the_second_method_has_no_keyword_arguments(case): + def fake_callable1(b, **kwargs): + pass + + def fake_callable2(a): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertEqual(actual, False) + + @it.should( + 'return false if the first method has no keyword arguments and the second method has a kwargs argument') + def test_should_return_false_if_the_first_method_has_no_keyword_arguments_and_the_second_method_has_a_kwargs_argument(case): + def fake_callable1(a): + pass + + def fake_callable2(b, **kwargs): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertEqual(actual, False) + + @it.should('return false if both methods have different keyword arguments') + def test_should_return_false_if_both_methods_have_different_keyword_arguments(case): + def fake_callable1(a, k=mock.DEFAULT): + pass + + def fake_callable2(b, kk=mock.DEFAULT): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertEqual(actual, False) + + @it.should('return false if the first method has a more positional arguments than the second method') + def test_should_return_false_if_the_first_method_has_more_positional_arguments_than_the_second_method(case): + def fake_callable1(a): + pass + + def fake_callable2(a, b): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertEqual(actual, False) + + @it.should('return false if the second method has a more positional arguments than the first method') + def test_should_return_false_if_the_second_method_has_more_positional_arguments_than_the_first_method(case): + def fake_callable1(a, b): + pass + + def fake_callable2(a): + pass + + actual = are_argspecs_identical(fake_callable1, fake_callable2) + + case.assertEqual(actual, False) + + it.createTests(globals()) \ No newline at end of file diff --git a/tests/unit/fakes/support/fakes.py b/tests/unit/fakes/support/fakes.py new file mode 100644 index 0000000..e697f3e --- /dev/null +++ b/tests/unit/fakes/support/fakes.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from tests.common.compat import mock + + +def fake_live_bound_callable(): + mocked_callable = mock.MagicMock() + self = mock.PropertyMock() + type(mocked_callable).__self__ = self + defaults = mock.PropertyMock() + type(mocked_callable).__defaults__ = defaults + mocked_callable.__name__ = 'fake_live_bound_callable' + return mocked_callable, self + + +def fake_live_unbound_callable(): + mocked_callable = mock.MagicMock() + self = mock.PropertyMock() + self.return_value = None + type(mocked_callable).__self__ = self + return mocked_callable, self \ No newline at end of file diff --git a/tests/unit/fakes/support/mocks.py b/tests/unit/fakes/support/mocks.py new file mode 100644 index 0000000..f653eb4 --- /dev/null +++ b/tests/unit/fakes/support/mocks.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from testdoubles.fakes import callables + + +def mock_are_argspecs_identical(case, mocked_are_argspecs_identical): + try: + old_init_globals = callables.FakeCallable.__init__.__globals__ + callables.FakeCallable.__init__.__globals__['are_argspecs_identical'] = mocked_are_argspecs_identical + except AttributeError: + old_init_globals = callables.FakeCallable.__init__.func_globals + callables.FakeCallable.__init__.func_globals['are_argspecs_identical'] = mocked_are_argspecs_identical + case.old_init_globals = old_init_globals + + +def unmock_are_argspecs_identical(case): + try: + callables.FakeCallable.__init__.__globals__.update(case.old_init_globals) + except AttributeError: + callables.FakeCallable.__init__.func_globals.update(case.old_init_globals) \ No newline at end of file diff --git a/tests/unit/fakes/support/stubs.py b/tests/unit/fakes/support/stubs.py new file mode 100644 index 0000000..48ef648 --- /dev/null +++ b/tests/unit/fakes/support/stubs.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from imp import reload +import sys +import mock +from testdoubles.fakes import callables + + +def stub_callable(case, return_value): + try: + case.old_callable = __builtins__['callable'] + __builtins__['callable'] = lambda c: return_value + except TypeError: + case.old_callable = __builtins__.callable + __builtins__.callable = lambda c: return_value + + +def unstub_callable(case): + try: + __builtins__['callable'] = case.old_callable + except TypeError: + __builtins__.callable = case.old_callable + + +def stub_are_argspecs_identical(case, return_value): + try: + old_init_globals = callables.FakeCallable.__init__.__globals__ + callables.FakeCallable.__init__.__globals__['are_argspecs_identical'] = lambda _, __: return_value + except AttributeError: + old_init_globals = callables.FakeCallable.__init__.func_globals + callables.FakeCallable.__init__.func_globals['are_argspecs_identical'] = lambda _, __: return_value + case.old_init_globals = old_init_globals + + +def unstub_are_argspecs_identical(case): + try: + callables.FakeCallable.__init__.__globals__.update(case.old_init_globals) + except AttributeError: + callables.FakeCallable.__init__.func_globals.update(case.old_init_globals) + + +def stub_python_version(case, major_version): + stubbed_sys = mock.MagicMock() + stubbed_sys.version_info = (major_version, ) + case.patch_python_version = mock.patch.dict(sys.modules, sys=stubbed_sys) + case.patch_python_version.start() + reload(callables) + + +def unstub_python_version(case): + case.patch_python_version.stop() + reload(callables) \ No newline at end of file diff --git a/tests/unit/fakes/test_fake_callable.py b/tests/unit/fakes/test_fake_callable.py index 6981d2b..0f2237b 100644 --- a/tests/unit/fakes/test_fake_callable.py +++ b/tests/unit/fakes/test_fake_callable.py @@ -1,58 +1,473 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from imp import reload -import sys +from inspect import ArgSpec from nose2.tools import such from testdoubles.fakes import callables from tests.common.compat import mock from tests.common.layers import UnitTestsLayer +from tests.unit.fakes.support.fakes import fake_live_bound_callable, fake_live_unbound_callable +from tests.unit.fakes.support.mocks import mock_are_argspecs_identical, unmock_are_argspecs_identical +from tests.unit.fakes.support.stubs import stub_callable, unstub_callable, stub_are_argspecs_identical, unstub_are_argspecs_identical, stub_python_version, unstub_python_version with such.A("Fake Function object") as it: it.uses(UnitTestsLayer) @it.has_test_setup def setup(case): - try: - case.old_callable = __builtins__['callable'] - __builtins__['callable'] = lambda c: True - except TypeError: - case.old_callable = __builtins__.callable - __builtins__.callable = lambda c: True + stub_callable(case, True) @it.has_test_teardown def teardown(case): - try: - __builtins__['callable'] = case.old_callable - except TypeError: - __builtins__.callable = case.old_callable + unstub_callable(case) @it.should("be callable") def test_should_be_callable(case): sut = callables.FakeCallable(mock.DEFAULT) - case.assertTrue(case.old_callable(sut)) + + callable = case.old_callable + case.assertTrue(callable(sut)) + + with it.having('a python 3.x runtime2'): + @it.has_test_setup + def setup(case): + stub_callable(case, True) + stub_python_version(case, 3) + + @it.has_test_teardown + def teardown(case): + unstub_python_version(case) + unstub_callable(case) + + @it.should("have the same name as the live object") + def test_should_have_the_same_name_as_the_live_object(case): + with mock.patch('inspect.ismethod', return_value=True): + with mock.patch('inspect.isfunction', return_value=True): + sut = callables.FakeCallable(object) + expected = object.__name__ + + actual = sut.__name__ + + case.assertEqual(actual, expected) + + @it.should("have the same name as the live object when the live object is a callable class instance") + def test_should_have_the_same_name_as_the_live_object_when_the_live_object_is_a_callable_class_instance(case): + with mock.patch('inspect.ismethod', return_value=False): + with mock.patch('inspect.isfunction', return_value=False): + with mock.patch('inspect.getargspec', return_value=[]): + sut = callables.FakeCallable(mock.DEFAULT) + expected = mock.DEFAULT.__class__.__name__ + + actual = sut.__name__ + + case.assertEqual(actual, expected) + + @it.should("have a reference to the instance if the method is bound") + def test_should_have_a_reference_to_the_instance_if_the_method_is_bound(case): + live_callable, _ = fake_live_bound_callable() + expected = live_callable.__self__ + + sut = callables.FakeCallable(live_callable) + + actual = sut.__self__ + + case.assertEqual(actual, expected) + + @it.should("raise an attribute error when accessing __self__ and the method is not a bound or unbound instance method") + def test_should_raise_an_attribute_error_when_accessing_self_and_the_method_is_not_a_bound_or_unbound_instance_method(case): + with case.assertRaisesRegexp(AttributeError, + r"'function' object has no attribute '__self__'"): + sut = callables.FakeCallable(mock.DEFAULT) + + _ = sut.__self__ + + @it.should("raise an attribute error when attempting to use the im_self alias") + def test_should_raise_an_attribute_error_when_attempting_to_use_the_im_self_alias(case): + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'im_self'"): + sut = callables.FakeCallable(mock.DEFAULT) + + _ = sut.im_self + + @it.should("raise an attribute error when attempting to use the im_class internal attribute") + def test_should_raise_an_attribute_error_when_attempting_to_use_the_im_class_internal_attribute(case): + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'im_class'"): + sut = callables.FakeCallable(mock.DEFAULT) + + _ = sut.im_class + + @it.should("raise an attribute error when attempting to use the func_code internal attribute") + def test_should_raise_an_attribute_error_when_attempting_to_use_the_func_code_internal_attribute(case): + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'func_code'"): + sut = callables.FakeCallable(mock.DEFAULT) + + _ = sut.func_code + + @it.should("raise an attribute error when attempting to use the func_doc internal attribute") + def test_should_raise_an_attribute_error_when_attempting_to_use_the_func_doc_internal_attribute(case): + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'func_doc'"): + sut = callables.FakeCallable(mock.DEFAULT) + + _ = sut.func_doc + + @it.should("raise an attribute error when attempting to use the func_name internal attribute") + def test_should_raise_an_attribute_error_when_attempting_to_use_the_func_name_internal_attribute(case): + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'func_name'"): + sut = callables.FakeCallable(mock.DEFAULT) + + _ = sut.func_name + + @it.should("raise an attribute error when attempting to use the func_defaults internal attribute") + def test_should_raise_an_attribute_error_when_attempting_to_use_the_func_defaults_internal_attribute(case): + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'func_defaults'"): + sut = callables.FakeCallable(mock.DEFAULT) + + _ = sut.func_defaults + + @it.should("raise an attribute error when attempting to use the func_globals internal attribute") + def test_should_raise_an_attribute_error_when_attempting_to_use_the_func_globals_internal_attribute(case): + with case.assertRaisesRegexp(AttributeError, + r"'FakeCallable' object has no attribute 'func_globals'"): + sut = callables.FakeCallable(mock.DEFAULT) + + _ = sut.func_globals + + @it.should("have a reference to the fake unbound version of the method if the method is bound") + def test_should_have_a_reference_to_the_fake_unbound_version_of_the_method_if_the_method_is_bound(case): + live_callable, _ = fake_live_bound_callable() + + sut = callables.FakeCallable(live_callable) + expected = sut.fake.__func__ + with mock.patch('inspect.ismethod', return_value=True): + with mock.patch('inspect.getargspec', return_value=ArgSpec(['self'], None, None, None)): + actual = sut.__func__ + + case.assertEqual(actual, expected) + + @it.should("not have a reference to the fake unbound version of the method if the method is unbound") + def test_should_not_have_a_reference_to_the_fake_unbound_version_of_the_method_if_the_method_is_unbound(case): + live_callable, _ = fake_live_unbound_callable() + sut = callables.FakeCallable(live_callable) + expected = None + with mock.patch('inspect.ismethod', return_value=False): + with mock.patch('inspect.getargspec', return_value=ArgSpec(['self'], None, None, None)): + actual = sut.__func__ + + case.assertEqual(actual, expected) + + @it.should("raise an attribute error when accessing __func__ and the method is not a bound or unbound instance method") + def test_should_raise_an_attribute_error_when_accessing_func_and_the_method_is_not_a_bound_or_unbound_instance_method(case): + with case.assertRaisesRegexp(AttributeError, + r"'function' object has no attribute '__func__'"): + sut = callables.FakeCallable(mock.DEFAULT) + + _ = sut.__func__ + + @it.should("have the same docstring as the live callable") + def test_should_have_the_same_docstring_as_the_live_callable(case): + sut = callables.FakeCallable(mock.DEFAULT) + expected = mock.DEFAULT.__doc__ + + actual = sut.__doc__ + + case.assertEqual(actual, expected) + + @it.should("have the same code object as the fake callable") + def test_should_have_the_same_docstring_as_the_live_callable(case): + sut = callables.FakeCallable(mock.DEFAULT) + expected = sut.fake.__code__ + + actual = sut.__code__ + + case.assertEqual(actual, expected) + + @it.should("have the same default values for keyword arguments as the fake callable") + def test_should_have_the_same_default_values_for_keyword_arguments_as_the_fake_callable(case): + sut = callables.FakeCallable(mock.DEFAULT) + expected = sut.fake.__defaults__ + + actual = sut.__defaults__ + + case.assertEqual(actual, expected) + + @it.should("have the same globals as the fake callable") + def test_should_have_the_same_default_values_for_keyword_arguments_as_the_fake_callable(case): + sut = callables.FakeCallable(mock.DEFAULT) + expected = sut.fake.__globals__ + + actual = sut.__globals__ + + case.assertEqual(actual, expected) + + with it.having('a python 2.x runtime2'): + @it.has_test_setup + def setup(case): + stub_callable(case, True) + stub_python_version(case, 2) + + @it.has_test_teardown + def teardown(case): + unstub_python_version(case) + unstub_callable(case) + + @it.should("have the same name as the live object") + def test_should_have_the_same_name_as_the_live_object(case): + with mock.patch('inspect.ismethod', return_value=True): + with mock.patch('inspect.isfunction', return_value=True): + sut = callables.FakeCallable(object) + expected = object.__name__ + + actual = sut.__name__ + + case.assertEqual(actual, expected) + + @it.should("have the same name as the live object when the live object is a callable class instance") + def test_should_have_the_same_name_as_the_live_object_when_the_live_object_is_a_callable_class_instance(case): + with mock.patch('inspect.ismethod', return_value=False): + with mock.patch('inspect.isfunction', return_value=False): + sut = callables.FakeCallable(mock.DEFAULT) + expected = mock.DEFAULT.__class__.__name__ + + actual = sut.__name__ + + case.assertEqual(actual, expected) + + @it.should("have a reference to the instance if the method is bound") + def test_should_have_a_reference_to_the_instance_if_the_method_is_bound(case): + live_callable, _ = fake_live_bound_callable() + expected = live_callable.__self__ + + sut = callables.FakeCallable(live_callable) + + actual = sut.__self__ + + case.assertEqual(actual, expected) + + @it.should("raise an attribute error when accessing __self__ and the method is not a bound or unbound instance method") + def test_should_raise_an_attribute_error_when_accessing_self_and_the_method_is_not_a_bound_or_unbound_instance_method(case): + with case.assertRaisesRegexp(AttributeError, + r"'function' object has no attribute '__self__'"): + sut = callables.FakeCallable(mock.DEFAULT) + + _ = sut.__self__ + + @it.should("have an attribute named im_self that is equal to the __self__ attribute") + def test_should_have_an_attribute_named_im_self_that_is_equal_to_the_self_attribute(case): + live_callable, _ = fake_live_bound_callable() + expected = live_callable.__self__ + + sut = callables.FakeCallable(live_callable) + + actual = sut.im_self + + case.assertEqual(actual, expected) + + @it.should("have an attribute named func_code that is equal to the __code__ attribute") + def test_should_have_an_attribute_named_func_code_that_is_equal_to_the_code_attribute(case): + + sut = callables.FakeCallable(mock.DEFAULT) + expected = sut.__code__ + + actual = sut.func_code + + case.assertEqual(actual, expected) + + @it.should("have an attribute named func_doc that is equal to the __doc__ attribute") + def test_should_have_an_attribute_named_func_doc_that_is_equal_to_the_doc_attribute(case): + + sut = callables.FakeCallable(mock.DEFAULT) + expected = sut.__doc__ + + actual = sut.func_doc + + case.assertEqual(actual, expected) + + @it.should("have an attribute named im_class that is equal to the __self__ attribute's type") + def test_should_have_an_attribute_named_im_self_that_is_equal_to_the_self_attribute_type(case): + live_callable, _ = fake_live_bound_callable() + expected = live_callable.__self__.__class__ + + sut = callables.FakeCallable(live_callable) + + actual = sut.im_class + + case.assertEqual(actual, expected) + + @it.should("have an attribute named func_name that is equal to the __name__ attribute") + def test_should_have_an_attribute_named_func_name_that_is_equal_to_the_name_attribute(case): + live_callable, _ = fake_live_bound_callable() + expected = live_callable.__name__ + + sut = callables.FakeCallable(live_callable) + + actual = sut.func_name + + case.assertEqual(actual, expected) + + @it.should("have an attribute named func_defaults that is equal to the __defaults__ attribute") + def test_should_have_an_attribute_named_func_defaults_that_is_equal_to_the_defaults_attribute(case): + live_callable, _ = fake_live_bound_callable() + + sut = callables.FakeCallable(live_callable) + expected = sut.__defaults__ + + actual = sut.func_defaults + + case.assertEqual(actual, expected) + + @it.should("have an attribute named func_globals that is equal to the __globals__ attribute") + def test_should_have_an_attribute_named_func_globals_that_is_equal_to_the_globals_attribute(case): + live_callable, _ = fake_live_bound_callable() + + sut = callables.FakeCallable(live_callable) + expected = sut.__globals__ + + actual = sut.func_globals + + case.assertEqual(actual, expected) + + @it.should("have a reference to the fake unbound version of the method if the method is bound") + def test_should_have_a_reference_to_the_fake_unbound_version_of_the_method_if_the_method_is_bound(case): + live_callable, _ = fake_live_bound_callable() + + sut = callables.FakeCallable(live_callable) + expected = sut.fake.__func__ + with mock.patch('inspect.ismethod', return_value=True): + actual = sut.__func__ + + case.assertEqual(actual, expected) + + @it.should("not have a reference to the fake unbound version of the method if the method is unbound") + def test_should_not_have_a_reference_to_the_fake_unbound_version_of_the_method_if_the_method_is_unbound(case): + live_callable, _ = fake_live_unbound_callable() + sut = callables.FakeCallable(live_callable) + expected = None + + with mock.patch('inspect.ismethod', return_value=True): + actual = sut.__func__ + + case.assertEqual(actual, expected) + + @it.should("raise an attribute error when accessing __func__ and the method is not a bound or unbound instance method") + def test_should_raise_an_attribute_error_when_accessing_func_and_the_method_is_not_a_bound_or_unbound_instance_method(case): + with case.assertRaisesRegexp(AttributeError, + r"'function' object has no attribute '__func__'"): + sut = callables.FakeCallable(mock.DEFAULT) + + with mock.patch('inspect.ismethod', return_value=False): + _ = sut.__func__ + + @it.should("have the same docstring as the live callable") + def test_should_have_the_same_docstring_as_the_live_callable(case): + sut = callables.FakeCallable(mock.DEFAULT) + expected = mock.DEFAULT.__doc__ + + actual = sut.__doc__ + + case.assertEqual(actual, expected) + + @it.should("have the same code object as the fake callable") + def test_should_have_the_same_docstring_as_the_live_callable(case): + sut = callables.FakeCallable(mock.DEFAULT) + expected = sut.fake.__code__ + + actual = sut.__code__ + + case.assertEqual(actual, expected) + + @it.should("have the same default values for keyword arguments as the fake callable") + def test_should_have_the_same_default_values_for_keyword_arguments_as_the_fake_callable(case): + sut = callables.FakeCallable(mock.DEFAULT) + expected = sut.fake.__defaults__ + + actual = sut.__defaults__ + + case.assertEqual(actual, expected) + + @it.should("have the same globals as the fake callable") + def test_should_have_the_same_default_values_for_keyword_arguments_as_the_fake_callable(case): + sut = callables.FakeCallable(mock.DEFAULT) + expected = sut.fake.__globals__ + + actual = sut.__globals__ + + case.assertEqual(actual, expected) it.createTests(globals()) -with such.A("Fake Function's live property") as it: +with such.A("Fake Function's initialization method") as it: it.uses(UnitTestsLayer) @it.has_test_setup def setup(case): - try: - case.old_callable = __builtins__['callable'] - __builtins__['callable'] = lambda c: True - except TypeError: - case.old_callable = __builtins__.callable - __builtins__.callable = lambda c: True + stub_callable(case, True) @it.has_test_teardown def teardown(case): + unstub_callable(case) + + @it.should("raise a value error when inspecting arguments of a builtin callable") + def test_should_raise_a_value_error_when_inspecting_arguments_of_a_builtin_callable(case): + with case.assertRaisesRegexp(ValueError, + r"Cannot inspect arguments of a builtin live object."): + with mock.patch('inspect.isbuiltin', return_value=True): + callables.FakeCallable(mock.DEFAULT, inspect_args=True) + + @it.should("raise a value error when the provided live object does not match the argspec") + def test_should_raise_a_value_error_when_the_provided_live_object_does_not_match_the_argspec(case): + stub_are_argspecs_identical(case, False) + + with case.assertRaisesRegexp(ValueError, + r"The provided live object's arguments ArgSpec\((?:[a-zA-Z1-9_]+=.+(?:, |(?=\))))+\) does not match ArgSpec\((?:[a-zA-Z1-9_]+=.+(?:, |(?=\))))+\)"): + with mock.patch('inspect.getargspec', return_value=ArgSpec(['a'], None, None, (None,))): + callables.FakeCallable(mock.DEFAULT, inspect_args=True) + + unstub_are_argspecs_identical(case) + + @it.should("not raise a value error when the provided live object matches the argspec") + def test_should_not_raise_a_value_error_when_the_provided_live_object_matches_the_argspec(case): + stub_are_argspecs_identical(case, True) + try: - __builtins__['callable'] = case.old_callable - except TypeError: - __builtins__.callable = case.old_callable + callables.FakeCallable(mock.DEFAULT, inspect_args=True) + except ValueError: + case.fail() + + unstub_are_argspecs_identical(case) + + @it.should("not inspect the argspec of the live object when argument inspection is opted out") + def test_should_not_inspect_the_argspec_of_the_live_object_when_argument_inspection_is_opted_out(case): + mocked_are_argspecs_identical = mock.Mock() + + mock_are_argspecs_identical(case, mocked_are_argspecs_identical) + + try: + callables.FakeCallable(mock.DEFAULT, inspect_args=False) + except ValueError: + pass + + mocked_are_argspecs_identical.assert_has_calls([]) + + unmock_are_argspecs_identical(case) + + it.createTests(globals()) + +with such.A("Fake Function's live property") as it: + it.uses(UnitTestsLayer) + + @it.has_test_setup + def setup(case): + stub_callable(case, True) + + @it.has_test_teardown + def teardown(case): + unstub_callable(case) @it.should("have a read only property named live") def test_should_have_a_read_only_property_named_live(case): @@ -74,19 +489,11 @@ def test_should_have_a_read_only_property_named_live(case): @it.has_test_setup def setup(case): - try: - case.old_callable = __builtins__['callable'] - __builtins__['callable'] = lambda c: True - except TypeError: - case.old_callable = __builtins__.callable - __builtins__.callable = lambda c: True + stub_callable(case, True) @it.has_test_teardown def teardown(case): - try: - __builtins__['callable'] = case.old_callable - except TypeError: - __builtins__.callable = case.old_callable + unstub_callable(case) @it.should("have a read only property named fake") def test_should_have_a_read_only_property_named_live(case): @@ -109,35 +516,18 @@ def test_should_have_a_read_only_property_named_live(case): with it.having('a python 3.x runtime1'): @it.has_test_setup def setup(case): - stubbed_sys = mock.MagicMock() - stubbed_sys.version_info = (3, ) + stub_python_version(case, 3) - case.patch_python_version = mock.patch.dict(sys.modules, sys=stubbed_sys) - case.patch_python_version.start() - - reload(callables) - - try: - case.old_callable = __builtins__['callable'] - __builtins__['callable'] = lambda c: True - except TypeError: - case.old_callable = __builtins__.callable - __builtins__.callable = lambda c: True + stub_callable(case, True) @it.has_test_teardown def teardown(case): - case.patch_python_version.stop() - - reload(callables) + unstub_python_version(case) - try: - __builtins__['callable'] = case.old_callable - except TypeError: - __builtins__.callable = case.old_callable + unstub_callable(case) @it.should("return true if the live function is an instance method") def test_should_return_true_if_the_live_function_is_an_instance_method(case): - sut = callables.FakeCallable(mock.DEFAULT) with mock.patch('inspect.ismethod', return_value=True): with mock.patch('inspect.getargspec', return_value=([], )): @@ -168,31 +558,15 @@ def test_should_return_false_if_the_live_function_is_not_an_instance_method(case with it.having('a python 2.x runtime1'): @it.has_test_setup def setup(case): - stubbed_sys = mock.MagicMock() - stubbed_sys.version_info = (2, ) - - case.patch_python_version = mock.patch.dict(sys.modules, sys=stubbed_sys) - case.patch_python_version.start() + stub_python_version(case, 2) - reload(callables) - - try: - case.old_callable = __builtins__['callable'] - __builtins__['callable'] = lambda c: True - except TypeError: - case.old_callable = __builtins__.callable - __builtins__.callable = lambda c: True + stub_callable(case, True) @it.has_test_teardown def teardown(case): - case.patch_python_version.stop() - - reload(callables) + unstub_python_version(case) - try: - __builtins__['callable'] = case.old_callable - except TypeError: - __builtins__.callable = case.old_callable + unstub_callable(case) @it.should("return true if the live function is an instance method") def test_should_return_true_if_the_live_function_is_an_instance_method(case): @@ -206,14 +580,12 @@ def test_should_return_true_if_the_live_function_is_an_instance_method(case): @it.should("return true if the live function is an unbound instance method") def test_should_return_true_if_the_live_function_is_an_unbound_instance_method(case): - mocked_callable = mock.MagicMock() - self = mock.PropertyMock() - self.return_value = False - type(mocked_callable).__self__ = self + mocked_callable, self = fake_live_bound_callable() + sut = callables.FakeCallable(mocked_callable) with mock.patch('inspect.ismethod', return_value=True): - actual = sut.is_instance_method + actual = sut.is_instance_method case.assertTrue(actual) @@ -235,31 +607,15 @@ def test_should_return_false_if_the_live_function_is_not_an_instance_method(case with it.having('a python 3.x runtime'): @it.has_test_setup def setup(case): - stubbed_sys = mock.MagicMock() - stubbed_sys.version_info = (3, ) - - case.patch_python_version = mock.patch.dict(sys.modules, sys=stubbed_sys) - case.patch_python_version.start() + stub_python_version(case, 3) - reload(callables) - - try: - case.old_callable = __builtins__['callable'] - __builtins__['callable'] = lambda c: True - except TypeError: - case.old_callable = __builtins__.callable - __builtins__.callable = lambda c: True + stub_callable(case, True) @it.has_test_teardown def teardown(case): - case.patch_python_version.stop() - - reload(callables) + unstub_python_version(case) - try: - __builtins__['callable'] = case.old_callable - except TypeError: - __builtins__.callable = case.old_callable + unstub_callable(case) @it.should("return true if the live function is an unbound instance method") def test_should_return_true_if_the_live_function_is_an_unbound_instance_method(case): @@ -302,9 +658,7 @@ def test_should_detect_unbound_instance_methods_by_inspecting_the_arguments(case @it.should("not detect unbound instance methods by inspecting their self attribute") def test_should_not_detect_unbound_instance_methods_by_inspecting_their_self_attribute(case): - mocked_callable = mock.MagicMock() - self = mock.PropertyMock() - type(mocked_callable).__self__ = self + mocked_callable, self = fake_live_bound_callable() sut = callables.FakeCallable(mocked_callable) @@ -316,55 +670,35 @@ def test_should_not_detect_unbound_instance_methods_by_inspecting_their_self_att with it.having('a python 2.x runtime'): @it.has_test_setup def setup(case): - stubbed_sys = mock.MagicMock() - stubbed_sys.version_info = (2, ) - - case.patch_python_version = mock.patch.dict(sys.modules, sys=stubbed_sys) - case.patch_python_version.start() + stub_python_version(case, 2) - reload(callables) - - try: - case.old_callable = __builtins__['callable'] - __builtins__['callable'] = lambda c: True - except TypeError: - case.old_callable = __builtins__.callable - __builtins__.callable = lambda c: True + stub_callable(case, True) @it.has_test_teardown def teardown(case): - case.patch_python_version.stop() - - reload(callables) + unstub_python_version(case) - try: - __builtins__['callable'] = case.old_callable - except TypeError: - __builtins__.callable = case.old_callable + unstub_callable(case) @it.should("return true if the live function is an unbound instance method") def test_should_return_true_if_the_live_function_is_an_unbound_instance_method(case): - mocked_callable = mock.MagicMock() - self = mock.PropertyMock() - self.return_value = False - type(mocked_callable).__self__ = self + mocked_callable, self = fake_live_bound_callable() + sut = callables.FakeCallable(mocked_callable) with mock.patch('inspect.ismethod', return_value=True): - actual = sut.is_instance_method + actual = sut.is_instance_method case.assertTrue(actual) @it.should("return false if the live function is a bound instance method") def test_should_return_false_if_the_live_function_is_a_bound_instance_method(case): - mocked_callable = mock.MagicMock() - self = mock.PropertyMock() - self.return_value = True - type(mocked_callable).__self__ = self + mocked_callable, self = fake_live_bound_callable() + sut = callables.FakeCallable(mocked_callable) with mock.patch('inspect.ismethod', return_value=True): - actual = sut.is_unbound_instance_method + actual = sut.is_unbound_instance_method case.assertEqual(actual, False) @@ -390,15 +724,12 @@ def test_should_not_detect_unbound_instance_methods_by_inspecting_the_arguments( @it.should("detect unbound instance methods by inspecting their self attribute") def test_should_detect_unbound_instance_methods_by_inspecting_their_self_attribute(case): - mocked_callable = mock.MagicMock() - self = mock.PropertyMock() - self.return_value = False - type(mocked_callable).__self__ = self + mocked_callable, self = fake_live_bound_callable() sut = callables.FakeCallable(mocked_callable) with mock.patch('inspect.ismethod', return_value=True): - _ = sut.is_unbound_instance_method - self.assert_called_once_with() + _ = sut.is_unbound_instance_method + self.assert_called_once_with() it.createTests(globals()) \ No newline at end of file diff --git a/tests/unit/utils/__init__.py b/tests/unit/utils/__init__.py new file mode 100644 index 0000000..faa18be --- /dev/null +++ b/tests/unit/utils/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- diff --git a/tests/unit/utils/test_are_argspecs_identical.py b/tests/unit/utils/test_are_argspecs_identical.py new file mode 100644 index 0000000..8565fa6 --- /dev/null +++ b/tests/unit/utils/test_are_argspecs_identical.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import random +import string +from nose2.tools import such +from testdoubles.utils import are_argspecs_identical +from tests.common.layers import UnitTestsLayer +from tests.common.compat import mock + +with such.A('argspecs comparison method') as it: + it.uses(UnitTestsLayer) + + @it.should("return true if the argspecs are completely identical") + def test_should_return_true_if_the_argspecs_are_completely_identical(case): + with mock.patch('inspect.getargspec', return_value=mock.DEFAULT): + actual = are_argspecs_identical(mock.DEFAULT, mock.DEFAULT) + + case.assertTrue(actual) + + @it.should("return true if the argspecs are almost identical") + def test_should_return_true_if_the_argspecs_are_almost_identical(case): + with mock.patch('inspect.getargspec', return_value=mock.DEFAULT): + with mock.patch('testdoubles.utils.are_arguments_identical', return_value=True): + with mock.patch('testdoubles.utils.are_keyword_arguments_identical', return_value=True): + actual = are_argspecs_identical(mock.DEFAULT, mock.DEFAULT) + + case.assertTrue(actual) + + @it.should("return false if the argspecs are completely different") + def test_should_return_true_if_the_argspecs_are_completely_identical(case): + def fake_getargspec(_): + return getattr(mock.sentinel, random.choice(string.ascii_letters)) + + with mock.patch('inspect.getargspec', fake_getargspec): + with mock.patch('testdoubles.utils.are_arguments_identical', return_value=False): + with mock.patch('testdoubles.utils.are_keyword_arguments_identical', return_value=False): + actual = are_argspecs_identical(mock.DEFAULT, mock.DEFAULT) + + case.assertEqual(actual, False) + + it.createTests(globals()) \ No newline at end of file diff --git a/tests/unit/utils/test_are_arguments_identical.py b/tests/unit/utils/test_are_arguments_identical.py new file mode 100644 index 0000000..4afacb8 --- /dev/null +++ b/tests/unit/utils/test_are_arguments_identical.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import inspect +from nose2.tools import such +from testdoubles.utils import are_arguments_identical +from tests.common.layers import UnitTestsLayer + +with such.A('arguments comparison method') as it: + it.uses(UnitTestsLayer) + + @it.should("return true if the first method has an argument and the second method has specified a varargs argument") + def test_should_return_true_if_the_first_method_has_an_argument_and_the_second_method_has_specified_a_varargs_argument( + case): + actual = are_arguments_identical(inspect.ArgSpec(['a'], None, None, None), + inspect.ArgSpec([], 'args', None, None)) + + case.assertTrue(actual) + + @it.should( + "return true if the first method has specified a varargs argument and the second method has an argument ") + def test_should_return_true_if_the_first_method_has_specified_a_varargs_argument_and_the_second_method_has_an_argument( + case): + actual = are_arguments_identical(inspect.ArgSpec([], 'args', None, None), + inspect.ArgSpec(['a'], None, None, None)) + + case.assertTrue(actual) + + @it.should("return true if both methods have the same amount of arguments") + def test_should_return_true_if_both_methods_have_the_same_amount_of_arguments(case): + actual = are_arguments_identical(inspect.ArgSpec(['a'], None, None, None), + inspect.ArgSpec(['a'], None, None, None)) + + case.assertTrue(actual) + + @it.should("return false if the first method doesn't have the same amount of arguments as the second") + def test_should_return_true_if_both_methods_have_the_same_amount_of_arguments(case): + actual = are_arguments_identical(inspect.ArgSpec(['a', 'b'], None, None, None), + inspect.ArgSpec(['a'], None, None, None)) + + case.assertEqual(actual, False) + + @it.should("return false if the second method doesn't have the same amount of arguments as the first") + def test_should_return_true_if_both_methods_have_the_same_amount_of_arguments(case): + actual = are_arguments_identical(inspect.ArgSpec(['a'], None, None, None), + inspect.ArgSpec(['a', 'b'], None, None, None)) + + case.assertEqual(actual, False) + + @it.should( + "return false if the first method has specified a varargs argument and the second method has no arguments") + def test_should_return_false_if_the_first_method_has_specified_a_varargs_argument_and_the_second_method_has_no_arguments( + case): + actual = are_arguments_identical(inspect.ArgSpec([], 'args', None, None), + inspect.ArgSpec([], None, None, None)) + + case.assertEqual(actual, False) + + @it.should( + "return false if the first method has no arguments and the second method has specified a varargs argument") + def test_should_return_false_if_the_first_method_has_no_arguments_and_the_second_method_has_specified_a_varargs_argument( + case): + actual = are_arguments_identical(inspect.ArgSpec([], None, None, None), + inspect.ArgSpec([], 'args', None, None)) + + case.assertEqual(actual, False) + + it.createTests(globals()) \ No newline at end of file diff --git a/tests/unit/utils/test_are_keyword_arguments_identical.py b/tests/unit/utils/test_are_keyword_arguments_identical.py new file mode 100644 index 0000000..9d7b680 --- /dev/null +++ b/tests/unit/utils/test_are_keyword_arguments_identical.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import inspect +import mock +from nose2.tools import such +from testdoubles.utils import are_keyword_arguments_identical +from tests.common.layers import UnitTestsLayer + +with such.A('keyword arguments comparison method') as it: + it.uses(UnitTestsLayer) + + @it.should( + "return true if the first method has a keyword argument and the second method has specified a kwargs argument") + def test_should_return_true_if_the_first_method_has_a_keyword_argument_and_the_second_method_has_specified_a_kwargs_argument( + case): + actual = are_keyword_arguments_identical(inspect.ArgSpec(['a'], None, None, (mock.DEFAULT, )), + inspect.ArgSpec([], None, 'kwargs', None)) + + case.assertTrue(actual) + + @it.should( + "return true if the first method has specified a kwargs argument and the second method has a keyword argument") + def test_should_return_true_if_the_first_method_has_specified_a_kwargs_argument_and_the_second_method_has_a_keyword_argument( + case): + actual = are_keyword_arguments_identical(inspect.ArgSpec([], None, 'kwargs', None), + inspect.ArgSpec(['a'], None, None, (mock.DEFAULT, ))) + + case.assertTrue(actual) + + @it.should("return true if both methods have exactly the same keyword arguments") + def test_should_return_true_if_both_methods_have_exactly_the_same_keyword_arguments(case): + actual = are_keyword_arguments_identical(inspect.ArgSpec(['a'], None, None, (mock.DEFAULT, )), + inspect.ArgSpec(['a'], None, None, (mock.DEFAULT, )), ) + + case.assertTrue(actual) + + @it.should("return false if both methods have different keyword arguments and no kwargs argument") + def test_should_return_false_if_both_methods_have_different_keyword_arguments_and_no_kwargs_argument(case): + actual = are_keyword_arguments_identical(inspect.ArgSpec(['a'], None, None, (mock.DEFAULT, )), + inspect.ArgSpec(['b'], None, None, (mock.DEFAULT, )), ) + + case.assertEqual(actual, False) + + @it.should( + "return false if the first method has specified a kwargs argument and the second method has no arguments") + def test_should_return_false_if_the_first_method_has_specified_a_kwargs_argument_and_the_second_method_has_no_arguments( + case): + actual = are_keyword_arguments_identical(inspect.ArgSpec([], None, 'kwargs', None), + inspect.ArgSpec([], None, None, None)) + + case.assertEqual(actual, False) + + @it.should( + "return false if the first method has no arguments and the second method has specified a kwargs argument") + def test_should_return_false_if_the_first_method_has_no_arguments_and_the_second_method_has_specified_a_kwargs_argument( + case): + actual = are_keyword_arguments_identical(inspect.ArgSpec([], None, None, None), + inspect.ArgSpec([], None, 'kwargs', None)) + + case.assertEqual(actual, False) + + it.createTests(globals()) \ No newline at end of file