From c9d592cf9af50c8bf292952fb4f61422ac9c5228 Mon Sep 17 00:00:00 2001 From: Nicholas Tollervey Date: Thu, 18 Jul 2024 16:35:37 +0000 Subject: [PATCH] Pending changes exported from your codespace --- tests/test_asyncmock.py | 2 +- tests/test_mock.py | 153 +++++++++++++++++++++++++++++++++++++--- tests/test_patch.py | 2 +- umock.py | 70 +++++++++--------- 4 files changed, 185 insertions(+), 42 deletions(-) diff --git a/tests/test_asyncmock.py b/tests/test_asyncmock.py index 3fc1086..a7246a7 100644 --- a/tests/test_asyncmock.py +++ b/tests/test_asyncmock.py @@ -1,3 +1,3 @@ """ Ensure AsyncMock works as expected. -""" \ No newline at end of file +""" diff --git a/tests/test_mock.py b/tests/test_mock.py index 0d1c472..ef81d2b 100644 --- a/tests/test_mock.py +++ b/tests/test_mock.py @@ -3,24 +3,161 @@ exacty the same as unittest.mock.Mock). """ +import upytest from umock import Mock def test_init_mock(): """ - A Mock object should be created with no attributes. + A plain Mock object can be created with no arguments. Accessing arbitrary + attributes on such an object (without a spec) should return another Mock + object. """ mock = Mock() - assert mock.__dict__ == {}, "Not an empty dict." + assert mock.call_count == 0, "Non zero call count with new Mock object." + assert isinstance( + mock.foo, Mock + ), "Attribute access did not return a Mock." -def test_init_mock_with_spec(): + +def test_init_mock_with_spec_from_list(): + """ + A Mock object should be created with the specified list of attributes. + Accessing arbitrary attributes not in the list should raise an + AttributeError. + + If an arbitrary attribute is subqeuently added to the mock object, it + should be accessible as per normal Python behaviour. + """ + mock = Mock(spec=["foo", "bar"]) + assert hasattr(mock, "foo"), "Mock object missing 'foo' attribute." + assert hasattr(mock, "bar"), "Mock object missing 'bar' attribute." + assert not hasattr( + mock, "baz" + ), "Mock object has unexpected 'baz' attribute." + mock.baz = "test" + assert mock.baz == "test", "Mock object attribute 'baz' not set correctly." + + +def test_init_mock_with_spec_from_object(): + """ + A Mock object should be created with the specified attributes derived from + the referenced instance. The Mock's __class__ should be set to that of the + spec object's. Accessing arbitrary attributes not on the class should raise + an AttributeError. + + If an arbitrary attribute is subqeuently added to the mock object, it + should be accessible as per normal Python behaviour. + """ + + class TestClass: + x = 1 + y = 2 + + obj = TestClass() + mock = Mock(spec=obj) + assert hasattr(mock, "x"), "Mock object missing 'x' attribute." + assert hasattr(mock, "y"), "Mock object missing 'y' attribute." + assert not hasattr(mock, "z"), "Mock object has unexpected 'z' attribute." + assert mock.__class__ == TestClass, "Mock object has unexpected class." + mock.z = "test" + assert mock.z == "test", "Mock object attribute 'z' not set correctly." + + +def test_init_mock_with_spec_from_class(): + """ + A Mock object should be created with the specified attributes derived from + the referenced class. Since this is a class spec, the Mock's __class__ + remains as Mock. Accessing arbitrary attributes not on the class should + raise an AttributeError. + + If an arbitrary attribute is subqeuently added to the mock object, it + should be accessible as per normal Python behaviour. + """ + + class TestClass: + x = 1 + y = 2 + + mock = Mock(spec=TestClass) + assert hasattr(mock, "x"), "Mock object missing 'x' attribute." + assert hasattr(mock, "y"), "Mock object missing 'y' attribute." + assert not hasattr(mock, "z"), "Mock object has unexpected 'z' attribute." + assert mock.__class__ == Mock, "Mock object has unexpected class." + mock.z = "test" + assert mock.z == "test", "Mock object attribute 'z' not set correctly." + + +def test_init_mock_with_callable_side_effect(): + """ + A Mock object should be created with the specified callable side effect + that computes the result of a call on the mock object. + """ + + def side_effect(a, b): + return a + b + + mock = Mock(side_effect=side_effect) + assert ( + mock(1, 2) == 3 + ), "Mock object side effect did not compute correctly." + + +def test_init_mock_with_exception_class_side_effect(): + """ + A Mock object should be created with the specified exception class side + effect that raises the exception when the mock object is called. + """ + + class TestException(Exception): + pass + + mock = Mock(side_effect=TestException) + with upytest.raises(TestException): + mock() + + +def test_init_mock_with_exception_instance_side_effect(): + """ + A Mock object should be created with the specified exception instance side + effect that is raised when the mock object is called. + """ + + ex = ValueError("test") + mock = Mock(side_effect=ex) + with upytest.raises(ValueError) as expected: + mock() + assert ( + str(expected.exception.value) == "test" + ), "Exception message not as expected." + + +def test_init_mock_with_iterable_side_effect(): + """ + A Mock object should be created with the specified iterable side effect + that returns the next item in the iterable each time the mock object is + called. + """ + + mock = Mock(side_effect=[1, 2, 3]) + assert mock() == 1, "First call did not return 1." + assert mock() == 2, "Second call did not return 2." + assert mock() == 3, "Third call did not return 3." + with upytest.raises(StopIteration): + mock() + +def test_init_mock_with_invalid_side_effect(): """ - A Mock object should be created with the specified attributes. + If an invalid side effect is specified, a TypeError should be raised. """ - pass + mock = Mock(side_effect=1) + with upytest.raises(TypeError): + mock() -def test_init_mock_with_spec_and_values(): +def test_init_mock_with_return_value(): """ - A Mock object should be created with the specified attributes and values. + A Mock object should be created with the specified return value that is + returned each time the mock object is called. """ - pass \ No newline at end of file + mock = Mock(return_value=42) + assert mock() == 42, "Return value not as expected." \ No newline at end of file diff --git a/tests/test_patch.py b/tests/test_patch.py index 67eac9a..782bb38 100644 --- a/tests/test_patch.py +++ b/tests/test_patch.py @@ -1,3 +1,3 @@ """ Tests the patch decorator/context manager. -""" \ No newline at end of file +""" diff --git a/umock.py b/umock.py index 2c2195e..1e672af 100644 --- a/umock.py +++ b/umock.py @@ -78,11 +78,19 @@ class or instance) that acts as the specification for the mock if not name.startswith("_") and not callable(getattr(spec, name)) ] - self.__class__ = spec.__class__ + if type(spec) is not type: + # Set the mock object's class to that of the spec object. + self.__class__ = type(spec) + for name in self._spec: + # Create a new mock object for each attribute in the spec. + setattr(self, name, Mock()) if return_value: self.return_value = return_value if side_effect: - self.side_effect = side_effect + if type(side_effect) in (str, list, tuple, set, dict): + self.side_effect = iter(side_effect) + else: + self.side_effect = side_effect self.reset_mock() for key, value in kwargs.items(): setattr(self, key, value) @@ -198,49 +206,47 @@ def assert_never_called(self): def __call__(self, *args, **kwargs): """ - Record the call. + Record the call and return the specified result. + + In order of precedence, the return value is determined as follows: + + If a side_effect is specified then that is used to determine the + return value. If a return_value is specified then that is used. If + neither are specified then the same Mock object is returned each time. """ self._calls.append(("__call__", args, kwargs)) if hasattr(self, "side_effect"): - if callable(self.side_effect): - return self.side_effect(*args, **kwargs) - elif isinstance(self.side_effect, Exception): + if type(self.side_effect) is type and issubclass( + self.side_effect, BaseException + ): + raise self.side_effect() + elif isinstance(self.side_effect, BaseException): raise self.side_effect - elif hasattr(self.side_effect, "__iter__"): - return self.side_effect.pop(0) - elif hasattr(self, "return_value"): - return self.return_value + elif hasattr(self.side_effect, "__next__"): + return next(self.side_effect) + elif callable(self.side_effect): + return self.side_effect(*args, **kwargs) + raise TypeError("The mock object has an invalid side_effect.") + if hasattr(self, "return_value"): + print("YES") + #return self.return_value else: return Mock() def __getattr__(self, name): """ - Return a callable that records the call. + Return an attribute. """ if name.startswith("_"): return super().__getattr__(name) - if hasattr(self, "return_value"): - return self.return_value + elif name in self.__dict__: + return self.__dict__[name] + elif hasattr(self, "_spec") and name not in self._spec: + raise AttributeError(f"Mock object has no attribute '{name}'.") else: - return Mock() - - def __setattr__(self, name, value): - """ - Set an attribute on the mock object. - """ - if name.startswith("_"): - super().__setattr__(name, value) - if hasattr(self, "_spec") and name not in self._spec: - raise AttributeError(f"{name} is not in the mock's spec.") - super().__setattr__(name, value) - - def __delattr__(self, name): - """ - Delete an attribute on the mock object. - """ - if hasattr(self, "_spec") and name not in self._spec: - raise AttributeError(f"{name} is not in the mock's spec.") - super().__delattr__(name) + new_mock = Mock() + setattr(self, name, new_mock) + return new_mock class AsyncMock(Mock):