Generate URLs for Flask test client with url_for function - python

I'm trying to write unit tests for a Flask app using pytest. I have an app factory:
def create_app():
from flask import Flask
app = Flask(__name__)
app.config.from_object('config')
import os
app.secret_key = os.urandom(24)
from models import db
db.init_app(app)
return app
And a test class:
class TestViews(object):
#classmethod
def setup_class(cls):
cls.app = create_app()
cls.app.testing = True
cls.client = cls.app.test_client()
#classmethod
def teardown_class(cls):
cls.app_context.pop()
def test_create_user(self):
"""
Tests the creation of a new user.
"""
view = TestViews.client.get(url_for('create_users')).status_code == 200
but when I run my tests I get the following error:
RuntimeError: Attempted to generate a URL without the application context being pushed. This has to be executed when application context is available.
Googling this tells me (I think) that using the test client should create an automatic application context. What am I missing?

Making requests with the test client does indeed push an app context (indirectly). However, you're confusing the fact that url_for is visually inside the test request call with the idea that it is actually called inside. The url_for call is evaluated first, the result is passed to client.get.
url_for is typically for generating URLs within the app, unit tests are external. Typically, you just write exactly the URL you're trying to test in the request instead of generating it.
self.client.get('/users/create')
If you really want to use url_for here, you must do it in an app context. Note that when you're in an app context but not a request context, you must set the SERVER_NAME config and also pass _external=False. But again, you should probably just write out the URL you're trying to test.
app.config['SERVER_NAME'] = 'localhost'
with self.app.app_context():
url = url_for(..., _external=False)
self.client.get(url, ...)

You can call url_for() in test request context that created with app.test_request_context() method. There are three methods to achieve this.
With setup and teardown
Since you have created the setup and teardown method, just like what I normally do with unittest, you can just push a test request context in setup method then pop it in teardown method:
class TestViews(object):
#classmethod
def setup_class(cls):
cls.app = create_app()
cls.app.testing = True
cls.client = cls.app.test_client()
cls.context = cls.app.test_request_context() # create the context object
cls.context.push() # push the context
#classmethod
def teardown_class(cls):
cls.context.pop() # pop the context
def test_create_user(self):
"""
Tests the creation of a new user.
"""
view = TestViews.client.get(url_for('create_users')).status_code == 200
With pytest-flask
Besides, you can also just use pytest-flask. With pytest-flask, you can access to context bound objects (url_for, request, session) without context managers:
def test_app(client):
assert client.get(url_for('myview')).status_code == 200
With autouse fixture
If you don't want to install the plugin, you can just use the following fixtures to do similar things (stolen from the source of pytest-flask):
#pytest.fixture
def app():
app = create_app('testing')
return app
#pytest.fixture(autouse=True)
def _push_request_context(request, app):
ctx = app.test_request_context() # create context
ctx.push() # push
def teardown():
ctx.pop() # pop
request.addfinalizer(teardown) # set teardown

Related

Why does my 'Flask' object have no attribute 'get' [duplicate]

I am getting working outside of request context when trying to access session in a test. How can I set up a context when I'm testing something that requires one?
import unittest
from flask import Flask, session
app = Flask(__name__)
#app.route('/')
def hello_world():
t = Test()
hello = t.hello()
return hello
class Test:
def hello(self):
session['h'] = 'hello'
return session['h']
class MyUnitTest(unittest.TestCase):
def test_unit(self):
t = tests.Test()
t.hello()
If you want to make a request to your application, use the test_client.
c = app.test_client()
response = c.get('/test/url')
# test response
If you want to test code which uses an application context (current_app, g, url_for), push an app_context.
with app.app_context():
# test your app context code
If you want test code which uses a request context (request, session), push a test_request_context.
with current_app.test_request_context():
# test your request context code
Both app and request contexts can also be pushed manually, which is useful when using the interpreter.
>>> ctx = app.app_context()
>>> ctx.push()
Flask-Script or the new Flask cli will automatically push an app context when running the shell command.
Flask-Testing is a useful library that contains helpers for testing Flask apps.

