Skip to content

Commit

Permalink
For SinonStub, it's now possible to chain multiple returns calls
Browse files Browse the repository at this point in the history
- e.g.
stub.withArgs(42).onFirstCall().returns(1).onSecondCall().returns(2)
- Makes progress toward fixing #23

Fixes #30
  • Loading branch information
jonathan-benn-copilot authored and note35 committed Nov 13, 2017
1 parent 6477cc3 commit 0b7e17f
Show file tree
Hide file tree
Showing 3 changed files with 236 additions and 75 deletions.
207 changes: 134 additions & 73 deletions sinon/lib/stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,118 +17,179 @@ class SinonStub(SinonSpy):
(4) a module
All function/module will be replaced into customized or empty function/class
Able to be assigned special condition (on which call, with what args...etc)
Able to be assigned special conditions (on which call, with what args, etc.)
"""

def __init__(self, obj=None, prop=None, func=None):
super(SinonStub, self).__init__(obj, prop)
self.stubfunc = func if func else Wrapper.empty_function
super(SinonStub, self).wrap2stub(self.stubfunc)
self._stubfunc = func if func else Wrapper.empty_function
super(SinonStub, self).wrap2stub(self._stubfunc)
self._cond_args = self._cond_kwargs = self._oncall = None
# Todo: target is a dirty hack
self.condition = {"args":[], "kwargs":[], "action": [], "oncall":[], "target":self.obj}
self.cond_args = self.cond_kwargs = self.oncall = None

def __append_condition(self, func):
self.condition["args"].append(self.cond_args)
self.condition["kwargs"].append(self.cond_kwargs)
self.condition["oncall"].append(self.oncall)
self.condition["action"].append(func)
self.cond_args = self.cond_kwargs = self.oncall = None
self._conditions = {"args":[], "kwargs":[], "action": [], "oncall": [], "target": self.obj}

def _append_condition(self, sinon_stub_condition, func):
'''
Permanently saves the current (volatile) conditions, which would be otherwise lost
Args:
sinon_stub_condition: the SinonStubCondition object that holds the current conditions
func: returns a value or raises an exception, as specified by the user
Returns: the SinonStub._conditions dictionary (for convenience)
'''
self._conditions["args"].append(sinon_stub_condition._cond_args)
self._conditions["kwargs"].append(sinon_stub_condition._cond_kwargs)
self._conditions["oncall"].append(sinon_stub_condition._oncall)
self._conditions["action"].append(func)
return self._conditions

def _get_original(self):
"""
Returns the original SinonStub object that wrapped the function under test
"""
return self

def withArgs(self, *args, **kwargs): #pylint: disable=invalid-name
"""
Adding arguments into condition list
When meeting conditions, special return will be triggered
Adds a condition for when the stub is called. When the condition is met, a special
return value can be returned. Adds the specified argument(s) into the condition list.
When stub function is called with arguments 1, it will return "#"
For example, when the stub function is called with argument 1, it will return "#":
stub.withArgs(1).returns("#")
Without returns/throws in the end of chained functions, nothing will happen
In this case, although 1 is in condition list, nothing will happed
Without returns/throws at the end of the chain of functions, nothing will happen.
For example, in this case, although 1 is in the condition list, nothing will happen:
stub.withArgs(1)
Return:
self (able to be chained)
a SinonStub object (able to be chained)
"""
if args:
self.cond_args = args
if kwargs:
self.cond_kwargs = kwargs
return self
cond_args = args if len(args) > 0 else None
cond_kwargs = kwargs if len(kwargs) > 0 else None
return SinonStubCondition(copy_of=self, cond_args=cond_args, cond_kwargs=cond_kwargs, oncall=self._oncall)

def onCall(self, n): #pylint: disable=invalid-name
"""
Adding specified call number into condition list
Adds a condition for when the stub is called. When the condition is met, a special
return value can be returned. Adds the specified call number into the condition
list.
When stub function is called second times, it will return "#"
stub.onCall(2).returns("#")
For example, when the stub function is called the second time, it will return "#":
stub.onCall(1).returns("#")
Without returns/throws in the end of chained functions, nothing will happen
In this case, although 2 is in condition list, nothing will happed
Without returns/throws at the end of the chain of functions, nothing will happen.
For example, in this case, although 2 is in the condition list, nothing will happen:
stub.onCall(2)
Args:
n: integer, the call # for which we want a special return value.
The first call has an index of 0.
Return:
self (able to be chained)
a SinonStub object (able to be chained)
"""
self.oncall = n + 1
return self
cond_oncall = n + 1
return SinonStubCondition(copy_of=self, oncall=cond_oncall, cond_args=self._cond_args, cond_kwargs=self._cond_kwargs)

def onFirstCall(self): #pylint: disable=invalid-name,missing-docstring
self.oncall = 1
return self
def onFirstCall(self): #pylint: disable=invalid-name
"""
Equivalent to stub.onCall(0)
"""
return self.onCall(0)

def onSecondCall(self): #pylint: disable=invalid-name,missing-docstring
self.oncall = 2
def onSecondCall(self): #pylint: disable=invalid-name
"""
Equivalent to stub.onCall(1)
"""
return self.onCall(1)

def onThirdCall(self): #pylint: disable=invalid-name
"""
Equivalent to stub.onCall(2)
"""
return self.onCall(2)

def returns(self, obj):
"""
Customizes the return values of the stub function. If conditions like withArgs or onCall
were specified, then the return value will only be returned when the conditions are met.
Args: obj (anything)
Return: a SinonStub object (able to be chained)
"""
super(SinonStub, self).wrap2stub(lambda *args, **kwargs: obj)
return self

def onThirdCall(self): #pylint: disable=invalid-name,missing-docstring
self.oncall = 3
def throws(self, exception=Exception):
"""
Customizes the stub function to raise an exception. If conditions like withArgs or onCall
were specified, then the return value will only be returned when the conditions are met.
Args: exception (by default=Exception, it could be any customized exception)
Return: a SinonStub object (able to be chained)
"""
def exception_function(*args, **kwargs):
raise exception
super(SinonStub, self).wrap2stub(exception_function)
return self

def returns(self, obj):
class SinonStubCondition(SinonStub):
"""
Allows a new SinonStub object to be created each time the end user specifies a new condition. This is necessary
to mimic the behaviour of Sinon.JS.
Author: Jonathan Benn
"""

def __new__(cls, *args, **kwargs):
"""
Override the __new__ provided by SinonBase, since we don't want to do any function wrapping
"""
Customizing return of stub function
return object.__new__(cls)

The final chained functions of returns/throws will be triggered
If there is some conditions, it will ONLY triggered when meeting conditions
def __init__(self, copy_of, cond_args, cond_kwargs, oncall):
"""
Args:
copy_of: the original SinonStub object that spawned this one
cond_args: the args to which a subsequent call to returns/throws should apply
cond_kwargs: the kwargs to which a subsequent call to returns/throws should apply
oncall: the integer call number to which a subsequent call to returns/throws should apply
"""
self.__copy_of = copy_of
self._cond_args = cond_args
self._cond_kwargs = cond_kwargs
self._oncall = oncall

def _get_original(self):
"""
Returns the original SinonStub object that wrapped the function under test
"""
return self.__copy_of._get_original()

def returns(self, obj):
"""
Customizes the return values of the stub function. If conditions like withArgs or onCall
were specified, then the return value will only be returned when the conditions are met.
Args: obj (anything)
Return: self (able to be chained)
"""
def return_function(*args, **kwargs):
"""
A stub function with customized return
"""
_ = args, kwargs
return obj

if self.cond_args or self.cond_kwargs or self.oncall:
self.__append_condition(return_function)
super(SinonStub, self).wrap2stub(self.stubfunc, self.condition)
else:
super(SinonStub, self).wrap2stub(return_function)
Return: a SinonStub object (able to be chained)
"""
original = self._get_original()
conditions = original._append_condition(self, lambda *args, **kwargs: obj)
super(SinonStub, original).wrap2stub(original._stubfunc, conditions)
return self

