diff --git a/tests/twistedsupport/test_runtest.py b/tests/twistedsupport/test_runtest.py index ab92a691..e9062947 100644 --- a/tests/twistedsupport/test_runtest.py +++ b/tests/twistedsupport/test_runtest.py @@ -1121,6 +1121,84 @@ def test_something(self): ) +class TestAsyncSetUpTearDownValidation(NeedsTwistedTestCase): + """Tests for async setUp/tearDown validation with Deferreds. + + This tests the fix for GitHub issue #547. + """ + + def test_async_setup_with_deferred_upcall(self): + # setUp that calls parent asynchronously via Deferred callback + # should work correctly with AsynchronousDeferredRunTest. + from twisted.internet import reactor + + class AsyncSetUpTest(TestCase): + run_tests_with = AsynchronousDeferredRunTest + + def setUp(self): + d = defer.Deferred() + d.addCallback(lambda ignored: super(AsyncSetUpTest, self).setUp()) + reactor.callLater(0.0, d.callback, None) + return d + + def test_something(self): + pass + + test = AsyncSetUpTest("test_something") + result = TestResult() + test.run(result) + # The test should pass - the async setUp should be validated correctly + self.assertTrue(result.wasSuccessful()) + + def test_async_teardown_with_deferred_upcall(self): + # tearDown that calls parent asynchronously via Deferred callback + # should work correctly with AsynchronousDeferredRunTest. + from twisted.internet import reactor + + class AsyncTearDownTest(TestCase): + run_tests_with = AsynchronousDeferredRunTest + + def tearDown(self): + d = defer.Deferred() + d.addCallback(lambda ignored: super(AsyncTearDownTest, self).tearDown()) + reactor.callLater(0.0, d.callback, None) + return d + + def test_something(self): + pass + + test = AsyncTearDownTest("test_something") + result = TestResult() + test.run(result) + # The test should pass - the async tearDown should be validated correctly + self.assertTrue(result.wasSuccessful()) + + def test_async_setup_missing_upcall_fails(self): + # setUp that returns a Deferred but doesn't call parent should fail. + from twisted.internet import reactor + + class BadAsyncSetUpTest(TestCase): + run_tests_with = AsynchronousDeferredRunTest + + def setUp(self): + # Returns a Deferred but doesn't call super().setUp() + d = defer.Deferred() + reactor.callLater(0.0, d.callback, None) + return d + + def test_something(self): + pass + + test = BadAsyncSetUpTest("test_something") + result = TestResult() + test.run(result) + # The test should fail with a validation error + self.assertFalse(result.wasSuccessful()) + self.assertEqual(len(result.errors), 1) + error_text = str(result.errors[0][1]) + self.assertIn("TestCase.setUp was not called", error_text) + + def test_suite(): from unittest import TestLoader diff --git a/testtools/testcase.py b/testtools/testcase.py index 64724e7a..be335a71 100644 --- a/testtools/testcase.py +++ b/testtools/testcase.py @@ -694,13 +694,32 @@ def _run_setup(self, result): ValueError is raised. """ ret = self.setUp() - if not self.__setup_called: - raise ValueError( - f"In File: {sys.modules[self.__class__.__module__].__file__}\n" - "TestCase.setUp was not called. Have you upcalled all the " - "way up the hierarchy from your setUp? e.g. Call " - f"super({self.__class__.__name__}, self).setUp() from your setUp()." - ) + + # Check if the return value is a Deferred (duck-typing to avoid hard dependency) + if hasattr(ret, "addBoth") and callable(getattr(ret, "addBoth")): + # Deferred-like object: validate asynchronously after it resolves + def _validate_setup_called(result): + if not self.__setup_called: + raise ValueError( + f"In File: {sys.modules[self.__class__.__module__].__file__}\n" + "TestCase.setUp was not called. Have you upcalled all the " + "way up the hierarchy from your setUp? e.g. Call " + f"super({self.__class__.__name__}, self).setUp() " + "from your setUp()." + ) + return result + + ret.addBoth(_validate_setup_called) + else: + # Synchronous: validate immediately + if not self.__setup_called: + raise ValueError( + f"In File: {sys.modules[self.__class__.__module__].__file__}\n" + "TestCase.setUp was not called. Have you upcalled all the " + "way up the hierarchy from your setUp? e.g. Call " + f"super({self.__class__.__name__}, self).setUp() " + "from your setUp()." + ) return ret def _run_teardown(self, result): @@ -711,14 +730,32 @@ def _run_teardown(self, result): ValueError is raised. """ ret = self.tearDown() - if not self.__teardown_called: - raise ValueError( - f"In File: {sys.modules[self.__class__.__module__].__file__}\n" - "TestCase.tearDown was not called. Have you upcalled all the " - "way up the hierarchy from your tearDown? e.g. Call " - f"super({self.__class__.__name__}, self).tearDown() " - "from your tearDown()." - ) + + # Check if the return value is a Deferred (duck-typing to avoid hard dependency) + if hasattr(ret, "addBoth") and callable(getattr(ret, "addBoth")): + # Deferred-like object: validate asynchronously after it resolves + def _validate_teardown_called(result): + if not self.__teardown_called: + raise ValueError( + f"In File: {sys.modules[self.__class__.__module__].__file__}\n" + "TestCase.tearDown was not called. Have you upcalled all the " + "way up the hierarchy from your tearDown? e.g. Call " + f"super({self.__class__.__name__}, self).tearDown() " + "from your tearDown()." + ) + return result + + ret.addBoth(_validate_teardown_called) + else: + # Synchronous: validate immediately + if not self.__teardown_called: + raise ValueError( + f"In File: {sys.modules[self.__class__.__module__].__file__}\n" + "TestCase.tearDown was not called. Have you upcalled all the " + "way up the hierarchy from your tearDown? e.g. Call " + f"super({self.__class__.__name__}, self).tearDown() " + "from your tearDown()." + ) return ret def _get_test_method(self):