From f2b16354e6fd07044d86149fb2f2e7f321574c65 Mon Sep 17 00:00:00 2001 From: smiddlek Date: Tue, 1 May 2012 20:43:51 +0000 Subject: Patch for Issue 16 from Ben Love. Thanks! git-svn-id: http://pymox.googlecode.com/svn/trunk@72 b1010a0a-674b-0410-b734-77272b80c875 --- mox.py | 106 ++++++++++++++++++++++++++++------- mox_test.py | 179 +++++++++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 233 insertions(+), 52 deletions(-) diff --git a/mox.py b/mox.py index 2ffdf90..9c919c9 100755 --- a/mox.py +++ b/mox.py @@ -224,6 +224,30 @@ class UnexpectedMockCreationError(Error): return error +class SwallowedExceptionError(Error): + """Raised when Verify() is called after something already threw an exception. + + This means that the exception that was thrown was somehow swallowed, allowing + the test to continue when it should not have. + """ + + def __init__(self, previous_exceptions): + """Init exception. + + Args: + # previous_exceptions: A sequence of Error objects that were raised. + previous_exceptions: [Error] + """ + + Error.__init__(self) + self._previous_exceptions = previous_exceptions + + def __str__(self): + exceptions = "\n".join(["%3d. %s: %s" % (i, e.__class__.__name__, e) + for i, e in enumerate(self._previous_exceptions)]) + return "Previous exceptions thrown:\n%s" % (exceptions,) + + class Mox(object): """Mox: a factory for creating mock objects.""" @@ -426,8 +450,18 @@ class MockAnything: created, for debugging output purposes. """ self._description = description + self._exceptions_thrown = [] self._Reset() + def __bases__(self): + pass + + def __members__(self): + pass + + def __methods__(self): + pass + def __repr__(self): if self._description: return '' % self._description @@ -467,7 +501,8 @@ class MockAnything: """ return MockMethod(method_name, self._expected_calls_queue, - self._replay_mode, method_to_mock=method_to_mock, + self._exceptions_thrown, self._replay_mode, + method_to_mock=method_to_mock, description=self._description) def __nonzero__(self): @@ -498,8 +533,15 @@ class MockAnything: Raises: ExpectedMethodCallsError: if there are still more method calls in the expected queue. + any exception previously raised by this object: if _Verify was called + afterwards anyway. (This detects tests passing erroneously.) """ + # If any exceptions were thrown, re-raise them. (This should only happen + # if the original exception was swallowed, in which case it's necessary to + # re-raise it so that the test will fail. See Issue #16.) + if self._exceptions_thrown: + raise SwallowedExceptionError(self._exceptions_thrown) # If the list of expected calls is not empty, raise an exception if self._expected_calls_queue: # The last MultipleTimesGroup is not popped from the queue. @@ -610,7 +652,9 @@ class MockObject(MockAnything, object): name, method_to_mock=getattr(self._class_to_mock, name)) - raise UnknownMethodCallError(name) + exception = UnknownMethodCallError(name) + self._exceptions_thrown.append(exception) + raise exception def __eq__(self, rhs): """Provide custom logic to compare objects.""" @@ -644,7 +688,7 @@ class MockObject(MockAnything, object): # If we are in replay mode then simply call the mock __setitem__ method. if self._replay_mode: return MockMethod('__setitem__', self._expected_calls_queue, - self._replay_mode)(key, value) + self._exceptions_thrown, self._replay_mode)(key, value) # Otherwise, create a mock method __setitem__. @@ -673,7 +717,7 @@ class MockObject(MockAnything, object): # If we are in replay mode then simply call the mock __getitem__ method. if self._replay_mode: return MockMethod('__getitem__', self._expected_calls_queue, - self._replay_mode)(key) + self._exceptions_thrown, self._replay_mode)(key) # Otherwise, create a mock method __getitem__. @@ -713,7 +757,7 @@ class MockObject(MockAnything, object): # If we are in replay mode then simply call the mock __iter__ method. if self._replay_mode: return MockMethod('__iter__', self._expected_calls_queue, - self._replay_mode)() + self._exceptions_thrown, self._replay_mode)() # Otherwise, create a mock method __iter__. @@ -743,7 +787,7 @@ class MockObject(MockAnything, object): if self._replay_mode: return MockMethod('__contains__', self._expected_calls_queue, - self._replay_mode)(key) + self._exceptions_thrown, self._replay_mode)(key) return self._CreateMockMethod('__contains__')(key) @@ -809,8 +853,10 @@ class _MockObjectFactory(MockObject): if self._replay_mode: if not self._instance_queue: - raise UnexpectedMockCreationError(self._class_to_mock, *params, - **named_params) + exception = UnexpectedMockCreationError(self._class_to_mock, *params, + **named_params) + self._exceptions_thrown.append(exception) + raise exception mock_method(*params, **named_params) @@ -967,7 +1013,7 @@ class MockMethod(object): signature) matches the expected method. """ - def __init__(self, method_name, call_queue, replay_mode, + def __init__(self, method_name, call_queue, exception_list, replay_mode, method_to_mock=None, description=None): """Construct a new mock method. @@ -975,6 +1021,8 @@ class MockMethod(object): # method_name: the name of the method # call_queue: deque of calls, verify this call against the head, or add # this call to the queue. + # exception_list: list of exceptions; any exceptions thrown by this + # instance are appended to this list. # replay_mode: False if we are recording, True if we are verifying calls # against the call queue. # method_to_mock: The actual method being mocked, used for introspection. @@ -982,6 +1030,7 @@ class MockMethod(object): # this is equal to the descriptive name of the method's class. method_name: str call_queue: list or deque + exception_list: list replay_mode: bool method_to_mock: a method object description: str or None @@ -992,6 +1041,7 @@ class MockMethod(object): self._call_queue = call_queue if not isinstance(call_queue, deque): self._call_queue = deque(self._call_queue) + self._exception_list = exception_list self._replay_mode = replay_mode self._description = description @@ -1061,7 +1111,9 @@ class MockMethod(object): try: return self._call_queue.popleft() except IndexError: - raise UnexpectedMethodCallError(self, None) + exception = UnexpectedMethodCallError(self, None) + self._exception_list.append(exception) + raise exception def _VerifyMethodCall(self): """Verify the called method is expected. @@ -1086,7 +1138,9 @@ class MockMethod(object): # This is a mock method, so just check equality. if expected != self: - raise UnexpectedMethodCallError(self, expected) + exception = UnexpectedMethodCallError(self, expected) + self._exception_list.append(exception) + raise exception return expected @@ -1158,7 +1212,7 @@ class MockMethod(object): return self # Create a new group and add the method. - new_group = group_class(group_name) + new_group = group_class(group_name, self._exception_list) new_group.AddMethod(self) self._call_queue.append(new_group) return self @@ -1831,8 +1885,18 @@ class Remember(Comparator): class MethodGroup(object): """Base class containing common behaviour for MethodGroups.""" - def __init__(self, group_name): + def __init__(self, group_name, exception_list): + """Construct a new method group. + + Args: + # group_name: the name of the method group + # exception_list: list of exceptions; any exceptions thrown by this + # instance are appended to this list. + group_name: str + exception_list: list + """ self._group_name = group_name + self._exception_list = exception_list def group_name(self): return self._group_name @@ -1856,8 +1920,8 @@ class UnorderedGroup(MethodGroup): over the keys of a dict. """ - def __init__(self, group_name): - super(UnorderedGroup, self).__init__(group_name) + def __init__(self, group_name, exception_list): + super(UnorderedGroup, self).__init__(group_name, exception_list) self._methods = [] def __str__(self): @@ -1907,7 +1971,9 @@ class UnorderedGroup(MethodGroup): return self, method - raise UnexpectedMethodCallError(mock_method, self) + exception = UnexpectedMethodCallError(mock_method, self) + self._exception_list.append(exception) + raise exception def IsSatisfied(self): """Return True if there are not any methods in this group.""" @@ -1923,8 +1989,8 @@ class MultipleTimesGroup(MethodGroup): This is helpful, if you don't know or care how many times a method is called. """ - def __init__(self, group_name): - super(MultipleTimesGroup, self).__init__(group_name) + def __init__(self, group_name, exception_list): + super(MultipleTimesGroup, self).__init__(group_name, exception_list) self._methods = set() self._methods_left = set() @@ -1968,7 +2034,9 @@ class MultipleTimesGroup(MethodGroup): next_method = mock_method._PopNextMethod(); return next_method, None else: - raise UnexpectedMethodCallError(mock_method, self) + exception = UnexpectedMethodCallError(mock_method, self) + self._exception_list.append(exception) + raise exception def IsSatisfied(self): """Return True if all methods in this group are called at least once.""" diff --git a/mox_test.py b/mox_test.py index f865fb9..d489f0b 100755 --- a/mox_test.py +++ b/mox_test.py @@ -33,7 +33,7 @@ class ExpectedMethodCallsErrorTest(unittest.TestCase): self.assertRaises(ValueError, mox.ExpectedMethodCallsError, []) def testOneError(self): - method = mox.MockMethod("testMethod", [], False) + method = mox.MockMethod("testMethod", [], [], False) method(1, 2).AndReturn('output') e = mox.ExpectedMethodCallsError([method]) self.assertEqual( @@ -42,13 +42,13 @@ class ExpectedMethodCallsErrorTest(unittest.TestCase): str(e)) def testManyErrors(self): - method1 = mox.MockMethod("testMethod", [], False) + method1 = mox.MockMethod("testMethod", [], [], False) method1(1, 2).AndReturn('output') - method2 = mox.MockMethod("testMethod", [], False) + method2 = mox.MockMethod("testMethod", [], [], False) method2(a=1, b=2, c="only named") - method3 = mox.MockMethod("testMethod2", [], False) + method3 = mox.MockMethod("testMethod2", [], [], False) method3().AndReturn(44) - method4 = mox.MockMethod("testMethod", [], False) + method4 = mox.MockMethod("testMethod", [], [], False) method4(1, 2).AndReturn('output') e = mox.ExpectedMethodCallsError([method1, method2, method3, method4]) self.assertEqual( @@ -457,8 +457,9 @@ class MockMethodTest(unittest.TestCase): """Test class to verify that the MockMethod class is working correctly.""" def setUp(self): - self.expected_method = mox.MockMethod("testMethod", [], False)(['original']) - self.mock_method = mox.MockMethod("testMethod", [self.expected_method], + self.expected_method = mox.MockMethod("testMethod", [], [], False)( + ['original']) + self.mock_method = mox.MockMethod("testMethod", [self.expected_method], [], True) def testNameAttribute(self): @@ -523,18 +524,18 @@ class MockMethodTest(unittest.TestCase): def testEqualityNoParamsEqual(self): """Methods with the same name and without params should be equal.""" - expected_method = mox.MockMethod("testMethod", [], False) + expected_method = mox.MockMethod("testMethod", [], [], False) self.assertEqual(self.mock_method, expected_method) def testEqualityNoParamsNotEqual(self): """Methods with different names and without params should not be equal.""" - expected_method = mox.MockMethod("otherMethod", [], False) + expected_method = mox.MockMethod("otherMethod", [], [], False) self.failIfEqual(self.mock_method, expected_method) def testEqualityParamsEqual(self): """Methods with the same name and parameters should be equal.""" params = [1, 2, 3] - expected_method = mox.MockMethod("testMethod", [], False) + expected_method = mox.MockMethod("testMethod", [], [], False) expected_method._params = params self.mock_method._params = params @@ -542,7 +543,7 @@ class MockMethodTest(unittest.TestCase): def testEqualityParamsNotEqual(self): """Methods with the same name and different params should not be equal.""" - expected_method = mox.MockMethod("testMethod", [], False) + expected_method = mox.MockMethod("testMethod", [], [], False) expected_method._params = [1, 2, 3] self.mock_method._params = ['a', 'b', 'c'] @@ -551,7 +552,7 @@ class MockMethodTest(unittest.TestCase): def testEqualityNamedParamsEqual(self): """Methods with the same name and same named params should be equal.""" named_params = {"input1": "test", "input2": "params"} - expected_method = mox.MockMethod("testMethod", [], False) + expected_method = mox.MockMethod("testMethod", [], [], False) expected_method._named_params = named_params self.mock_method._named_params = named_params @@ -559,7 +560,7 @@ class MockMethodTest(unittest.TestCase): def testEqualityNamedParamsNotEqual(self): """Methods with the same name and diffnamed params should not be equal.""" - expected_method = mox.MockMethod("testMethod", [], False) + expected_method = mox.MockMethod("testMethod", [], [], False) expected_method._named_params = {"input1": "test", "input2": "params"} self.mock_method._named_params = {"input1": "test2", "input2": "params2"} @@ -575,39 +576,39 @@ class MockMethodTest(unittest.TestCase): instB = TestClass(); params = [instA, ] - expected_method = mox.MockMethod("testMethod", [], False) + expected_method = mox.MockMethod("testMethod", [], [], False) expected_method._params = params self.mock_method._params = [instB, ] self.assertEqual(self.mock_method, expected_method) def testStrConversion(self): - method = mox.MockMethod("f", [], False) + method = mox.MockMethod("f", [], [], False) method(1, 2, "st", n1=8, n2="st2") self.assertEqual(str(method), ("f(1, 2, 'st', n1=8, n2='st2') -> None")) - method = mox.MockMethod("testMethod", [], False) + method = mox.MockMethod("testMethod", [], [], False) method(1, 2, "only positional") self.assertEqual(str(method), "testMethod(1, 2, 'only positional') -> None") - method = mox.MockMethod("testMethod", [], False) + method = mox.MockMethod("testMethod", [], [], False) method(a=1, b=2, c="only named") self.assertEqual(str(method), "testMethod(a=1, b=2, c='only named') -> None") - method = mox.MockMethod("testMethod", [], False) + method = mox.MockMethod("testMethod", [], [], False) method() self.assertEqual(str(method), "testMethod() -> None") - method = mox.MockMethod("testMethod", [], False) + method = mox.MockMethod("testMethod", [], [], False) method(x="only 1 parameter") self.assertEqual(str(method), "testMethod(x='only 1 parameter') -> None") - method = mox.MockMethod("testMethod", [], False) + method = mox.MockMethod("testMethod", [], [], False) method().AndReturn('return_value') self.assertEqual(str(method), "testMethod() -> 'return_value'") - method = mox.MockMethod("testMethod", [], False) + method = mox.MockMethod("testMethod", [], [], False) method().AndReturn(('a', {1: 2})) self.assertEqual(str(method), "testMethod() -> ('a', {1: 2})") @@ -746,7 +747,7 @@ class MethodCheckerTest(unittest.TestCase): """Tests MockMethod's use of MethodChecker method.""" def testNoParameters(self): - method = mox.MockMethod('NoParameters', [], False, + method = mox.MockMethod('NoParameters', [], [], False, CheckCallTestClass.NoParameters) method() self.assertRaises(AttributeError, method, 1) @@ -755,7 +756,7 @@ class MethodCheckerTest(unittest.TestCase): self.assertRaises(AttributeError, method, 1, b=2) def testOneParameter(self): - method = mox.MockMethod('OneParameter', [], False, + method = mox.MockMethod('OneParameter', [], [], False, CheckCallTestClass.OneParameter) self.assertRaises(AttributeError, method) method(1) @@ -766,7 +767,7 @@ class MethodCheckerTest(unittest.TestCase): self.assertRaises(AttributeError, method, 1, b=2) def testTwoParameters(self): - method = mox.MockMethod('TwoParameters', [], False, + method = mox.MockMethod('TwoParameters', [], [], False, CheckCallTestClass.TwoParameters) self.assertRaises(AttributeError, method) self.assertRaises(AttributeError, method, 1) @@ -783,7 +784,7 @@ class MethodCheckerTest(unittest.TestCase): self.assertRaises(AttributeError, method, 3, a=1, b=2) def testOneDefaultValue(self): - method = mox.MockMethod('OneDefaultValue', [], False, + method = mox.MockMethod('OneDefaultValue', [], [], False, CheckCallTestClass.OneDefaultValue) method() method(1) @@ -794,7 +795,7 @@ class MethodCheckerTest(unittest.TestCase): self.assertRaises(AttributeError, method, 1, b=2) def testTwoDefaultValues(self): - method = mox.MockMethod('TwoDefaultValues', [], False, + method = mox.MockMethod('TwoDefaultValues', [], [], False, CheckCallTestClass.TwoDefaultValues) self.assertRaises(AttributeError, method) self.assertRaises(AttributeError, method, c=3) @@ -814,7 +815,7 @@ class MethodCheckerTest(unittest.TestCase): self.assertRaises(AttributeError, method, a=1, b=2, e=9) def testArgs(self): - method = mox.MockMethod('Args', [], False, CheckCallTestClass.Args) + method = mox.MockMethod('Args', [], [], False, CheckCallTestClass.Args) self.assertRaises(AttributeError, method) self.assertRaises(AttributeError, method, 1) method(1, 2) @@ -825,7 +826,7 @@ class MethodCheckerTest(unittest.TestCase): self.assertRaises(AttributeError, method, 1, 2, c=3) def testKwargs(self): - method = mox.MockMethod('Kwargs', [], False, CheckCallTestClass.Kwargs) + method = mox.MockMethod('Kwargs', [], [], False, CheckCallTestClass.Kwargs) self.assertRaises(AttributeError, method) method(1) method(1, 2) @@ -840,7 +841,7 @@ class MethodCheckerTest(unittest.TestCase): self.assertRaises(AttributeError, method, 1, 2, 3, 4) def testArgsAndKwargs(self): - method = mox.MockMethod('ArgsAndKwargs', [], False, + method = mox.MockMethod('ArgsAndKwargs', [], [], False, CheckCallTestClass.ArgsAndKwargs) self.assertRaises(AttributeError, method) method(1) @@ -1031,7 +1032,7 @@ class MockObjectTest(unittest.TestCase): self.assertRaises(mox.UnexpectedMethodCallError, call) - dummy._Verify() + self.assertRaises(mox.SwallowedExceptionError, dummy._Verify) def testMockSetItem_WithSubClassOfNewStyleClass(self): class NewStyleTestClass(object): @@ -1099,7 +1100,7 @@ class MockObjectTest(unittest.TestCase): self.assertRaises(mox.UnexpectedMethodCallError, call) - dummy._Verify() + self.assertRaises(mox.SwallowedExceptionError, dummy._Verify) def testMockGetItem_WithSubClassOfNewStyleClass(self): class NewStyleTestClass(object): @@ -1169,7 +1170,7 @@ class MockObjectTest(unittest.TestCase): self.assertRaises(mox.UnexpectedMethodCallError, call) - dummy._Verify() + self.assertRaises(mox.SwallowedExceptionError, dummy._Verify) def testMockIter_ExpectedIter_NoSuccess(self): """Test that __iter__() gets mocked in Dummy. @@ -1711,7 +1712,7 @@ class MoxTest(unittest.TestCase): self.assertRaises(mox.UnexpectedMethodCallError, TestClass.OtherValidCall, "wrong self") - self.mox.VerifyAll() + self.assertRaises(mox.SwallowedExceptionError, self.mox.VerifyAll) self.mox.UnsetStubs() def testStubOutMethod_Unbound_NamedUsingPositional(self): @@ -2001,6 +2002,118 @@ class MoxTest(unittest.TestCase): self.assertEquals('MockMethod has no attribute "ShowMeTheMoney". ' 'Did you remember to put your mocks in replay mode?', str(e)) + def testSwallowedUnknownMethodCall(self): + """Test that a swallowed UnknownMethodCallError will be re-raised.""" + dummy = self.mox.CreateMock(TestClass) + dummy._Replay() + + def call(): + try: + dummy.InvalidCall() + except mox.UnknownMethodCallError: + pass + + # UnknownMethodCallError swallowed + call() + + self.assertRaises(mox.SwallowedExceptionError, self.mox.VerifyAll) + + def testSwallowedUnexpectedMockCreation(self): + """Test that a swallowed UnexpectedMockCreationError will be re-raised.""" + self.mox.StubOutClassWithMocks(mox_test_helper, 'CallableClass') + self.mox.ReplayAll() + + def call(): + try: + mox_test_helper.CallableClass(1, 2) + except mox.UnexpectedMockCreationError: + pass + + # UnexpectedMockCreationError swallowed + call() + + self.assertRaises(mox.SwallowedExceptionError, self.mox.VerifyAll) + self.mox.UnsetStubs() + + def testSwallowedUnexpectedMethodCall_WrongMethod(self): + """Test that a swallowed UnexpectedMethodCallError will be re-raised. + + This case is an extraneous method call.""" + mock_obj = self.mox.CreateMockAnything() + mock_obj.Open() + self.mox.ReplayAll() + + def call(): + mock_obj.Open() + try: + mock_obj.Close() + except mox.UnexpectedMethodCallError: + pass + + # UnexpectedMethodCall swallowed + call() + + self.assertRaises(mox.SwallowedExceptionError, self.mox.VerifyAll) + + def testSwallowedUnexpectedMethodCall_WrongArguments(self): + """Test that a swallowed UnexpectedMethodCallError will be re-raised. + + This case is an extraneous method call.""" + mock_obj = self.mox.CreateMockAnything() + mock_obj.Open() + self.mox.ReplayAll() + + def call(): + try: + mock_obj.Open(1) + except mox.UnexpectedMethodCallError: + pass + + # UnexpectedMethodCall swallowed + call() + + self.assertRaises(mox.SwallowedExceptionError, self.mox.VerifyAll) + + def testSwallowedUnexpectedMethodCall_UnorderedGroup(self): + """Test that a swallowed UnexpectedMethodCallError will be re-raised. + + This case is an extraneous method call in an unordered group.""" + mock_obj = self.mox.CreateMockAnything() + mock_obj.Open().InAnyOrder() + mock_obj.Close().InAnyOrder() + self.mox.ReplayAll() + + def call(): + mock_obj.Close() + try: + mock_obj.Open(1) + except mox.UnexpectedMethodCallError: + pass + + # UnexpectedMethodCall swallowed + call() + + self.assertRaises(mox.SwallowedExceptionError, self.mox.VerifyAll) + + def testSwallowedUnexpectedMethodCall_MultipleTimesGroup(self): + """Test that a swallowed UnexpectedMethodCallError will be re-raised. + + This case is an extraneous method call in a multiple times group.""" + mock_obj = self.mox.CreateMockAnything() + mock_obj.Open().MultipleTimes() + self.mox.ReplayAll() + + def call(): + try: + mock_obj.Open(1) + except mox.UnexpectedMethodCallError: + pass + + # UnexpectedMethodCall swallowed + call() + + self.assertRaises(mox.SwallowedExceptionError, self.mox.VerifyAll) + class ReplayTest(unittest.TestCase): """Verify Replay works properly.""" -- cgit v1.2.1