Skip to content

Commit 50fa530

Browse files
committed
Support async setUp/tearDown validation with Deferreds
Fix issue where setUp/tearDown validation failed when using AsynchronousDeferredRunTest with Deferred-based async upcalls. The validation logic now detects when setUp() or tearDown() returns a Deferred-like object (duck-typing via addBoth method) and defers the validation check until after the Deferred resolves. This allows patterns where super().setUp() or super().tearDown() is called asynchronously via callback chains. For synchronous cases, validation continues to happen immediately as before. Fixes #547
1 parent a21157f commit 50fa530

File tree

2 files changed

+130
-15
lines changed

2 files changed

+130
-15
lines changed

tests/twistedsupport/test_runtest.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,84 @@ def test_something(self):
11211121
)
11221122

11231123

1124+
class TestAsyncSetUpTearDownValidation(NeedsTwistedTestCase):
1125+
"""Tests for async setUp/tearDown validation with Deferreds.
1126+
1127+
This tests the fix for GitHub issue #547.
1128+
"""
1129+
1130+
def test_async_setup_with_deferred_upcall(self):
1131+
# setUp that calls parent asynchronously via Deferred callback
1132+
# should work correctly with AsynchronousDeferredRunTest.
1133+
from twisted.internet import reactor
1134+
1135+
class AsyncSetUpTest(TestCase):
1136+
run_tests_with = AsynchronousDeferredRunTest
1137+
1138+
def setUp(self):
1139+
d = defer.Deferred()
1140+
d.addCallback(lambda ignored: super(AsyncSetUpTest, self).setUp())
1141+
reactor.callLater(0.0, d.callback, None)
1142+
return d
1143+
1144+
def test_something(self):
1145+
pass
1146+
1147+
test = AsyncSetUpTest("test_something")
1148+
result = TestResult()
1149+
test.run(result)
1150+
# The test should pass - the async setUp should be validated correctly
1151+
self.assertTrue(result.wasSuccessful())
1152+
1153+
def test_async_teardown_with_deferred_upcall(self):
1154+
# tearDown that calls parent asynchronously via Deferred callback
1155+
# should work correctly with AsynchronousDeferredRunTest.
1156+
from twisted.internet import reactor
1157+
1158+
class AsyncTearDownTest(TestCase):
1159+
run_tests_with = AsynchronousDeferredRunTest
1160+
1161+
def tearDown(self):
1162+
d = defer.Deferred()
1163+
d.addCallback(lambda ignored: super(AsyncTearDownTest, self).tearDown())
1164+
reactor.callLater(0.0, d.callback, None)
1165+
return d
1166+
1167+
def test_something(self):
1168+
pass
1169+
1170+
test = AsyncTearDownTest("test_something")
1171+
result = TestResult()
1172+
test.run(result)
1173+
# The test should pass - the async tearDown should be validated correctly
1174+
self.assertTrue(result.wasSuccessful())
1175+
1176+
def test_async_setup_missing_upcall_fails(self):
1177+
# setUp that returns a Deferred but doesn't call parent should fail.
1178+
from twisted.internet import reactor
1179+
1180+
class BadAsyncSetUpTest(TestCase):
1181+
run_tests_with = AsynchronousDeferredRunTest
1182+
1183+
def setUp(self):
1184+
# Returns a Deferred but doesn't call super().setUp()
1185+
d = defer.Deferred()
1186+
reactor.callLater(0.0, d.callback, None)
1187+
return d
1188+
1189+
def test_something(self):
1190+
pass
1191+
1192+
test = BadAsyncSetUpTest("test_something")
1193+
result = TestResult()
1194+
test.run(result)
1195+
# The test should fail with a validation error
1196+
self.assertFalse(result.wasSuccessful())
1197+
self.assertEqual(len(result.errors), 1)
1198+
error_text = str(result.errors[0][1])
1199+
self.assertIn("TestCase.setUp was not called", error_text)
1200+
1201+
11241202
def test_suite():
11251203
from unittest import TestLoader
11261204

testtools/testcase.py

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -694,13 +694,32 @@ def _run_setup(self, result):
694694
ValueError is raised.
695695
"""
696696
ret = self.setUp()
697-
if not self.__setup_called:
698-
raise ValueError(
699-
f"In File: {sys.modules[self.__class__.__module__].__file__}\n"
700-
"TestCase.setUp was not called. Have you upcalled all the "
701-
"way up the hierarchy from your setUp? e.g. Call "
702-
f"super({self.__class__.__name__}, self).setUp() from your setUp()."
703-
)
697+
698+
# Check if the return value is a Deferred (duck-typing to avoid hard dependency)
699+
if hasattr(ret, "addBoth") and callable(getattr(ret, "addBoth")):
700+
# Deferred-like object: validate asynchronously after it resolves
701+
def _validate_setup_called(result):
702+
if not self.__setup_called:
703+
raise ValueError(
704+
f"In File: {sys.modules[self.__class__.__module__].__file__}\n"
705+
"TestCase.setUp was not called. Have you upcalled all the "
706+
"way up the hierarchy from your setUp? e.g. Call "
707+
f"super({self.__class__.__name__}, self).setUp() "
708+
"from your setUp()."
709+
)
710+
return result
711+
712+
ret.addBoth(_validate_setup_called)
713+
else:
714+
# Synchronous: validate immediately
715+
if not self.__setup_called:
716+
raise ValueError(
717+
f"In File: {sys.modules[self.__class__.__module__].__file__}\n"
718+
"TestCase.setUp was not called. Have you upcalled all the "
719+
"way up the hierarchy from your setUp? e.g. Call "
720+
f"super({self.__class__.__name__}, self).setUp() "
721+
"from your setUp()."
722+
)
704723
return ret
705724

706725
def _run_teardown(self, result):
@@ -711,14 +730,32 @@ def _run_teardown(self, result):
711730
ValueError is raised.
712731
"""
713732
ret = self.tearDown()
714-
if not self.__teardown_called:
715-
raise ValueError(
716-
f"In File: {sys.modules[self.__class__.__module__].__file__}\n"
717-
"TestCase.tearDown was not called. Have you upcalled all the "
718-
"way up the hierarchy from your tearDown? e.g. Call "
719-
f"super({self.__class__.__name__}, self).tearDown() "
720-
"from your tearDown()."
721-
)
733+
734+
# Check if the return value is a Deferred (duck-typing to avoid hard dependency)
735+
if hasattr(ret, "addBoth") and callable(getattr(ret, "addBoth")):
736+
# Deferred-like object: validate asynchronously after it resolves
737+
def _validate_teardown_called(result):
738+
if not self.__teardown_called:
739+
raise ValueError(
740+
f"In File: {sys.modules[self.__class__.__module__].__file__}\n"
741+
"TestCase.tearDown was not called. Have you upcalled all the "
742+
"way up the hierarchy from your tearDown? e.g. Call "
743+
f"super({self.__class__.__name__}, self).tearDown() "
744+
"from your tearDown()."
745+
)
746+
return result
747+
748+
ret.addBoth(_validate_teardown_called)
749+
else:
750+
# Synchronous: validate immediately
751+
if not self.__teardown_called:
752+
raise ValueError(
753+
f"In File: {sys.modules[self.__class__.__module__].__file__}\n"
754+
"TestCase.tearDown was not called. Have you upcalled all the "
755+
"way up the hierarchy from your tearDown? e.g. Call "
756+
f"super({self.__class__.__name__}, self).tearDown() "
757+
"from your tearDown()."
758+
)
722759
return ret
723760

724761
def _get_test_method(self):

0 commit comments

Comments
 (0)