466 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			466 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #
 | |
| # (c) Dave Kirby 2001 - 2005
 | |
| #     mock@thedeveloperscoach.com
 | |
| #
 | |
| # Original call interceptor and call assertion code by Phil Dawes (pdawes@users.sourceforge.net)
 | |
| # Call interceptor code enhanced by Bruce Cropley (cropleyb@yahoo.com.au)
 | |
| #
 | |
| # This Python  module and associated files are released under the FreeBSD
 | |
| # license. Essentially, you can do what you like with it except pretend you wrote
 | |
| # it yourself.
 | |
| #
 | |
| #
 | |
| #     Copyright (c) 2005, Dave Kirby
 | |
| #     Copyright (c) 2009, Yann Leboulanger
 | |
| #
 | |
| #     All rights reserved.
 | |
| #
 | |
| #     Redistribution and use in source and binary forms, with or without
 | |
| #     modification, are permitted provided that the following conditions are met:
 | |
| #
 | |
| #         * Redistributions of source code must retain the above copyright
 | |
| #           notice, this list of conditions and the following disclaimer.
 | |
| #
 | |
| #         * Redistributions in binary form must reproduce the above copyright
 | |
| #           notice, this list of conditions and the following disclaimer in the
 | |
| #           documentation and/or other materials provided with the distribution.
 | |
| #
 | |
| #         * Neither the name of this library nor the names of its
 | |
| #           contributors may be used to endorse or promote products derived from
 | |
| #           this software without specific prior written permission.
 | |
| #
 | |
| #     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 | |
| #     ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 | |
| #     WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 | |
| #     DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 | |
| #     ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 | |
| #     (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 | |
| #     LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 | |
| #     ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 | |
| #     (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 | |
| #     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 | |
| #
 | |
| #         mock@thedeveloperscoach.com
 | |
| 
 | |
| 
 | |
| """
 | |
| Mock object library for Python. Mock objects can be used when unit testing
 | |
| to remove a dependency on another production class. They are typically used
 | |
| when the dependency would either pull in lots of other classes, or
 | |
| significantly slow down the execution of the test.
 | |
| They are also used to create exceptional conditions that cannot otherwise
 | |
| be easily triggered in the class under test.
 | |
| """
 | |
| 
 | |
| __version__ = "0.1.0"
 | |
| 
 | |
| # Added in Python 2.1
 | |
| import inspect
 | |
| import re
 | |
| 
 | |
| class MockInterfaceError(Exception):
 | |
|     pass
 | |
| 
 | |
| class Mock(object):
 | |
|     """
 | |
|     The Mock class emulates any other class for testing purposes.
 | |
|     All method calls are stored for later examination.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, returnValues=None, realClass=None):
 | |
|         """
 | |
|         The Mock class constructor takes a dictionary of method names and
 | |
|         the values they return.  Methods that are not in the returnValues
 | |
|         dictionary will return None.
 | |
|         You may also supply a class whose interface is being mocked.
 | |
|         All calls will be checked to see if they appear in the original
 | |
|         interface. Any calls to methods not appearing in the real class
 | |
|         will raise a MockInterfaceError.  Any calls that would fail due to
 | |
|         non-matching parameter lists will also raise a MockInterfaceError.
 | |
|         Both of these help to prevent the Mock class getting out of sync
 | |
|         with the class it is Mocking.
 | |
