Skip to content

Commit 882f2fc

Browse files
authored
Merge pull request #552 from testing-cabal/setup-teardown-deferreds
Support async setUp/tearDown validation with Deferreds
2 parents b130900 + 3ec7c33 commit 882f2fc

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)