flask-unittest
flask-unittest copied to clipboard
Changes in Flask 2.2.0 causes difficulties with how flask_unittest's ClientTestCase prepares the 'app' field
I updated flask to 2.2.0 recently and had issues with previously passing tests start failing. I ended up determining the root cause to be the _got_first_request
field on the Flask
class, which I believe was added in that version. The field is initialized to False
, and then when the first request is made against the application, the field is changed to True
. From then on, any attempts to call add_url_rule
(registering a handler) throws an exception, saying that it can't be called after the app has received it's first request.
This change did not cause me any issues in my production code as all the routes are registered at the beginning prior to the app receiving any requests.
However, one of my test classes started failing because of how ClientTestCase
initializes and reuses the app
field. Because it is expected to be initialized once as a class field and is the used for all the test cases, any tests that register routes as part of the 'setup' and the invoke a request cannot be run together. The first test (alphabetically, as this is the order unittest runs them) will properly run and pass. The second will throw an exception and fail when the add_url_rule
is called as the first test already invoked a request.
I was able to find a workaround. In the setup
method, I 'reset' this boolean with self.app._got_first_request = False
. This works and passes tests as it did in earlier versions, but is not a satisfying fix because it requires manually setting a 'private' field. I could not come up with any other ways to achieve this 'reset' though because of the way the ClientTestCase
reuses the app
for all the tests... If there are any ideas I would appreciate them. The best idea I have is to use this workaround now and make some change to flask_unittest
to handle this, perhaps by spinning up a new flask app for each test or finding some way of resetting the registered routes.
Assuming it would be helpful, I put together a trivialized version of the situation I am experiencing:
from typing import Callable, List
from flask import Flask
from flask.testing import FlaskClient
from flask_unittest import ClientTestCase
def _create_flask_app() -> Flask:
flask_web_app = Flask(__name__)
@flask_web_app.route("/")
def root_handler():
return "<div>Hello, World!</div>"
return flask_web_app
class RestController:
_flask_app: Flask
def __init__(self, flask_app: Flask) -> None:
self._flask_app = flask_app
def register_route(self, route: str, methods: List[str], handler: Callable) -> None:
self._flask_app.add_url_rule(route, route, handler, methods=methods)
def _main():
flask_app = _create_flask_app()
controller = RestController(flask_app)
def route_handler():
return "<div>Hello, Handler!</div>"
controller.register_route("/some/route", ["GET"], route_handler)
flask_app.run("localhost", 8080)
if __name__ == "__main__":
_main()
class TestFlaskApp(ClientTestCase):
app = _create_flask_app()
_client: FlaskClient
_rest_controller: RestController
def setUp(self, client: FlaskClient) -> None:
self._client = client
self._rest_controller = RestController(self.app)
# self.app._got_first_request = False # This line allows tests to pass
def test__example_test_1(self, *_) -> None:
# Arrange
# Act
actual = self._client.open("/", method="GET")
# Assert
self.assertEqual(actual.status_code, 200)
def test__example_test_2(self, *_) -> None:
# Arrange
def test_handler_1():
return "<div>Test Handler 1</div>"
self._rest_controller.register_route("/test/route/1", ["GET"], test_handler_1)
# Act
actual = self._client.open("/test/route/1", method="GET")
# Assert
self.assertEqual(actual.data, b"<div>Test Handler 1</div>")
This simple program can be run normally and will create a flask application that handles requests on localhost, or tests can be run by running unittest and specifying the script as the target. Note that both the main
and the tests are in the same file, this was fine for me. When I ran this with flask 2.1.3
(the precursor), the tests passed. Running it with 2.2.0
causes the second test to fail.
Sorry for the long issue, but I hope the description and details makes reproducing/addressing it simple.
I agree, a flask 2.0 compatibility update in general is overdue. Unfortunately, I don't have meaningful bandwidth to dedicate here as I have mostly moved on to other projects.
If someone is able to PR such an update, I'd gladly review and merge it. If not, I'll try to look into this over the weekend - but I can't promise anything at this stage.
I'd be willing to take a crack at it, I think I have a general idea of the usage/intention as well as knowing the problem itself.
I would be interested in knowing what other the other computability upgrades you have in mind though.
Outstanding find @kirypto