Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions tests/twistedsupport/test_runtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
67 changes: 52 additions & 15 deletions testtools/testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down