Skip to content

Commit

Permalink
Merge sync-deferred-testing-helper-6105-2
Browse files Browse the repository at this point in the history
Author: exarkun
Reviewer: therve
Fixes: twisted#6105


Add `successResultOf`, `failureResultOf`, and `assertNoResult` to `SynchronousTestCase`, to help
developers write tests for `Deferred`-using code.


git-svn-id: svn://svn.twistedmatrix.com/svn/Twisted/trunk@36506 bbbe8e31-12d6-0310-92fd-ac37d47ddeeb
  • Loading branch information
exarkun committed Dec 4, 2012
1 parent 82f2a96 commit bbc9494
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_timeoutConnectionLost(self):
def lost(arg):
called.append(True)
self.proto.connectionLost = lost

d = self.proto.add(9, 4)
self.assertEqual(self.tr.value(), 'add 9 4\r\n')
self.clock.advance(self.proto.timeOut)
Expand Down
63 changes: 63 additions & 0 deletions doc/core/howto/listings/trial/calculus/test/test_client_4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from calculus.client_3 import RemoteCalculationClient, ClientTimeoutError

from twisted.internet import task
from twisted.trial import unittest
from twisted.test import proto_helpers



class ClientCalculationTestCase(unittest.TestCase):
def setUp(self):
self.tr = proto_helpers.StringTransportWithDisconnection()
self.clock = task.Clock()
self.proto = RemoteCalculationClient()
self.tr.protocol = self.proto
self.proto.callLater = self.clock.callLater
self.proto.makeConnection(self.tr)


def _test(self, operation, a, b, expected):
d = getattr(self.proto, operation)(a, b)
self.assertEqual(self.tr.value(), '%s %d %d\r\n' % (operation, a, b))
self.tr.clear()
self.proto.dataReceived("%d\r\n" % (expected,))
self.assertEqual(expected, self.successResultOf(d))


def test_add(self):
self._test('add', 7, 6, 13)


def test_subtract(self):
self._test('subtract', 82, 78, 4)


def test_multiply(self):
self._test('multiply', 2, 8, 16)


def test_divide(self):
self._test('divide', 14, 3, 4)


def test_timeout(self):
d = self.proto.add(9, 4)
self.assertEqual(self.tr.value(), 'add 9 4\r\n')
self.clock.advance(self.proto.timeOut)
self.failureResultOf(d).trap(ClientTimeoutError)


def test_timeoutConnectionLost(self):
called = []
def lost(arg):
called.append(True)
self.proto.connectionLost = lost

d = self.proto.add(9, 4)
self.assertEqual(self.tr.value(), 'add 9 4\r\n')
self.clock.advance(self.proto.timeOut)

def check(ignore):
self.assertEqual(called, [True])
self.failureResultOf(d).trap(ClientTimeoutError)
self.assertEqual(called, [True])
44 changes: 43 additions & 1 deletion doc/core/howto/trial.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,9 @@ that we don't use a client factory. We're lazy, and it's not very useful in
the client part, so we instantiate the protocol directly.</p>

<p>Incidentally, we have introduced a very important concept here: the tests
now return a Deferred object, and the assertion is done in a callback. The
now return a Deferred object, and the assertion is done in a callback. When
a test returns a Deferred, the reactor is run until the Deferred fires and
its callbacks run. The
important thing to do here is to <strong>not forget to return the
Deferred</strong>. If you do, your tests will pass even if nothing is asserted.
That's also why it's important to make tests fail first: if your tests pass
Expand Down Expand Up @@ -624,6 +626,46 @@ changes nothing to our class.</p>

<a href="listings/trial/calculus/client_3.py" class="py-listing">client_3.py</a>

<h2>Testing Deferreds without the reactor</h2>

<p>Above we learned about returning Deferreds from test methods in order to make
assertions about their results, or side-effects that only happen after they
fire. This can be useful, but we don't actually need the feature in this
example. Because we were careful to use <code class="python">Clock</code>, we
don't need the global reactor to run in our tests. Instead of returning the
Deferred with a callback attached to it which performs the necessary assertions,
we can use a testing helper,
<code class="API" base="twisted.trial.unittest.SynchronousTestCase">successResultOf</code> (and
the corresponding error-case helper
<code class="API" base="twisted.trial.unittest.SynchronousTestCase">failureResultOf</code>), to
extract its result and make assertions against it directly. Compared to
returning a Deferred, this avoids the problem of forgetting to return the
Deferred, improves the stack trace reported when the assertion fails, and avoids
the complexity of using global reactor (which, for example, may then require
cleanup).</p>