|         """
 | |
|         self.mockCalledMethods = {}
 | |
|         self.mockAllCalledMethods = []
 | |
|         self.mockReturnValues = returnValues or {}
 | |
|         self.mockExpectations = {}
 | |
|         self.realClass = realClass
 | |
|         self.realClassMethods = None
 | |
|         if realClass:
 | |
|             self.realClassMethods = dict(inspect.getmembers(realClass, inspect.isroutine))
 | |
|             for retMethod in self.mockReturnValues.keys():
 | |
|                 if retMethod not in self.realClassMethods:
 | |
|                     raise MockInterfaceError("Return value supplied for method '%s' that was not in the original class" % retMethod)
 | |
|         self._setupSubclassMethodInterceptors()
 | |
| 
 | |
|     def _setupSubclassMethodInterceptors(self):
 | |
|         methods = inspect.getmembers(self.realClass, inspect.isroutine)
 | |
|         baseMethods = dict(inspect.getmembers(Mock, inspect.ismethod))
 | |
|         for m in methods:
 | |
|             name = m[0]
 | |
|             # Don't record calls to methods of Mock base class.
 | |
|             if not name in baseMethods:
 | |
|                 self.__dict__[name] = MockCallable(name, self, handcrafted=True)
 | |
| 
 | |
|     def get_module(self, name):
 | |
|         return Mock()
 | |
| 
 | |
|     def __getattr__(self, name):
 | |
|         return MockCallable(name, self)
 | |
| 
 | |
|     def mockAddReturnValues(self, **methodReturnValues ):
 | |
|         self.mockReturnValues.update(methodReturnValues)
 | |
| 
 | |
|     def mockSetExpectation(self, name, testFn, after=0, until=0):
 | |
|         self.mockExpectations.setdefault(name, []).append((testFn, after, until))
 | |
| 
 | |
|     def _checkInterfaceCall(self, name, callParams, callKwParams):
 | |
|         """
 | |
|         Check that a call to a method of the given name to the original
 | |
|         class with the given parameters would not fail. If it would fail,
 | |
|         raise a MockInterfaceError.
 | |
|         Based on the Python 2.3.3 Reference Manual section 5.3.4: Calls.
 | |
|         """
 | |
|         if self.realClassMethods is None:
 | |
|             return
 | |
|         if name not in self.realClassMethods:
 | |
|             return
 | |
| 
 | |
|         func = self.realClassMethods[name]
 | |
|         try:
 | |
|             args, varargs, varkw, defaults = inspect.getargspec(func)
 | |
|         except TypeError:
 | |
|             # func is not a Python function. It is probably a builtin,
 | |
|             # such as __repr__ or __coerce__. TODO: Checking?
 | |
|             # For now assume params are OK.
 | |
|             return
 | |
| 
 | |
|         # callParams doesn't include self; args does include self.
 | |
|         numPosCallParams = 1 + len(callParams)
 | |
| 
 | |
|         if numPosCallParams > len(args) and not varargs:
 | |
|             raise MockInterfaceError("Original %s() takes at most %s arguments (%s given)" %
 | |
|                 (name, len(args), numPosCallParams))
 | |
| 
 | |
|         # Get the number of positional arguments that appear in the call,
 | |
|         # also check for duplicate parameters and unknown parameters
 | |
|         numPosSeen = _getNumPosSeenAndCheck(numPosCallParams, callKwParams, args, varkw)
 | |
| 
 | |
|         lenArgsNoDefaults = len(args) - len(defaults or [])
 | |
|         if numPosSeen < lenArgsNoDefaults:
 | |
|             raise MockInterfaceError("Original %s() takes at least %s arguments (%s given)" % (name, lenArgsNoDefaults, numPosSeen))
 | |
| 
 | |
|     def mockGetAllCalls(self):
 | |
|         """
 | |
|         Return a list of MockCall objects,
 | |
|         representing all the methods in the order they were called.
 | |
|         """
 | |
|         return self.mockAllCalledMethods
 | |
|     getAllCalls = mockGetAllCalls  # deprecated - kept for backward compatibility
 | |
| 
 | |
|     def mockGetNamedCalls(self, methodName):
 | |
|         """
 | |
|         Return a list of MockCall objects,
 | |
|         representing all the calls to the named method in the order they were called.
 | |
