Search code examples
pythonflaskpytestflask-restful

pytest `AssertionError: View function mapping is overwriting an existing endpoint function:` flask-restful while registring blueprint


The problem is as follow, I created a dummy example. Where the folder structure is:

.
├── api_bp
│   └── __init__.py
├── app.py
├── pytest.ini
└── tests
    ├── conftest.py
    ├── __init__.py
    ├── test_todo1.py
    └── test_todo2.py

Code in the folder api_bp inside __init__.py:

# __init__.py

from flask import Blueprint

api_bp = Blueprint('api', __name__)

Flask app:

# app.py

from flask import Flask, Blueprint
from flask_restful import Api, Resource


class TodoItem(Resource):
    def get(self, id):
        return {'task': 'Say "Hello, World!"'}


def create_app():
    """Initialize the app. """
    app = Flask(__name__)
    from api_bp import api_bp
    # api_bp = Blueprint('api', __name__)
    api = Api(api_bp)

    api.add_resource(TodoItem, '/todos/<int:id>')
    app.register_blueprint(api_bp, url_prefix='/api')

    return app


if __name__ == '__main__':
    app = create_app()
    app.run(debug=True)

For the testing purposes I have the client fixture and two tests (which I intentionally put into separate modules) for different todos:

# conftest.py

import pytest

from app_factory import create_app

@pytest.fixture(scope='module')
def client():
    flask_app = create_app()

    testing_client = flask_app.test_client()

    context = flask_app.app_context()
    context.push()

    yield testing_client

    context.pop()
# test_todo1.py

import pytest

def test_todo2(client):
    """Test"""
    response = client.get('/api/todos/1')
    print(response)
    assert response.status_code == 200
# test_todo2.py

import pytest

def test_todo2(client):
    """Test"""
    response = client.get('/api/todos/2')
    print(response)
    assert response.status_code == 200

So when I run $ pytest -v to test it, I end up with the following error:

AssertionError: View function mapping is overwriting an existing endpoint function: api.todoitem

That's happens because of registring a blueprint. And I wanted to understand the magic that happens under the hood of flask (flask-restful) combining with pytest. Because if I were to define my app.py module like this, it successfully passes the tests:

# app.py

from flask import Flask, Blueprint
from flask_restful import Api, Resource


class TodoItem(Resource):
    def get(self, id):
        return {'task': 'Say "Hello, World!"'}


def create_app():
    """Initialize the app. """
    app = Flask(__name__)
    # note: I commented the line below and defined the blueprint in-place
    # from api_bp import api_bp  
    api_bp = Blueprint('api', __name__)
    api = Api(api_bp)

    api.add_resource(TodoItem, '/todos/<int:id>')
    app.register_blueprint(api_bp, url_prefix='/api')

    return app


if __name__ == '__main__':
    app = create_app()
    app.run(debug=True)
$ pytest -v
tests/test_api1.py::test_todo2 PASSED    [ 50%]
tests/test_api2.py::test_todo2 PASSED    [100%]

Or if I used not the app factory it also works fine:

# app.py

from flask import Flask, Blueprint
from flask_restful import Api, Resource

app = Flask(__name__)
api_bp = Blueprint('api', __name__)
api = Api(api_bp)


class TodoItem(Resource):
    def get(self, id):
        return {'task': 'Say "Hello, World!"'}


api.add_resource(TodoItem, '/todos/<int:id>')
app.register_blueprint(api_bp, url_prefix='/api')

Also it can be fixed if I put all my tests inside one module, or if I registered the blueprint first and then added resources like this:

# app.py

...

def create_app():
    """Initialize the app. """
    app = Flask(__name__)

    from api_bp import api_bp

    api = Api(api_bp)

    app.register_blueprint(api_bp, url_prefix='/api')
    api.add_resource(TodoItem, '/todos/<int:id>')


    return app

...

Who knows what happened here and can explain magic? Thanks in advance.


Solution

  • So the explainig for the problem is that when pytest setup and uses client in tests it runs create_app() and tries to reuse a Blueprint when not defining Blueprint inside app.py:

    tests/test_api1.py::test_todo2 <flask.blueprints.Blueprint object at 0x7f04a8c9c610>
    
        SETUP    M client
            tests/test_api1.py::test_todo2 (fixtures used: client)<Response streamed [200 OK]>
    PASSED
        TEARDOWN M client
    tests/test_api2.py::test_todo2 <flask.blueprints.Blueprint object at 0x7f04a8c9c610>
    
        SETUP    M clientERROR
        TEARDOWN M client
    

    It can be fixed by doing this:

    # api_bp/__init__.py
    
    from flask import Blueprint
    
    
    get_blueprint = lambda: Blueprint('api', __name__)
    

    And using:

    def create_app():
        """Initialize the app. """
        app = Flask(__name__)
        from api_bp import get_blueprint
    
        api_bp = get_blueprint()
        api = Api(api_bp)
    
        api.add_resource(TodoItem, '/todos/<int:id>')
        app.register_blueprint(api_bp, url_prefix='/api')
    
        return app
    

    So the simplest solution for such problem would be to use proper pytest scope (not 'module'):

    @pytest.fixture(scope='session')
    def client():
    ...
    

    Update:

    This approach won't work with defining a management command like:

    class Test(Command):
        def run(self):
            """Runs the tests."""
            pytest.main(['-s', '-v', './tests'])
    
    
    manager.add_command('test', Test)  # run the tests
    

    Using python app.py test you'll get the same error as in previous examples. For more details read 'Note:' section in the following link: https://docs.pytest.org/en/latest/usage.html#calling-pytest-from-python-code