diff --git a/docs/_newsfragments/2156.newandimproved.rst b/docs/_newsfragments/2156.newandimproved.rst new file mode 100644 index 000000000..50aaa8d8f --- /dev/null +++ b/docs/_newsfragments/2156.newandimproved.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 632b662b7..b995c87e7 100644 --- a/falcon/testing/test_case.py +++ b/falcon/testing/test_case.py @@ -18,6 +18,10 @@ utilities for simulating and validating HTTP requests. """ +from importlib import import_module +import os +import warnings + try: import testtools as unittest except ImportError: # pragma: nocover @@ -25,26 +29,66 @@ 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 +# TODO hoist for backwards compat. Remove in falcon 5. + +base_case_path = os.environ.get('FALCON_BASE_TEST_CASE') + +if base_case_path: + try: + module_path, class_name = base_case_path.rsplit('.', 1) + module = import_module(module_path) + BaseTestCase = getattr(module, class_name) + except (ImportError, AttributeError): + warnings.warn( + f"Could not import '{base_case_path}', defaulting to testtools.TestCase.", + ImportWarning, + ) + BaseTestCase = unittest.TestCase +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: - If available, uses :mod:`testtools` in lieu of - :mod:`unittest`. + This class uses ``unittest`` by default, but you may use ``pytest`` + to run ``unittest.TestCase`` instances, allowing a hybrid approach. - 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`. + Recommended: + We recommend using **pytest** alongside **unittest** for testing. + See our tutorial on using pytest. + + 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` or :class:`testtools.TestCase`. + ``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! @@ -60,7 +104,6 @@ class TestCase(unittest.TestCase, TestClient): from falcon import testing import myapp - class MyTestCase(testing.TestCase): def setUp(self): super(MyTestCase, self).setUp() @@ -69,19 +112,22 @@ 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: 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