https://github.com/python/cpython/commit/198a83581cfdcf9f6c4050e3d1567ab49af754eb
commit: 198a83581cfdcf9f6c4050e3d1567ab49af754eb
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-11T11:20:56+03:00
summary:
gh-155411: Fix test.support.subTests() for asynchronous tests (GH-155412)
An asynchronous test was wrapped in a synchronous function, which
discarded the coroutine without awaiting it, so the test did not run at
all and was reported as successful.
files:
A Misc/NEWS.d/next/Tests/2026-08-09-14-00-00.gh-issue-155411.Qw8Lm2.rst
M Lib/test/support/__init__.py
M Lib/test/test_support.py
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index c1460b806806f81..f98da49171dc3b5 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -1098,16 +1098,29 @@ def subTests(arg_names, arg_values, /, *,
_do_cleanups=False):
def decorator(func):
if isinstance(func, type):
raise TypeError('subTests() can only decorate methods, not
classes')
- @functools.wraps(func)
- def wrapper(self, /, *args, **kwargs):
+
+ def iter_subtest_kwargs():
for values in arg_values:
- if single_param:
- values = (values,)
- subtest_kwargs = dict(zip(arg_names, values))
- with self.subTest(**subtest_kwargs):
- func(self, *args, **kwargs, **subtest_kwargs)
- if _do_cleanups:
- self.doCleanups()
+ yield dict(zip(arg_names, (values,) if single_param else
values))
+
+ # A synchronous wrapper would discard the coroutine without awaiting
+ # it, so an asynchronous test would not run at all.
+ if inspect.iscoroutinefunction(func):
+ @functools.wraps(func)
+ async def wrapper(self, /, *args, **kwargs):
+ for subtest_kwargs in iter_subtest_kwargs():
+ with self.subTest(**subtest_kwargs):
+ await func(self, *args, **kwargs, **subtest_kwargs)
+ if _do_cleanups:
+ self.doCleanups()
+ else:
+ @functools.wraps(func)
+ def wrapper(self, /, *args, **kwargs):
+ for subtest_kwargs in iter_subtest_kwargs():
+ with self.subTest(**subtest_kwargs):
+ func(self, *args, **kwargs, **subtest_kwargs)
+ if _do_cleanups:
+ self.doCleanups()
return wrapper
return decorator
diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py
index 7c59bb38aaee9ae..84b91bb00cdbe4d 100644
--- a/Lib/test/test_support.py
+++ b/Lib/test/test_support.py
@@ -1247,5 +1247,65 @@ def test_skipped_without_subprocess_support(self):
self.assertEqual(calls, [])
+class TestSubTests(unittest.TestCase):
+
+ def run_test(self, cls):
+ result = unittest.TestResult()
+ cls('test_it').run(result)
+ return result
+
+ def test_sync(self):
+ ran = []
+
+ class Sample(unittest.TestCase):
+ @support.subTests('a', [1, 2, 3])
+ def test_it(self, a):
+ ran.append(a)
+ self.assertNotEqual(a, 2)
+
+ result = self.run_test(Sample)
+ self.assertEqual(ran, [1, 2, 3])
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.failures), 1)
+ self.assertEndsWith(result.failures[0][0].id(), 'test_it (a=2)')
+
+ # Running an asyncio event loop needs a working socket.
+ @support.requires_working_socket()
+ def test_async(self):
+ # An asynchronous test must be awaited: a synchronous wrapper would
+ # make it silently not run at all.
+ ran = []
+
+ class Sample(unittest.IsolatedAsyncioTestCase):
+ @support.subTests('a', [1, 2, 3])
+ async def test_it(self, a):
+ ran.append(a)
+ self.assertNotEqual(a, 2)
+
+ result = self.run_test(Sample)
+ self.assertEqual(ran, [1, 2, 3])
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.failures), 1)
+ self.assertEndsWith(result.failures[0][0].id(), 'test_it (a=2)')
+
+ def test_multiple_parameters(self):
+ ran = []
+
+ class Sample(unittest.TestCase):
+ @support.subTests('a,b', [(1, 'x'), (2, 'y')])
+ def test_it(self, a, b):
+ ran.append((a, b))
+
+ result = self.run_test(Sample)
+ self.assertTrue(result.wasSuccessful(), result.errors)
+ self.assertEqual(ran, [(1, 'x'), (2, 'y')])
+
+ def test_cannot_decorate_class(self):
+ with self.assertRaises(TypeError):
+ @support.subTests('a', [1])
+ class Sample(unittest.TestCase):
+ pass
+
+
if __name__ == '__main__':
unittest.main()
diff --git
a/Misc/NEWS.d/next/Tests/2026-08-09-14-00-00.gh-issue-155411.Qw8Lm2.rst
b/Misc/NEWS.d/next/Tests/2026-08-09-14-00-00.gh-issue-155411.Qw8Lm2.rst
new file mode 100644
index 000000000000000..50f684052526051
--- /dev/null
+++ b/Misc/NEWS.d/next/Tests/2026-08-09-14-00-00.gh-issue-155411.Qw8Lm2.rst
@@ -0,0 +1,3 @@
+Fix :func:`!test.support.subTests` for asynchronous test methods. They were
+wrapped in a synchronous function, which discarded the coroutine without
+awaiting it, so the test silently did not run at all.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]