Flask click command unittests - how to use testing app with "with_appcontext" decorator?

I don't know how to correctly use testing app version while unittesting (with pytest) flask cli command (with click) decorated with with_app_context decorator. This decorator replaces pytest fixture app with the "normal", development application. I use app factory pattern.
My command in a simplified version looks like this (mind #with_appcontext):
#click.command()
#click.option('--username', prompt='Username')
#click.option('--email', prompt='User E-Mail')
#click.option('--password', prompt='Password', confirmation_prompt=True, hide_input=True)
#with_appcontext # from flask.cli import with_appcontext
def createsuperuser(username, email, password):
user = User(
username=username,
email=email,
password=password,
active=True,
is_admin=True,
)
user.save()
Without #with_appcontext unittests work just fine (they get the app injected by pytest), but the command itself does not, as it needs an app context.
My extracted pytest code:
# pytest fixtures
#pytest.yield_fixture(scope='function')
def app():
"""An application for the tests."""
_app = create_app(TestConfig)
ctx = _app.test_request_context()
ctx.push()
yield _app
ctx.pop()
#pytest.yield_fixture(scope='function')
def db(app):
"""A database for the tests."""
_db.app = app
with app.app_context():
_db.create_all()
yield _db
# Explicitly close DB connection
_db.session.close()
_db.drop_all()
#pytest.mark.usefixtures('db')
class TestCreateSuperUser:
# db fixture uses app fixture, works the same if app was injected here as well
def test_user_is_created(self, cli_runner, db):
result = cli_runner.invoke(
createsuperuser,
input='johnytheuser\nemail#email.com\nsecretpass\nsecretpass'
)
assert result.exit_code == 0
assert 'SUCCESS' in result.output
# etc.
All my tests using app and db fixtures work just fine apart from these decorated ones. I'm not sure how I should workaround this with_appcontext decorator that sets the app itself.
Thank you in advance for any hint.
Inspiration taken from https://github.com/pallets/flask/blob/master/tests/test_cli.py#L254.
from flask.cli import ScriptInfo
#pytest.fixture
def script_info(app):
return ScriptInfo(create_app=lambda info: app)
In your test:
def test_user_is_created(self, cli_runner, db, script_info):
result = cli_runner.invoke(
createsuperuser,
input='johnytheuser\nemail#email.com\nsecretpass\nsecretpass',
obj=obj,
)
The accepted answer helped me figure out a solution, but did not work out of the box. Here is what worked for me in case anyone else has a similar issue
#pytest.fixture(scope='session')
def script_info(app):
return ScriptInfo(create_app=lambda: app)
def test_user_is_created(self, cli_runner, db, script_info):
result = cli_runner.invoke(
createsuperuser,
input='johnytheuser\nemail#email.com\nsecretpass\nsecretpass',
obj=script_info,
)
I had the same problem testing one of my flask commands. Although your approach works, I think it is valuable to have a look at the flask documentation here:
https://flask.palletsprojects.com/en/1.1.x/testing/#testing-cli-commands
Flask has its own test runner for cli commands that probably has a fix to our problem builtin. So instead of patching the create_app function with a lambda you could also just use app.test_cli_runner() and it works out of the box.

Flask test setup with Flask-Babel

I'd like to setUp with unittest module.
My Flask App is created using factory (create_app) uses Flask-Babel for i18n/
def create_app(config=None, app_name=None, blueprints=None):
# Create Flask App instance
app_name = app_name or __name__
app = Flask(app_name)
app.config.from_pyfile(config)
configure_hook(app)
configure_blueprints(app, blueprints)
configure_extensions(app)
configure_jinja_filters(app)
configure_logging(app)
configure_error_handlers(app)
configure_cli(app)
return app
create_app function calls configure_extensions(app) which is as follows:
def configure_extensions(app):
"""Initialize Flask Extensions."""
db.init_app(app)
babel.init_app(app)
csrf.init_app(app)
#babel.localeselector
def get_locale():
# If logged in, load user locale settings.
user = getattr(g, 'user', None)
if user is not None:
return user.locale
# Otherwise, choose the language from user browser.
return request.accept_languages.best_match(
app.config['BABEL_LANGUAGES'].keys())
#babel.timezoneselector
def get_timezone():
user = getattr(g, 'user', None)
if user is not None:
return user.timezone
It works fine when I run app, but I can't create a unittest properly because it asserts error like this:
File "C:\projects\rabiang\venv\lib\site-packages\flask_babel\__init__.py", line 127, in localeselector
'a localeselector function is already registered'
AssertionError: a localeselector function is already registered
Due to the message "a localeselector function is already registered", I thought that fact that my setUp method of unittest was invoked when each test method is called makes problem. Thus, I changed #classmethod setUpClass like this:
# -*- coding: utf-8 -*-
import unittest
from app import create_app, db
from app.blueprints.auth import auth
from app.blueprints.forum import forum
from app.blueprints.main import main
from app.blueprints.page import page
class BasicsTestCase(unittest.TestCase):
#classmethod
def setUpClass(cls):
blueprints = [main, page, auth, forum]
app = create_app(config='../test.cfg', blueprints=blueprints)
cls.app = app.test_client()
db.create_all()
#classmethod
def tearDownClass(cls):
db.session.remove()
db.drop_all()
def test_app_exists(self):
self.assertFalse(BasicsTestCase.app is None)
if __name__ == '__main__':
unittest.main()
However, #babel.localeselector and #babel.timezoneselector decorator doesn't work.
I fixed it by setting the app only once with the function setUpClass from unittest.
See also the answer Run setUp only once

Access Flask config outside of application factory

I'm currently using the Flask Application Factory pattern with Blueprints. The issue that I'm having is how do I access the app.config object outside of the application factory?
I don't need all the configuration options from the Flask app. I just need 6 keys. So the current way I do this is when the create_app(application factory) is called, I basically create a global_config dictionary object and I just set the global_config dictionary to have the 6 keys that I need.
Then, the other modules that need those configuration options, they just import global_config dictionary.
I'm thinking, there has to be a better way to do this right?
So, on to the code
My current init.py file:
def set_global_config(app_config):
global_config['CUPS_SAFETY'] = app_config['CUPS_SAFETY']
global_config['CUPS_SERVERS'] = app_config['CUPS_SERVERS']
global_config['API_SAFE_MODE'] = app_config['API_SAFE_MODE']
global_config['XSS_SAFETY'] = app_config['XSS_SAFETY']
global_config['ALLOWED_HOSTS'] = app_config['ALLOWED_HOSTS']
global_config['SQLALCHEMY_DATABASE_URI'] = app_config['SQLALCHEMY_DATABASE_URI']
def create_app(config_file):
app = Flask(__name__, instance_relative_config=True)
try:
app.config.from_pyfile(config_file)
except IOError:
app.config.from_pyfile('default.py')
cel.conf.update(app.config)
set_global_config(app.config)
else:
cel.conf.update(app.config)
set_global_config(app.config)
CORS(app, resources=r'/*')
Compress(app)
# Initialize app with SQLAlchemy
db.init_app(app)
with app.app_context():
db.Model.metadata.reflect(db.engine)
db.create_all()
from authenication.auth import auth
from club.view import club
from tms.view import tms
from reports.view import reports
from conveyor.view import conveyor
# Register blueprints
app.register_blueprint(auth)
app.register_blueprint(club)
app.register_blueprint(tms)
app.register_blueprint(reports)
app.register_blueprint(conveyor)
return app
An example of a module that needs access to those global_config options:
from package import global_config as config
club = Blueprint('club', __name__)
#club.route('/get_printers', methods=['GET', 'POST'])
def getListOfPrinters():
dict = {}
for eachPrinter in config['CUPS_SERVERS']:
dict[eachPrinter] = {
'code': eachPrinter,
'name': eachPrinter
}
outDict = {'printers': dict, 'success': True}
return jsonify(outDict)
There has to be a better way then passing a global dictionary around the application correct?
There is no need to use global names here, that defeats the purpose of using an app factory in the first place.
Within views, such as in your example, current_app is bound to the app handling the current app/request context.
from flask import current_app
#bp.route('/')
def example():
servers = current_app.config['CUPS_SERVERS']
...
If you need access to the app while setting up a blueprint, the record decorator marks functions that are called with the state the blueprint is being registered with.
#bp.record
def setup(state):
servers = state.app.config['CUPS_SERVERS']
...

How can I test a custom Flask error page?

I'm attempting to test a custom error page in flask (404 in this case).
I've defined my custom 404 page as such:
#app.errorhandler(404)
def page_not_found(e):
print "Custom 404!"
return render_template('404.html'), 404
This works perfectly when hitting an unknown page in a browser (I see Custom 404! in stdout and my custom content is visible). However, when trying to trigger a 404 via unittest with nose, the standard/server 404 page renders. I get no log message or the custom content I am trying to test for.
My test case is defined like so:
class MyTestCase(TestCase):
def setUp(self):
self.app = create_app()
self.app_context = self.app.app_context()
self.app.config.from_object('config.TestConfiguration')
self.app.debug = False # being explicit to debug what's going on...
self.app_context.push()
self.client = self.app.test_client()
def tearDown(self):
self.app_context.pop()
def test_custom_404(self):
path = '/non_existent_endpoint'
response = self.client.get(path)
self.assertEqual(response.status_code, 404)
self.assertIn(path, response.data)
I have app.debug explicitly set to False on my test app. Is there something else I have to explicitly set?
After revisiting this with fresh eyes, it's obvious that the problem is in my initialization of the application and not in my test/configuration. My app's __init__.py basically looks like this:
def create_app():
app = Flask(__name__)
app.config.from_object('config.BaseConfiguration')
app.secret_key = app.config.get('SECRET_KEY')
app.register_blueprint(main.bp)
return app
app = create_app()
# Custom error pages
#app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
Notice that the error handler is attached to #app outside of create_app(), the method I'm calling in my TestCase.setUp() method.
If I simply move that error handler up into the create_app() method, everything works fine... but it feels a bit gross? Maybe?
def create_app():
app = Flask(__name__)
app.config.from_object('config.BaseConfiguration')
app.secret_key = app.config.get('SECRET_KEY')
app.register_blueprint(main.bp)
# Custom error pages
#app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
return app
This ultimately answers my question and fixes my problem, but I'd love other thoughts on how to differently register those errorhandlers.
The Flask application object has an error_handler_spec property that can be mocked to solve this:
A dictionary of all registered error handlers. The key is None for
error handlers active on the application, otherwise the key is the
name of the blueprint. Each key points to another dictionary where the
key is the status code of the http exception. The special key None
points to a list of tuples where the first item is the class for the
instance check and the second the error handler function.
So something like this in your test method should work:
mock_page_not_found = mock.magicMock()
mock_page_not_found.return_value = {}, 404
with mock.patch.dict(self.app.error_handler_spec[None], {404: mock_page_not_found}):
path = '/non_existent_endpoint'
response = self.client.get(path)
self.assertEqual(response.status_code, 404)
mock_page_not_found.assert_called_once()
In reference to the following comment you made "If I simply move that error handler up into the create_app() method, everything works fine... but it feels a bit gross? Maybe?":
You can define a function to register an error handler and call that in your create_app function:
def create_app():
app = Flask(__name__)
app.config.from_object('config.BaseConfiguration')
app.secret_key = app.config.get('SECRET_KEY')
app.register_blueprint(main.bp)
register_error_pages(app)
return app
app = create_app()
# Custom error pages
def register_error_pages(app):
#app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
That way if you have more custom error handlers you want to register (403, 405, 500) you can define them inside the register_error_pages function instead of your create_app function.

Categories

Resources