def throws(self, exceptions=Exception):
def throws(self, exception=Exception):
"""
Customizing exception of stub function
The final chained functions of returns/throws will be triggered
If there is some conditions, it will ONLY triggered when meeting conditions
Customizes the stub function to raise an exception. If conditions like withArgs or onCall
were specified, then the return value will only be returned when the conditions are met.
Args: exception (by default=Exception, it could be any customized exception)
Return: self (able to be chained)
Return: a SinonStub object (able to be chained)
"""
def exception_function(*args, **kwargs):
"""
A stub function with customized exception
"""
_ = args, kwargs
raise exceptions

if self.cond_args or self.cond_kwargs or self.oncall:
self.__append_condition(exception_function)
super(SinonStub, self).wrap2stub(self.stubfunc, self.condition)
else:
super(SinonStub, self).wrap2stub(exception_function)
raise exception
original = self._get_original()
conditions = original._append_condition(self, exception_function)
super(SinonStub, original).wrap2stub(original._stubfunc, conditions)
return self
2 changes: 1 addition & 1 deletion sinon/lib/util/Wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def wrapped(*args, **kwargs):
# Todo: make sure e.__class__ is enough for all purpose or not
wrapped.error_list.append(excpt.__class__)
call.exception = excpt
return func(*args, **kwargs)
raise excpt

