Repository: marshmallow-code/webargs

Issue:
Testsuite fails under pytest==8.2.0 with `'AsyncHTTPTestCase' has no attribute 'runTest'`
We currently have some test failures in basically all python versions, starting ~2 days ago. At first glance, it looks like an issue with `tornado`'s `AsyncHTTPTestCase`, but `tornado` doesn't have a recent release.

Looking at what projects updated recently, I flagged `pytest` as a good candidate for investigation, and testing with `pytest=8.1.2` works fine. So something related to unittest TestCases changed in 8.2.0 in a way that breaks tornado tests.

For reference, here's one of the error traces:
```
____________________ ERROR collecting tests/test_tornadoparser.py ____________________
.tox/py311/lib/python3.11/site-packages/tornado/testing.py:180: in __init__
    setattr(self, methodName, _TestMethodWrapper(getattr(self, methodName)))
E   AttributeError: 'AsyncHTTPTestCase' object has no attribute 'runTest'
____________________ ERROR collecting tests/test_tornadoparser.py ____________________
.tox/py311/lib/python3.11/site-packages/tornado/testing.py:180: in __init__
    setattr(self, methodName, _TestMethodWrapper(getattr(self, methodName)))
E   AttributeError: 'TestApp' object has no attribute 'runTest'
____________________ ERROR collecting tests/test_tornadoparser.py ____________________
.tox/py311/lib/python3.11/site-packages/tornado/testing.py:180: in __init__
    setattr(self, methodName, _TestMethodWrapper(getattr(self, methodName)))
E   AttributeError: 'TestValidateApp' object has no attribute 'runTest'
```

`runTest` looks like a dynamically created method in unittest, whose name gets passed on init to TestCase.
I started looking at changes to `pytest`'s unittest module ( https://github.com/pytest-dev/pytest/blame/main/src/_pytest/unittest.py ), but I'm out of time for digging into this right now.


Discussion:

Resolving patch:
diff --git a/src/webargs/__init__.py b/src/webargs/__init__.py
--- a/src/webargs/__init__.py
+++ b/src/webargs/__init__.py
@@ -14,8 +14,8 @@
 __version__ = importlib.metadata.version("webargs")
 __parsed_version__ = Version(__version__)
 __version_info__: tuple[int, int, int] | tuple[int, int, int, str, int] = (
-    __parsed_version__.release
-)  # type: ignore[assignment]
+    __parsed_version__.release  # type: ignore[assignment]
+)
 if __parsed_version__.pre:
     __version_info__ += __parsed_version__.pre  # type: ignore[assignment]
 __all__ = ("ValidationError", "fields", "missing", "validate")


Test patch:
diff --git a/tests/test_tornadoparser.py b/tests/test_tornadoparser.py
--- a/tests/test_tornadoparser.py
+++ b/tests/test_tornadoparser.py
@@ -8,8 +8,8 @@
 import tornado.httpserver
 import tornado.httputil
 import tornado.ioloop
+import tornado.testing
 import tornado.web
-from tornado.testing import AsyncHTTPTestCase
 
 from webargs import fields, missing
 from webargs.core import json, parse_json
@@ -20,6 +20,22 @@
     use_kwargs,
 )
 
+
+class BaseAsyncTestCase(tornado.testing.AsyncHTTPTestCase):
+    # this isn't a real test case itself
+    __test__ = False
+
+    # Workaround for https://github.com/pytest-dev/pytest/issues/12263.
+    #
+    # this was suggested by one of the pytest maintainers while a patch
+    # for Tornado is pending
+    #
+    # we may need it even after the patch, since we want to support testing on
+    # older Tornado versions until we drop support for them
+    def runTest(self):
+        pass
+
+
 name = "name"
 value = "value"
 
@@ -460,7 +476,7 @@ def get(self, id, args):
 )
 
 
-class TestApp(AsyncHTTPTestCase):
+class TestApp(BaseAsyncTestCase):
     def get_app(self):
         return echo_app
 
@@ -528,7 +544,7 @@ def post(self, args):
 )
 
 
-class TestValidateApp(AsyncHTTPTestCase):
+class TestValidateApp(BaseAsyncTestCase):
     def get_app(self):
         return validate_app