<a href="listings/trial/calculus/test/test_client_4.py" class="py-listing">test_client_4.py</a>

<p>This version of the code makes the same assertions, but no longer returns any
Deferreds from any test methods. Instead of making assertions about the result
of the Deferred in a callback, it makes the assertions as soon as
it <em>knows</em> the Deferred is supposed to have a result (in
the <code>_test</code> method and in <code>test_timeout</code>
and <code>test_timeoutConnectionLost</code>). The possibility
of <em>knowing</em> exactly when a Deferred is supposed to have a test is what
makes <code>successResultOf</code> useful in unit testing, but prevents it from being
applicable to non-testing purposes.</p>

<p><code>successResultOf</code> will raise an exception (failing the test) if
the <code>Deferred</code> passed to it does not have a result, or has a failure
result. Similarly, <code>failureResultOf</code> will raise an exception (also
failing the test) if the <code>Deferred</code> passed to it does not have a
result, or has a success result. There is a third helper method for testing the
final case,
<code class="API" base="twisted.trial.unittest.SynchronousTestCase">assertNoResult</code>,
which only raises an exception (failing the test) if the <code>Deferred</code> passed
to it <em>has</em> a result (either success or failure).</p>

<h2>Dropping into a debugger</h2>

<p>In the course of writing and running your tests, it is often helpful to
Expand Down
1 change: 1 addition & 0 deletions twisted/topfiles/6105.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Several new methods of twisted.trial.unittest.SynchronousTestCase -- `successResultOf`, `failureResultOf`, and `assertNoResult` - have been added to make testing `Deferred`-using code easier.
93 changes: 93 additions & 0 deletions twisted/trial/_synctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,99 @@ def assertNotIsInstance(self, instance, classOrTuple):
failIfIsInstance = assertNotIsInstance


def successResultOf(self, deferred):
"""
Return the current success result of C{deferred} or raise
C{self.failException}.
@param deferred: A L{Deferred<twisted.internet.defer.Deferred>} which
has a success result. This means
L{Deferred.callback<twisted.internet.defer.Deferred.callback>} or
L{Deferred.errback<twisted.internet.defer.Deferred.errback>} has
been called on it and it has reached the end of its callback chain
and the last callback or errback returned a non-L{failure.Failure}.
@type deferred: L{Deferred<twisted.internet.defer.Deferred>}
@raise SynchronousTestCase.failureException: If the
L{Deferred<twisted.internet.defer.Deferred>} has no result or has a
failure result.
@return: The result of C{deferred}.
"""
result = []
deferred.addBoth(result.append)
if not result:
self.fail(
"Success result expected on %r, found no result instead" % (
deferred,))
elif isinstance(result[0], failure.Failure):
self.fail(
"Success result expected on %r, "
"found failure result (%r) instead" % (deferred, result[0]))
else:
return result[0]


def failureResultOf(self, deferred):
"""
Return the current failure result of C{deferred} or raise
C{self.failException}.
@param deferred: A L{Deferred<twisted.internet.defer.Deferred>} which
has a failure result. This means
L{Deferred.callback<twisted.internet.defer.Deferred.callback>} or
L{Deferred.errback<twisted.internet.defer.Deferred.errback>} has
been called on it and it has reached the end of its callback chain
and the last callback or errback raised an exception or returned a
L{failure.Failure}.
@type deferred: L{Deferred<twisted.internet.defer.Deferred>}
@raise SynchronousTestCase.failureException: If the
L{Deferred<twisted.internet.defer.Deferred>} has no result or has a
success result.
@return: The failure result of C{deferred}.
@rtype: L{failure.Failure}
"""
result = []
deferred.addBoth(result.append)
if not result:
self.fail(
"Failure result expected on %r, found no result instead" % (
deferred,))
elif not isinstance(result[0], failure.Failure):
self.fail(
"Failure result expected on %r, "
"found success result (%r) instead" % (deferred, result[0]))
else:
return result[0]



