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.
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