pythontwisteddeferredtrial

Using @inlineCallbacks in Twisted trial unit tests


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)

Solution

  • 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.