|         """
 | |
|         return self.mockCalledMethods.get(methodName, [])
 | |
|     getNamedCalls = mockGetNamedCalls  # deprecated - kept for backward compatibility
 | |
| 
 | |
|     def mockCheckCall(self, index, name, *args, **kwargs):
 | |
|         '''test that the index-th call had the specified name and parameters'''
 | |
|         call = self.mockAllCalledMethods[index]
 | |
|         assert name == call.getName(), "%r != %r" % (name, call.getName())
 | |
|         call.checkArgs(*args, **kwargs)
 | |
| 
 | |
| 
 | |
| def _getNumPosSeenAndCheck(numPosCallParams, callKwParams, args, varkw):
 | |
|     """
 | |
|     Positional arguments can appear as call parameters either named as
 | |
|     a named (keyword) parameter, or just as a value to be matched by
 | |
|     position. Count the positional arguments that are given by either
 | |
|     keyword or position, and check for duplicate specifications.
 | |
|     Also check for arguments specified by keyword that do not appear
 | |
|     in the method's parameter list.
 | |
|     """
 | |
|     posSeen = {}
 | |
|     for arg in args[:numPosCallParams]:
 | |
|         posSeen[arg] = True
 | |
|     for kwp in callKwParams:
 | |
|         if kwp in posSeen:
 | |
|             raise MockInterfaceError("%s appears as both a positional and named parameter." % kwp)
 | |
|         if kwp in args:
 | |
|             posSeen[kwp] = True
 | |
|         elif not varkw:
 | |
|             raise MockInterfaceError("Original method does not have a parameter '%s'" % kwp)
 | |
|     return len(posSeen)
 | |
| 
 | |
| class MockCall:
 | |
|     """
 | |
|     MockCall records the name and parameters of a call to an instance
 | |
|     of a Mock class. Instances of MockCall are created by the Mock class,
 | |
|     but can be inspected later as part of the test.
 | |
|     """
 | |
|     def __init__(self, name, params, kwparams ):
 | |
|         self.name = name
 | |
|         self.params = params
 | |
|         self.kwparams = kwparams
 | |
| 
 | |
|     def checkArgs(self, *args, **kwargs):
 | |
|         assert args == self.params, "%r != %r" % (args, self.params)
 | |
|         assert kwargs == self.kwparams, "%r != %r" % (kwargs, self.kwparams)
 | |
| 
 | |
|     def getParam( self, n ):
 | |
|         if isinstance(n, int):
 | |
|             return self.params[n]
 | |
|         elif isinstance(n, str):
 | |
|             return self.kwparams[n]
 | |
|         else:
 | |
|             raise IndexError('illegal index type for getParam')
 | |
| 
 | |
|     def getNumParams(self):
 | |
|         return len(self.params)
 | |
| 
 | |
|     def getNumKwParams(self):
 | |
|         return len(self.kwparams)
 | |
| 
 | |
|     def getName(self):
 | |
|         return self.name
 | |
| 
 | |
|     #pretty-print the method call
 | |
|     def __str__(self):
 | |
|         s = self.name + "("
 | |
|         sep = ''
 | |
|         for p in self.params:
 | |
|             s = s + sep + repr(p)
 | |
|             sep = ', '
 | |
|         items = sorted(self.kwparams.items())
 | |
|         for k, v in items:
 | |
|             s = s + sep + k + '=' + repr(v)
 | |
|             sep = ', '
 | |
|         s = s + ')'
 | |
|         return s
 | |
|     def __repr__(self):
 | |
|         return self.__str__()
 | |
| 
 | |
| class MockCallable:
 | |
|     """
 | |
|     Intercepts the call and records it, then delegates to either the mock's
 | |
|     dictionary of mock return values that was passed in to the constructor,
 | |
|     or a handcrafted method of a Mock subclass.
 | |
|     """
 | |
|     def __init__(self, name, mock, handcrafted=False):
 | |
|         self.name = name
 | |
|         self.mock = mock
 | |
|         self.handcrafted = handcrafted
 | |
| 
 | |
|     def __call__(self,  *params, **kwparams):
 | |
|         self.mock._checkInterfaceCall(self.name, params, kwparams)
 | |
|         thisCall = self.recordCall(params, kwparams)
 | |
|         self.checkExpectations(thisCall, params, kwparams)
 | |
|         return self.makeCall(params, kwparams)
 | |
| 
 | |
|     def recordCall(self, params, kwparams):
 | |
|         """
 | |