wrapped.__set__ = __set__
wrapped.callCount = 0
Expand Down
102 changes: 101 additions & 1 deletion sinon/test/TestSinonStub.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ def test370_multiple_onCall_returns(self):
self.assertEqual(o.A_func(), 30)

@sinontest
def test370_multiple_onCall_returns_named_functions(self):
def test371_multiple_onCall_returns_named_functions(self):
o = A_object()
stub = SinonStub(o, 'A_func')
stub.onFirstCall().returns(5)
Expand All @@ -280,3 +280,103 @@ def test370_multiple_onCall_returns_named_functions(self):
self.assertEqual(o.A_func(), 5)
self.assertEqual(o.A_func(), 10)
self.assertEqual(o.A_func(), 20)

@sinontest
def test380_chained_pure_returns(self):
stub = SinonStub()
stub.withArgs(42).onFirstCall().returns(1).onSecondCall().returns(2)
self.assertEqual(stub(42), 1)
self.assertEqual(stub(42), 2)

@sinontest
def test381_chained_module_returns(self):
stub = SinonStub(os, 'system')
stub.withArgs(42).onFirstCall().returns(1).onSecondCall().returns(2)
self.assertEqual(os.system(42), 1)
self.assertEqual(os.system(42), 2)

@sinontest
def test382_chained_function_returns(self):
stub = SinonStub(C_func)
stub.withArgs(42).onFirstCall().returns(1).onSecondCall().returns(2)
self.assertEqual(stub.g.C_func(42), 1)
self.assertEqual(stub.g.C_func(42), 2)

@sinontest
def test383_chained_method_returns(self):
o = A_object()
stub = SinonStub(o, 'A_func')
stub.withArgs(42).onFirstCall().returns(1).onSecondCall().returns(2)
self.assertEqual(o.A_func(42), 1)
self.assertEqual(o.A_func(42), 2)

@sinontest
def test390_chained_pure_throws(self):
stub = SinonStub()
stub.withArgs(42).onFirstCall().throws(Exception('A')).onSecondCall().throws(Exception('B'))
with self.assertRaisesRegexp(Exception, 'A'):
stub(42)
with self.assertRaisesRegexp(Exception, 'B'):
stub(42)

@sinontest
def test391_chained_module_throws(self):
stub = SinonStub(os, 'system')
stub.withArgs(42).onFirstCall().throws(Exception('A')).onSecondCall().throws(Exception('B'))
with self.assertRaisesRegexp(Exception, 'A'):
os.system(42)
with self.assertRaisesRegexp(Exception, 'B'):
os.system(42)

@sinontest
def test392_chained_function_throws(self):
stub = SinonStub(C_func)
stub.withArgs(42).onFirstCall().throws(Exception('A')).onSecondCall().throws(Exception('B'))
with self.assertRaisesRegexp(Exception, 'A'):
stub.g.C_func(42)
with self.assertRaisesRegexp(Exception, 'B'):
stub.g.C_func(42)

@sinontest
def test393_chained_method_throws(self):
o = A_object()
stub = SinonStub(o, 'A_func')
stub.withArgs(42).onFirstCall().throws(Exception('A')).onSecondCall().throws(Exception('B'))
with self.assertRaisesRegexp(Exception, 'A'):
o.A_func(42)
with self.assertRaisesRegexp(Exception, 'B'):
o.A_func(42)

@sinontest
def test410_conditions_do_not_persist(self):
stub = SinonStub()
stub.withArgs('A')
stub.onThirdCall()
stub.returns(5)
self.assertEqual(stub(), 5)

@sinontest
def test415_conditions_can_be_overwritten_withArgs(self):
stub = SinonStub()
stub.withArgs('A').withArgs('B').returns(3)
self.assertEqual(stub('A'), None)
self.assertEqual(stub('B'), 3)

@sinontest
def test420_conditions_can_be_overwritten_onCall(self):
stub = SinonStub()
stub.onFirstCall().onSecondCall().returns(3)
self.assertEqual(stub(), None)
self.assertEqual(stub(), 3)

@sinontest
def test425_returns_throws_can_be_overwritten(self):
stub = SinonStub()
self.assertEqual(stub(), None)
stub.returns(5)
self.assertEqual(stub(), 5)
stub.throws()
with self.assertRaises(Exception):
stub()
stub.returns(10)
self.assertEqual(stub(), 10)

0 comments on commit 0b7e17f

Please sign in to comment.