def assertNoResult(self, deferred):
"""
Assert that C{deferred} does not have a result at this point.
@param deferred: A L{Deferred<twisted.internet.defer.Deferred>} without
a result. This means that neither
L{Deferred.callback<twisted.internet.defer.Deferred.callback>} nor
L{Deferred.errback<twisted.internet.defer.Deferred.errback>} has
been called, or that the
L{Deferred<twisted.internet.defer.Deferred>} is waiting on another
L{Deferred<twisted.internet.defer.Deferred>} for a result.
@type deferred: L{Deferred<twisted.internet.defer.Deferred>}
@raise SynchronousTestCase.failureException: If the
L{Deferred<twisted.internet.defer.Deferred>} has a result.
"""
result = []
deferred.addBoth(result.append)
if result:
self.fail(
"No result expected on %r, found %r instead" % (
deferred, result[0]))



class _LogObserver(object):
"""
Expand Down
102 changes: 102 additions & 0 deletions twisted/trial/test/test_assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
from twisted.python._reflectpy3 import prefixedMethods, accumulateMethods
from twisted.python.deprecate import deprecated
from twisted.python.versions import Version, getVersionString
from twisted.python.failure import Failure
from twisted.trial import unittest
from twisted.internet.defer import Deferred, fail, succeed

class MockEquality(FancyEqMixin, object):
compareAttributes = ("name",)
Expand Down Expand Up @@ -807,6 +809,106 @@ def f(message):



class TestResultOfAssertions(unittest.SynchronousTestCase):
"""
Tests for L{SynchronousTestCase.successResultOf},
L{SynchronousTestCase.failureResultOf}, and
L{SynchronousTestCase.assertNoResult}.
"""
result = object()
failure = Failure(Exception("Bad times"))

def test_withoutSuccessResult(self):
"""
L{SynchronousTestCase.successResultOf} raises
L{SynchronousTestCase.failureException} when called with a L{Deferred}
with no current result.
"""
self.assertRaises(
self.failureException, self.successResultOf, Deferred())


def test_successResultOfWithFailure(self):
"""
L{SynchronousTestCase.successResultOf} raises
L{SynchronousTestCase.failureException} when called with a L{Deferred}
with a failure result.
"""
self.assertRaises(
self.failureException, self.successResultOf, fail(self.failure))


def test_withoutFailureResult(self):
"""
L{SynchronousTestCase.failureResultOf} raises
L{SynchronousTestCase.failureException} when called with a L{Deferred}
with no current result.
"""
self.assertRaises(
self.failureException, self.failureResultOf, Deferred())


def test_failureResultOfWithSuccess(self):
"""
L{SynchronousTestCase.failureResultOf} raises
L{SynchronousTestCase.failureException} when called with a L{Deferred}
with a success result.
"""
self.assertRaises(
self.failureException, self.failureResultOf, succeed(self.result))


def test_withSuccessResult(self):
"""
When passed a L{Deferred} which currently has a result (ie,
L{Deferred.addCallback} would cause the added callback to be called
before C{addCallback} returns), L{SynchronousTestCase.successResultOf}
returns that result.
"""
self.assertIdentical(
self.result, self.successResultOf(succeed(self.result)))


def test_withFailureResult(self):
"""
When passed a L{Deferred} which currently has a L{Failure} result (ie,
L{Deferred.addErrback} would cause the added errback to be called before
C{addErrback} returns), L{SynchronousTestCase.failureResultOf} returns
that L{Failure}.
"""
self.assertIdentical(
self.failure, self.failureResultOf(fail(self.failure)))


def test_assertNoResultSuccess(self):
"""
When passed a L{Deferred} which currently has a success result (see
L{test_withSuccessResult}), L{SynchronousTestCase.assertNoResult} raises
L{SynchronousTestCase.failureException}.
"""
self.assertRaises(
self.failureException, self.assertNoResult, succeed(self.result))


def test_assertNoResultFailure(self):
"""
When passed a L{Deferred} which currently has a failure result (see
L{test_withFailureResult}), L{SynchronousTestCase.assertNoResult} raises
L{SynchronousTestCase.failureException}.
"""
self.assertRaises(
self.failureException, self.assertNoResult, fail(self.failure))


def test_assertNoResult(self):
"""
When passed a L{Deferred} with no current result,
L{SynchronousTestCase.assertNoResult} raises no exception.
"""
self.assertNoResult(Deferred())



class TestAssertionNames(unittest.SynchronousTestCase):
"""
Tests for consistency of naming within TestCase assertion methods
Expand Down
Loading

0 comments on commit bbc9494

Please sign in to comment.