I have some trial unit tests. In my code base some methods return deferreds with callbacks added while others are decorated with @inlineCallbacks. I want to run the test without a reactor, as some of the methods don't do any I/O. I tought @inlineCallbacks just returns a deferred, so calling callbacks(0) to fire it would be enough. It seems not the case. Here is a minimal example:
from twisted.trial import unittest
from twisted.internet.defer import inlineCallbacks, returnValue, Deferred
def addone(val):
d = Deferred()
def cbk(res):
return val + 1
d.addCallback(cbk)
return d
@inlineCallbacks
def call_addone(val):
res = yield addone(val)
returnValue(res)
class Tester(unittest.TestCase):
def test_addone(self):
d = addone(2)
d.callback(0) # whatever I pass is ignored
self.assertEqual(3, self.successResultOf(d))
def test_call_addone(self):
d = call_addone(4)
d.callback(0) # whatever I pass is set as deferred's result
self.assertEqual(5, self.successResultOf(d))
When I call addone(2) I get back a deferred that will return 2+1 when I fire it with callback(0). In this case the value passed to callback is ignored. In the second test call_addone(4) returns a deferred too. But in this case the parameter passed to it is ignored, instead the return value will be whatever I pass to callback(). Why? I am clearly missing something.
Here is the output of trial runner:
test_trial
Tester
test_addone ... [OK]
test_call_addone ... [FAIL]
===============================================================================
[FAIL]
Traceback (most recent call last):
File "tests/test_trial.py", line 31, in test_call_addone
self.assertEqual(5, self.successResultOf(d))
File "/home/b/.local/share/virtualenvs/twproba-y019OThE/lib/python3.5/site-packages/twisted/trial/_synctest.py", line 432, in assertEqual
super(_Assertions, self).assertEqual(first, second, msg)
File "/usr/lib/python3.5/unittest/case.py", line 821, in assertEqual
assertion_func(first, second, msg=msg)
File "/usr/lib/python3.5/unittest/case.py", line 814, in _baseAssertEqual
raise self.failureException(msg)
twisted.trial.unittest.FailTest: 5 != 0
test_trial.Tester.test_call_addone
-------------------------------------------------------------------------------
Ran 2 tests in 0.029s
FAILED (failures=1, successes=1)
These lines are pretty much wrong (given that call_addone
is defined with inlineCallbacks
):
d = call_addone(4)
d.callback(0) # whatever I pass is set as deferred's result
This goes along with these lines being weird (given that inlineCallbacks
is not used):
d = addone(2)
d.callback(0) # whatever I pass is ignored
In both cases, you are asking some library code to create a Deferred
and then your application code is supplying a result for the Deferred
. Since addone
does what you want, clearly it's possible to use Deferred
this way. However, it's not a good practice. Best practice is for the responsibility for creating and firing a Deferred
to lie in the same place. So, in this case, with the implementation of addone
.
The reason your failing test fails is that you've not accounted for the implementation of inlineCallbacks
which believes that it is indeed responsible for firing the Deferred
it returns. And the Deferred
it returns is not the Deferred
returned by the addone
call it makes. It keeps that Deferred
to itself.
The result of d
in:
d = call_addone(4)
d.callback(0) # whatever I pass is set as deferred's result
is 0
precisely because you've supplied a result of 0. You've supplied the result to the inlineCallbacks
-managed Deferred
and short-circuited all of the rest of the implementation of inlineCallbacks
. If the Deferred
returned by addone
did ever fire, you would likely see an AlreadyCalledError
because inlineCallbacks
would try to supply a result to d
, the Deferred
you already supplied a result to.