From c6e6559b50b221bc1d49943d656d40e332a869c7 Mon Sep 17 00:00:00 2001 From: "eric.goulart" Date: Sun, 10 Nov 2024 11:01:56 -0300 Subject: [PATCH 1/3] refactor(testing): deprecate testtools support in TestCase Remove conditional dependency on testtools in TestCase to simplify maintenance and prepare for Falcon 5.0 where testtools will no longer be supported. Instead, unittest is now the default, and users are encouraged to use pytest for testing Falcon applications. This change emits a deprecation warning if testtools is still enabled via environment variables. Closes #2156 --- falcon/testing/test_case.py | 65 +++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/falcon/testing/test_case.py b/falcon/testing/test_case.py index 632b662b7..a1b9696ce 100644 --- a/falcon/testing/test_case.py +++ b/falcon/testing/test_case.py @@ -18,15 +18,25 @@ utilities for simulating and validating HTTP requests. """ -try: - import testtools as unittest -except ImportError: # pragma: nocover - import unittest +import os +import unittest +import warnings + +if 'DISABLE_TESTTOOLS' not in os.environ: + try: + import testtools + + warnings.warn( + 'Support for testtools is deprecated and will be removed in Falcon 5.0. ' + 'Please migrate to unittest or pytest.', + DeprecationWarning, + ) + BaseTestCase = testtools.TestCase + except ImportError: # pragma: nocover + BaseTestCase = unittest.TestCase import falcon import falcon.request - -# TODO hoist for backwards compat. Remove in falcon 4. from falcon.testing.client import Result # NOQA from falcon.testing.client import TestClient @@ -35,8 +45,14 @@ class TestCase(unittest.TestCase, TestClient): """Extends :mod:`unittest` to support WSGI/ASGI functional testing. Note: - If available, uses :mod:`testtools` in lieu of - :mod:`unittest`. + This class uses :mod:`unittest` by default. If :mod:`testtools` + is available and the environment variable + ``DISABLE_TESTTOOLS`` is **not** set, it will use :mod:`testtools` instead. + **Support for testtools is deprecated and will be removed in Falcon 5.0.** + + Recommended: + We recommend using **pytest** for testing Falcon applications. + See our tutorial on using pytest. This base class provides some extra plumbing for unittest-style test cases, to help simulate WSGI or ASGI requests without having @@ -44,7 +60,30 @@ class TestCase(unittest.TestCase, TestClient): derived from :class:`falcon.testing.TestClient`. Simply inherit from this class in your test case classes instead of - :class:`unittest.TestCase` or :class:`testtools.TestCase`. + :class:`unittest.TestCase`. + + For example:: + + from falcon import testing + import myapp + + + class MyTestCase(testing.TestCase): + def setUp(self): + super(MyTestCase, self).setUp() + + # Assume the hypothetical `myapp` package has a + # function called `create()` to initialize and + # return a `falcon.App` instance. + self.app = myapp.create() + + + class TestMyApp(MyTestCase): + def test_get_message(self): + doc = {'message': 'Hello world!'} + + result = self.simulate_get('/messages/42') + self.assertEqual(result.json, doc) """ # NOTE(vytas): Here we have to restore __test__ to allow collecting tests! @@ -69,14 +108,6 @@ def setUp(self): # function called `create()` to initialize and # return a `falcon.App` instance. self.app = myapp.create() - - - class TestMyApp(MyTestCase): - def test_get_message(self): - doc = {'message': 'Hello world!'} - - result = self.simulate_get('/messages/42') - self.assertEqual(result.json, doc) """ def setUp(self) -> None: From 59ed39809b278ab3a98f76de2dea1c35df33e774 Mon Sep 17 00:00:00 2001 From: "eric.goulart" Date: Mon, 11 Nov 2024 08:37:31 -0300 Subject: [PATCH 2/3] implementing requested improvements still missing test coverage --- docs/_newsfragments/2156.breakingchange.rst | 3 ++ falcon/testing/test_case.py | 51 +++++++++++---------- 2 files changed, 30 insertions(+), 24 deletions(-) create mode 100644 docs/_newsfragments/2156.breakingchange.rst diff --git a/docs/_newsfragments/2156.breakingchange.rst b/docs/_newsfragments/2156.breakingchange.rst new file mode 100644 index 000000000..50aaa8d8f --- /dev/null +++ b/docs/_newsfragments/2156.breakingchange.rst @@ -0,0 +1,3 @@ +- **Deprecation**: Support for `testtools` in `falcon.testing` is now deprecated and will be removed in Falcon 5.0. + If `testtools` is selected automatically (i.e., without explicit configuration via `FALCON_BASE_TEST_CASE`), a deprecation warning will be issued. + Explicit configuration using `FALCON_BASE_TEST_CASE=testtools.TestCase` will not emit a warning. diff --git a/falcon/testing/test_case.py b/falcon/testing/test_case.py index a1b9696ce..856961406 100644 --- a/falcon/testing/test_case.py +++ b/falcon/testing/test_case.py @@ -22,52 +22,58 @@ import unittest import warnings -if 'DISABLE_TESTTOOLS' not in os.environ: +import falcon +import falcon.request +from falcon.testing.client import Result # NOQA +from falcon.testing.client import TestClient + +base_case = os.environ.get('FALCON_BASE_TEST_CASE') + +if base_case and 'testtools.TestCase' in base_case: + try: + import testtools + + BaseTestCase = testtools.TestCase + except ImportError: + BaseTestCase = unittest.TestCase +elif base_case is None: try: import testtools warnings.warn( - 'Support for testtools is deprecated and will be removed in Falcon 5.0. ' - 'Please migrate to unittest or pytest.', + 'Support for testtools is deprecated and will be removed in Falcon 5.0.', DeprecationWarning, ) BaseTestCase = testtools.TestCase - except ImportError: # pragma: nocover + except ImportError: BaseTestCase = unittest.TestCase - -import falcon -import falcon.request -from falcon.testing.client import Result # NOQA -from falcon.testing.client import TestClient +else: + BaseTestCase = unittest.TestCase class TestCase(unittest.TestCase, TestClient): - """Extends :mod:`unittest` to support WSGI/ASGI functional testing. + """Extends ``unittest`` to support WSGI/ASGI functional testing. Note: - This class uses :mod:`unittest` by default. If :mod:`testtools` - is available and the environment variable - ``DISABLE_TESTTOOLS`` is **not** set, it will use :mod:`testtools` instead. - **Support for testtools is deprecated and will be removed in Falcon 5.0.** + This class uses ``unittest`` by default, but you may use ``pytest`` + to run ``unittest.TestCase`` instances, allowing a hybrid approach. Recommended: - We recommend using **pytest** for testing Falcon applications. + We recommend using **pytest** alongside **unittest** for testing. See our tutorial on using pytest. - This base class provides some extra plumbing for unittest-style - test cases, to help simulate WSGI or ASGI requests without having - to spin up an actual web server. Various simulation methods are - derived from :class:`falcon.testing.TestClient`. + This base class provides extra functionality for unittest-style test cases, + helping simulate WSGI or ASGI requests without spinning up a web server. Various + simulation methods are derived from :class:`falcon.testing.TestClient`. Simply inherit from this class in your test case classes instead of - :class:`unittest.TestCase`. + ``unittest.TestCase``. For example:: from falcon import testing import myapp - class MyTestCase(testing.TestCase): def setUp(self): super(MyTestCase, self).setUp() @@ -77,7 +83,6 @@ def setUp(self): # return a `falcon.App` instance. self.app = myapp.create() - class TestMyApp(MyTestCase): def test_get_message(self): doc = {'message': 'Hello world!'} @@ -86,7 +91,6 @@ def test_get_message(self): self.assertEqual(result.json, doc) """ - # NOTE(vytas): Here we have to restore __test__ to allow collecting tests! __test__ = True app: falcon.App @@ -99,7 +103,6 @@ def test_get_message(self): from falcon import testing import myapp - class MyTestCase(testing.TestCase): def setUp(self): super(MyTestCase, self).setUp() From 8952f71a5a0c762040f7a25f07e5652f4bb1cec4 Mon Sep 17 00:00:00 2001 From: "eric.goulart" Date: Mon, 11 Nov 2024 15:37:10 -0300 Subject: [PATCH 3/3] updating changes, still missing test covarage --- ...kingchange.rst => 2156.newandimproved.rst} | 0 falcon/testing/test_case.py | 42 ++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) rename docs/_newsfragments/{2156.breakingchange.rst => 2156.newandimproved.rst} (100%) diff --git a/docs/_newsfragments/2156.breakingchange.rst b/docs/_newsfragments/2156.newandimproved.rst similarity index 100% rename from docs/_newsfragments/2156.breakingchange.rst rename to docs/_newsfragments/2156.newandimproved.rst diff --git a/falcon/testing/test_case.py b/falcon/testing/test_case.py index 856961406..b995c87e7 100644 --- a/falcon/testing/test_case.py +++ b/falcon/testing/test_case.py @@ -18,34 +18,34 @@ utilities for simulating and validating HTTP requests. """ +from importlib import import_module import os -import unittest import warnings +try: + import testtools as unittest +except ImportError: # pragma: nocover + import unittest + import falcon import falcon.request from falcon.testing.client import Result # NOQA from falcon.testing.client import TestClient -base_case = os.environ.get('FALCON_BASE_TEST_CASE') +# TODO hoist for backwards compat. Remove in falcon 5. -if base_case and 'testtools.TestCase' in base_case: - try: - import testtools +base_case_path = os.environ.get('FALCON_BASE_TEST_CASE') - BaseTestCase = testtools.TestCase - except ImportError: - BaseTestCase = unittest.TestCase -elif base_case is None: +if base_case_path: try: - import testtools - + module_path, class_name = base_case_path.rsplit('.', 1) + module = import_module(module_path) + BaseTestCase = getattr(module, class_name) + except (ImportError, AttributeError): warnings.warn( - 'Support for testtools is deprecated and will be removed in Falcon 5.0.', - DeprecationWarning, + f"Could not import '{base_case_path}', defaulting to testtools.TestCase.", + ImportWarning, ) - BaseTestCase = testtools.TestCase - except ImportError: BaseTestCase = unittest.TestCase else: BaseTestCase = unittest.TestCase @@ -91,6 +91,7 @@ def test_get_message(self): self.assertEqual(result.json, doc) """ + # NOTE(vytas): Here we have to restore __test__ to allow collecting tests! __test__ = True app: falcon.App @@ -116,6 +117,17 @@ def setUp(self): def setUp(self) -> None: super(TestCase, self).setUp() + if ( + BaseTestCase == unittest.TestCase + and os.environ.get('FALCON_BASE_TEST_CASE') is None + and 'testtools' in str(unittest) + ): + warnings.warn( + 'Support for testtools is deprecated' + 'and will be removed in Falcon 5.0.', + DeprecationWarning, + ) + app = falcon.App() # NOTE(kgriffs): Don't use super() to avoid triggering