Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes in Flask 2.2.0 causes difficulties with how flask_unittest's ClientTestCase prepares the 'app' field #7

Open
kirypto opened this issue Sep 4, 2022 · 3 comments
Labels
help wanted Extra attention is needed

Comments

@kirypto
Copy link

kirypto commented Sep 4, 2022

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.

@TotallyNotChase
Copy link
Owner

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.

@TotallyNotChase TotallyNotChase added the help wanted Extra attention is needed label Sep 5, 2022
@kirypto
Copy link
Author

kirypto commented Sep 6, 2022

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.

@srepollock
Copy link

Outstanding find @kirypto

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

3 participants