|         Record the MockCall in an ordered list of all calls, and an ordered
 | |
|         list of calls for that method name.
 | |
|         """
 | |
|         thisCall = MockCall(self.name, params, kwparams)
 | |
|         calls = self.mock.mockCalledMethods.setdefault(self.name, [])
 | |
|         calls.append(thisCall)
 | |
|         self.mock.mockAllCalledMethods.append(thisCall)
 | |
|         return thisCall
 | |
| 
 | |
|     def makeCall(self, params, kwparams):
 | |
|         if self.handcrafted:
 | |
|             allPosParams = (self.mock,) + params
 | |
|             func = _findFunc(self.mock.realClass, self.name)
 | |
|             if not func:
 | |
|                 raise NotImplementedError
 | |
|             return func(*allPosParams, **kwparams)
 | |
|         else:
 | |
|             returnVal = self.mock.mockReturnValues.get(self.name)
 | |
|             if isinstance(returnVal, ReturnValuesBase):
 | |
|                 returnVal = returnVal.next()
 | |
|             return returnVal
 | |
| 
 | |
|     def checkExpectations(self, thisCall, params, kwparams):
 | |
|         if self.name in self.mock.mockExpectations:
 | |
|             callsMade = len(self.mock.mockCalledMethods[self.name])
 | |
|             for (expectation, after, until) in self.mock.mockExpectations[self.name]:
 | |
|                 if callsMade > after and (until==0 or callsMade < until):
 | |
|                     assert expectation(self.mock, thisCall, len(self.mock.mockAllCalledMethods)-1), 'Expectation failed: '+str(thisCall)
 | |
| 
 | |
| 
 | |
| def _findFunc(cl, name):
 | |
|     """ Depth first search for a method with a given name. """
 | |
|     if name in cl.__dict__:
 | |
|         return cl.__dict__[name]
 | |
|     for base in cl.__bases__:
 | |
|         func = _findFunc(base, name)
 | |
|         if func:
 | |
|             return func
 | |
|     return None
 | |
| 
 | |
| 
 | |
| 
 | |
| class ReturnValuesBase:
 | |
|     def next(self):
 | |
|         try:
 | |
|             return self.iter.next()
 | |
|         except StopIteration:
 | |
|             raise AssertionError("No more return values")
 | |
|     def __iter__(self):
 | |
|         return self
 | |
| 
 | |
| class ReturnValues(ReturnValuesBase):
 | |
|     def __init__(self, *values):
 | |
|         self.iter = iter(values)
 | |
| 
 | |
| 
 | |
| class ReturnIterator(ReturnValuesBase):
 | |
|     def __init__(self, iterator):
 | |
|         self.iter = iter(iterator)
 | |
| 
 | |
| 
 | |
| def expectParams(*params, **keywords):
 | |
|     '''check that the callObj is called with specified params and keywords
 | |
|     '''
 | |
|     def fn(mockObj, callObj, idx):
 | |
|         return callObj.params == params and callObj.kwparams == keywords
 | |
|     return fn
 | |
| 
 | |
| 
 | |
| def expectAfter(*methods):
 | |
|     '''check that the function is only called after all the functions in 'methods'
 | |
|     '''
 | |
|     def fn(mockObj, callObj, idx):
 | |
|         calledMethods = [method.getName() for method in mockObj.mockGetAllCalls()]
 | |
|         #skip last entry, since that is the current call
 | |
|         calledMethods = calledMethods[:-1]
 | |
|         for method in methods:
 | |
|             if method not in calledMethods:
 | |
|                 return False
 | |
|         return True
 | |
|     return fn
 | |
| 
 | |
| def expectException(exception, *args, **kwargs):
 | |
|     ''' raise an exception when the method is called
 | |
|     '''
 | |
|     def fn(mockObj, callObj, idx):
 | |
|         raise exception(*args, **kwargs)
 | |
|     return fn
 | |
| 
 | |
| 
 | |
| def expectParam(paramIdx, cond):
 | |
|     '''check that the callObj is called with parameter specified by paramIdx (a position index or keyword)
 | |
|     fulfills the condition specified by cond.
 | |
|     cond is a function that takes a single argument, the value to test.
 | |
|     '''
 | |
|     def fn(mockObj, callObj, idx):
 | |
|         param = callObj.getParam(paramIdx)
 | |
|         return cond(param)
 | |
|     return fn
 | |
| 
 | |
| def EQ(value):
 | |
|     def testFn(param):
 | |
|         return param == value
 | |
|     return testFn
 | |
| 
 | |
| def NE(value):
 | |
|     def testFn(param):
 | |
|         return param != value
 | |
|     return testFn
 | |
| 
 | |
| def GT(value):
 | |
|     def testFn(param):
 | |
|         return param > value
 | |
|     return testFn
 | |
| 
 | |
| def LT(value):
 | |
|     def testFn(param):
 | |
|         return param < value
 | |
|     return testFn
 | |
| 
 | |
| def GE(value):
 | |
|     def testFn(param):
 | |
|         return param >= value
 | |
|     return testFn
 | |
| 
 | |
| def LE(value):
 | |
|     def testFn(param):
 | |
|         return param <= value
 | |
|     return testFn
 | |
| 
 | |
| def AND(*condlist):
 | |
|     def testFn(param):
 | |
|         for cond in condlist:
 | |
|             if not cond(param):
 | |
|                 return False
 | |
|         return True
 | |
|     return testFn
 | |
| 
 | |
| def OR(*condlist):
 | |
|     def testFn(param):
 | |
|         for cond in condlist:
 | |
|             if cond(param):
 | |
|                 return True
 | |
|         return False
 | |
|     return testFn
 | |
| 
 | |
| def NOT(cond):
 | |
|     def testFn(param):
 | |
|         return not cond(param)
 | |
|     return testFn
 | |
| 
 | |
| def MATCHES(regex, *args, **kwargs):
 | |
|     compiled_regex = re.compile(regex, *args, **kwargs)
 | |
|     def testFn(param):
 | |
|         return compiled_regex.match(param) is not None
 | |
|     return testFn
 | |
| 
 | |
| def SEQ(*sequence):
 | |
|     iterator = iter(sequence)
 | |
|     def testFn(param):
 | |
|         try:
 | |
|             cond = iterator.next()
 | |
|         except StopIteration:
 | |
|             raise AssertionError('SEQ exhausted')
 | |
|         return cond(param)
 | |
|     return testFn
 | |
| 
 | |
| def IS(instance):
 | |
|     def testFn(param):
 | |
|         return param is instance
 | |
|     return testFn
 | |
| 
 | |
| def ISINSTANCE(class_):
 | |
|     def testFn(param):
 | |
|         return isinstance(param, class_)
 | |
|     return testFn
 | |
| 
 | |
| def ISSUBCLASS(class_):
 | |
|     def testFn(param):
 | |
|         return issubclass(param, class_)
 | |
|     return testFn
 | |
| 
 | |
| def CONTAINS(val):
 | |
|     def testFn(param):
 | |
|         return val in param
 | |
|     return testFn
 | |
| 
 | |
| def IN(container):
 | |
|     def testFn(param):
 | |
|         return param in container
 | |
|     return testFn
 | |
| 
 | |
| def HASATTR(attr):
 | |
|     def testFn(param):
 | |
|         return hasattr(param, attr)
 | |
|     return testFn
 | |
| 
 | |
| def HASMETHOD(method):
 | |
|     def testFn(param):
 | |
|         return hasattr(param, method) and callable(getattr(param, method))
 | |
|     return testFn
 | |
| 
 | |
| CALLABLE = callable
 |