Как настроить приложение Flask с SQLAlchemy для тестирования?
Кажется, что обычная практика в Flask начинается следующим образом:
from flask import Flask
from flaskext.sqlalchemy import SQLAlchemy
app = Flask(__name__)
SQLALCHEMY_DATABASE_URI = 'something'
app.config.from_object(__name__)
db = SQLAlchemy(app)
И затем импортируйте и используйте app
и db
всюду. Но когда вы создаете db
, как это, он захватывает конфигурацию из приложения, и кажется, что эта конфигурация никогда не может быть переопределена после ее возникновения. Есть несколько страниц на веб-сайте Flask о создании фабрик приложений, но неясно, как я мог бы использовать app
и db
всюду, если бы я это сделал.
Как написать script для проверки моего приложения Flask с другой базой данных? Как я должен структурировать мое приложение, чтобы сделать это возможным? Должен ли я использовать module
?
Ответы
Ответ 1
Ваш инстинкт использования переменных среды верен. Тем не менее, существует некоторая опасность запуска модульных тестов с неправильным db. Кроме того, вы можете не захотеть connect_db
с каждым запросом и везде, где хотите использовать db
. Вы можете использовать каталог конфигурации и переменные среды, которые вы задаете явно. Это лучшее, что я придумал до сих пор.
run.py
shell.py
config/__init__.py
config/test.py
config/postgres.py
...
main/__init__.py
main/someapp/__init__.py
main/someapp/models.py
...
main/tests/__init__.py
main/tests/testutils.py
поэтому файлы конфигурации могут быть:
# config/test.py
SQLALCHEMY_DATABASE_URI = "sqlite://"
и
# config/postgres.py
SQLALCHEMY_DATABASE_URI = 'postgresql://user:[email protected]/somedb'
Итак, я могу явно установить db в моей базовой TestCase:
import os
from flask.ext.testing import TestCase
os.environ["DIAG_CONFIG_MODULE"] = "config.test"
from main import app, db
class SQLAlchemyTest(TestCase):
def create_app(self):
return app
def setUp(self):
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
Тогда main/__init__.py
, для меня:
import os
from flask import Flask, render_template, g
from flask.ext.sqlalchemy import SQLAlchemy
# by default, let use a DB we don't care about
# but, we can override if we want
config_obj = os.environ.get("DIAG_CONFIG_MODULE", "config.test")
app = Flask(__name__)
app.config.from_object(config_obj)
db = SQLAlchemy(app)
@app.before_request
def before_request():
g.db = db
g.app = app
# ...
@app.route('/', methods=['GET'])
def get():
return render_template('home.html')
# ...
from main.someapp.api import mod as someappmod
app.register_blueprint(someappmod)
Затем в других файлах, где я знаю, какую конфигурацию я хочу запустить, потенциально:
# run.py
import os
os.environ["DIAG_CONFIG_MODULE"] = "config.postgres"
from main import app
app.run(debug=True)
и
# shell.py
import os
os.environ["DIAG_CONFIG_MODULE"] = "config.postgres"
from main import app, db
from main.symdiag.models import *
from main.auth.models import *
print sorted(k for k in locals().keys() if not k.startswith("_"))
import IPython
IPython.embed()
Может быть, лучше всего до сих пор: P.
Ответ 2
Вы не захотите, чтобы подключение к db происходило во время импорта. Идите и настройте свое приложение во время импорта, потому что вы всегда можете настроить конфигурацию в своих тестах, прежде чем пытаться протестировать или запустить приложение. В приведенном ниже примере у вас будет ваше соединение с db за некоторыми функциями, которые используют конфигурацию приложения, поэтому в unittest вы можете фактически изменить соединение db, чтобы указать на другой файл, а затем продолжить и явно подключиться к вашей настройке.
Скажем, у вас есть пакет myapp, содержащий myapp.py, который выглядит так:
# myapp/myapp.py
from __future__ import with_statement
from sqlite3 import dbapi2 as sqlite3
from contextlib import closing
from flask import Flask, request, session, g, redirect, url_for, abort, \
render_template, flash
# configuration
DATABASE = '/tmp/flaskr.db'
DEBUG = True
SECRET_KEY = 'development key'
USERNAME = 'admin'
PASSWORD = 'default'
# create our little application :)
app = Flask(__name__)
app.config.from_object(__name__)
app.config.from_envvar('MYAPP_SETTINGS', silent=True)
def connect_db():
"""Returns a new connection to the database."""
return sqlite3.connect(app.config['DATABASE'])
def init_db():
"""Creates the database tables."""
with closing(connect_db()) as db:
with app.open_resource('schema.sql') as f:
db.cursor().executescript(f.read())
db.commit()
@app.before_request
def before_request():
"""Make sure we are connected to the database each request."""
g.db = connect_db()
@app.after_request
def after_request(response):
"""Closes the database again at the end of the request."""
g.db.close()
return response
@app.route('/')
def show_entries():
cur = g.db.execute('select title, text from entries order by id desc')
entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()]
return render_template('show_entries.html', entries=entries)
if __name__=="__main__":
app.run()
Ваш тестовый файл myapp/test_myapp.py будет выглядеть следующим образом:
import os
import myapp
import unittest
import tempfile
class MyappTestCase(unittest.TestCase):
def setUp(self):
self.db_fd, myapp.app.config['DATABASE'] = tempfile.mkstemp()
self.app = myapp.app.test_client()
myapp.init_db()
def tearDown(self):
os.close(self.db_fd)
os.unlink(myapp.app.config['DATABASE'])
def test_empty_db(self):
rv = self.app.get('/')
assert 'No entries here so far' in rv.data
Конечно, если вы хотите использовать SQLAlchemy, вам нужно будет соответствующим образом обновить функции connect_db и init_db, но, надеюсь, вы получите эту идею.
Ответ 3
Во-первых, вместо того, чтобы создавать приложение Flask прямо в вашем script, вы используете приложение factory. Это означает, что вы создаете функцию, которая принимает ваш конфигурационный файл в качестве параметра, и возвращает экземпляр объекта приложения. Затем вы создаете глобальный объект SQLAlchemy без параметра и настраиваете его при создании приложения как описано здесь.
db = SQLAlchemy()
def create_app(configfile):
app = Flask(__name__)
app.config.from_pyfile(config, silent=True)
db.init_app(app)
# create routes, etc.
return app
Чтобы запустить приложение, вы просто делаете что-то вроде:
app = create_app('config.py')
app.run()
Чтобы запустить unittests, вы можете сделать что-то вроде:
class Test(TestCase):
def setUp(self):
# init test database, etc.
app = create_app('test_config.py')
self.app = app.test_client()
def tearDown(self):
# delete test database, etc.
В моем случае я использую SQLAlchemy напрямую с scoped_session вместо Flask-SQLAlchemy.
Я сделал то же самое, но с Lazy SQLAlchemy setup.