I'm trying to do unit testing of my Flask web app. I'm use a pattern I saw in a Udemy class on Flask and a pattern similar to the Flask Mega-Tutorial online (http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-vii-unit-testing). The problem I'm having is that the test does not actual create it's own database -- rather it uses the production database and messes it up.
Here's what tests.py script looks like:
import os,sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
basedir = os.path.abspath(os.path.dirname(__file__))
import unittest
from myapp import app, db
from user.models import User
class UserTest(unittest.TestCase):
def setUp(self):
self.db_uri = 'sqlite:///' + os.path.join(basedir, 'test.db')
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
app.config['SQL_ALCHEMY_DATABASE_URI'] = self.db_uri
self.app = app.test_client()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
def test_models(self):
#Create a customer user
user = User("John Doe", "jdoe#jdoe.com", "jdoe", "password", is_consultant=False)
db.session.add(user)
db.session.commit()
#Create 2 consultant users
user1 = User("Jane Doe", "janedoe#gg.com", "janedoe", "password", is_consultant=True)
db.session.add(user1)
user2 = User("Nikola Tesla", "nikola#tesla.com", "nikola", "password", is_consultant=True)
db.session.add(user2)
db.session.commit()
#Check that all users exist
assert len(User.query.all()) is 3
My app init is in the same folder and looks like so:
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.migrate import Migrate
from flask.ext.login import LoginManager
app = Flask(__name__)
app.config.from_object('config')
db = SQLAlchemy(app)
# flask-login extension setup
login_manager = LoginManager()
login_manager.init_app(app)
# migration setup
migrate = Migrate(app, db)
from user import views
I don't understand what is going on. It never creates the test.db SQLite database. It always creates the app.db production database. And if it's there, it totally messes up the database is there. Afterwards if I do python manage.py runserver -- it doesn't work anymore. It says table not found. Because the teardown dropped all the tables. What is going on? And how do I fix it?
Omigod I figured it out. I was setting the wrong key for the database URI. It should be: app.config['SQLALCHEMY_DATABASE_URI'] = self.db_uri.
So everything is fine. Just do:
class UserTest(unittest.TestCase):
def setUp(self):
self.db_uri = 'sqlite:///' + os.path.join(basedir, 'test.db')
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = self.db_uri
self.app = app.test_client()
db.create_all()
and everything works as intended.
I checked what was going on by putting a break-point in the tests and seeing what app.config was -- and I saw that there was both a SQL_ALCHEMY_DATABASE_URI key (which doesn't do anything and I was setting) and a SQLALCHEMY_DATABASE_URI key (this is the one that matters).
Related
I am first time trying flask application factory pattern and pytest framework together.
I started with a basic sanity test for the sqlite db backend and, although the tests are working fine so far and I see test db file created successfully, the falsk_sqlalchemy is telling me that it doesn't have a db backend defined.
I tried to find the problem with pdb and the interactive console - everything looks normal. It looks like it is somehow related to
could anyone help me understand where the problem is?
(venv) C:\Users\dv\PycharmProjects\ste-speach-booking>python -m pytest tests/
=========================== test session starts ============================
platform win32 -- Python 3.6.8, pytest-5.1.1, py-1.8.0, pluggy-0.12.0
rootdir: C:\Users\dv\PycharmProjects\ste-speach-booking
collected 3 items
tests\test_models.py ... [100%]
============================= warnings summary =============================
tests/test_models.py::test_init
C:\Users\d837758\PycharmProjects\ste-speach-booking\venv\lib\site-packages\flask_sqlalchemy\__init__.py:814: UserWarning: Neither SQLALCHEMY_DATABASE_URI nor SQLALCHEMY_BINDS is set. Defaulting SQLALCHEMY_DATABASE_URI to "sqlite:///:memory:".
'Neither SQLALCHEMY_DATABASE_URI nor SQLALCHEMY_BINDS is set. '
initial tests in the test_models:
import pytest
import src.models
import datetime
def test_ActionTypes(db):
actiontype1 = src.models.Act_types(action_tyoe='workshop')
db.session.add(actiontype1)
db.session.commit()
actiontype2 = src.models.Act_types(action_tyoe='speech')
db.session.add(actiontype2)
db.session.commit()
count = db.session.query(src.models.Act_types).count()
assert count is 2
def test_meeting_creation(db):
meeting = src.models.Meeting(
_date = datetime.datetime.strptime('2018-12-19', "%Y-%m-%d"),
)
db.session.add(meeting)
db.session.commit()
conftest fixture for the db:
import os
import pytest
import src.config
from src import create_app
from src import db as _db
#pytest.fixture(scope='session')
def db():
"""Session-wide test database."""
TESTDB_PATH = src.config.testDB
print(TESTDB_PATH)
if os.path.exists(TESTDB_PATH):
os.unlink(TESTDB_PATH)
app = create_app(config=src.config.TestingConfig)
with app.app_context():
_db.create_all()
yield _db
_db.drop_all()
os.unlink(TESTDB_PATH)
app factory:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app(config=None):
"""Construct the core application."""
app = Flask(__name__, instance_relative_config=True)
db.init_app(app)
if config is None:
app.config.from_object(config.BaseConfig)
else:
app.config.from_object(config)
with app.app_context():
# Imports
from . import routes
db.create_all()
return app
config.py:
basedir = os.path.abspath(os.path.dirname(__file__))
baseDB = os.path.join(basedir, 'app.db')
devDB = os.path.join(basedir, 'dev_app.db')
testDB = os.path.join(basedir, 'testing_app.db')
class BaseConfig(object):
DEBUG = False
TESTING = False
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + baseDB
SQLALCHEMY_TRACK_MODIFICATIONS = False
class TestingConfig(BaseConfig):
DEBUG = False
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + testDB
The issue is with the order of configuration of the components of your application in create_app().
When you call db.init_app(app) the first operations it performs are (source):
if (
'SQLALCHEMY_DATABASE_URI' not in app.config and
'SQLALCHEMY_BINDS' not in app.config
):
warnings.warn(
'Neither SQLALCHEMY_DATABASE_URI nor SQLALCHEMY_BINDS is set. '
'Defaulting SQLALCHEMY_DATABASE_URI to "sqlite:///:memory:".'
)
Recognize that warning?
Immediately it looks in app.config for required configurations. The method goes on to either accept the supplied configuration from the app or set a default, in this case the default is the in memory database.
In your implementation of create_app() the call to db.init_app() comes before the app itself is configured, with this:
db.init_app(app)
if config is None:
app.config.from_object(config.BaseConfig)
else:
app.config.from_object(config)
Until app.config is populated, none of the SQLALCHEMY_ prefixed configurations exist on the app and so when db.init_app() goes looking for them, they aren't found and the defaults are used. Moving the config of db to after the config of the app fixes the issue:
if config is None:
app.config.from_object(config.BaseConfig)
else:
app.config.from_object(config)
db.init_app(app)
This is quite similar to this question, however I think yours is a better example of a typical setup (scope of create_app() and configuration method) so worth answering.
Ensure the app.config dictionary has the following:
app = Flask(__name__) # The URI config should be initialized after flask
['SQLALCHEMY_DATABASE_URI'] = 'to your database string'
then:
db = SQAlchemy(app)
was having the same issue because I had the Flask initialization after the database Uri connection.
In development (so sqlite3) I'm getting this error on any database access:
sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) no such table: ujs ...
I got here by saying
export FLASK_ENV=development
export FLASK_APP=my_app.py
flask db init
flask db migrate
flask db upgrade
flask run
and then doing an HTTP GET against that dev server.
I believe the migration workflow succeeded, because when I use the sqlite3 commandline client, I can see the (empty) table with a believably correct schema.
╭╴ (get-db-working *%=)╶╮
╰ jeff#starshine:TN_flask_web $ sqlite3 dev.db
SQLite version 3.27.2 2019-02-25 16:06:06
Enter ".help" for usage hints.
sqlite> .table
alembic_version ujs
sqlite> .quit
╭╴ (get-db-working *%=)╶╮
╰ jeff#starshine:TN_flask_web $
I therefore believe I've made a coding error. But I'm not seeing it.
I have this code (pared down to what I believe is the essential bits):
my_app.py:
from app import create_app, db, cli
from app.models import UJS
app = create_app()
cli.register(app)
#app.shell_context_processor
def make_shell_context():
return {'db': db,
'UJS': UJS}
app/models.py:
from app import db
import time
def now_in_microseconds():
"""Return the current time in microseconds since the epoch.
"""
return time.time() * 1000 * 1000
class UJS(db.Model):
id = db.Column(db.Integer, primary_key=True)
timestamp_microseconds = db.Column(db.BigInteger, default=now_in_microseconds)
ip_hash = db.column(db.String(40))
# And then some more columns, all quite boring.
def __repr__(self):
return '<[{tag}]/[{ip}] {microsec}/{city}>'.format(
tag=self.tag, ip=self.ip_hash,
microsec=self.timestamp_microseconds, city=self.city)
app/__init__.py:
from flask import Flask, request, current_app
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import Config
db = SQLAlchemy()
migrate = Migrate()
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
try:
app.config.from_pyfile("../config_local.py")
except FileNotFoundError:
print('No local config found.')
except:
print('Unexpected error on app.config.from_pyfile()')
db.init_app(app)
migrate.init_app(app, db)
...
return app
from app import models
and app/main/routes.py:
from flask import request, g, current_app, session
from app import db
from app.main import bp
from app.models import UJS
#bp.before_app_request
def before_request():
if 'static' == request.endpoint:
# This should only happen in dev. Otherwise, nginx handles static routes directly.
return
# I expect this to return an empty list, but it throws a 500.
print(UJS.query.all())
Any suggestions what I'm missing?
For anyone who might find this question later on: the problem was about having the right absolute path to your DB in your SQLALCHEMY_DATABASE_URI config value.
Also (this wasnt the case here, but it might possibly gotcha with the same symptoms) - if you omit __tablename__ on Model declaration, SQLAlchemy might autogenerate something you wont expect. Just a thing to keep in mind, if you're working with an existing DB with some schema already in place.
I am first time trying flask application factory pattern and pytest framework together.
I started with a basic sanity test for the sqlite db backend and, although the tests are working fine so far and I see test db file created successfully, the falsk_sqlalchemy is telling me that it doesn't have a db backend defined.
I tried to find the problem with pdb and the interactive console - everything looks normal. It looks like it is somehow related to
could anyone help me understand where the problem is?
(venv) C:\Users\dv\PycharmProjects\ste-speach-booking>python -m pytest tests/
=========================== test session starts ============================
platform win32 -- Python 3.6.8, pytest-5.1.1, py-1.8.0, pluggy-0.12.0
rootdir: C:\Users\dv\PycharmProjects\ste-speach-booking
collected 3 items
tests\test_models.py ... [100%]
============================= warnings summary =============================
tests/test_models.py::test_init
C:\Users\d837758\PycharmProjects\ste-speach-booking\venv\lib\site-packages\flask_sqlalchemy\__init__.py:814: UserWarning: Neither SQLALCHEMY_DATABASE_URI nor SQLALCHEMY_BINDS is set. Defaulting SQLALCHEMY_DATABASE_URI to "sqlite:///:memory:".
'Neither SQLALCHEMY_DATABASE_URI nor SQLALCHEMY_BINDS is set. '
initial tests in the test_models:
import pytest
import src.models
import datetime
def test_ActionTypes(db):
actiontype1 = src.models.Act_types(action_tyoe='workshop')
db.session.add(actiontype1)
db.session.commit()
actiontype2 = src.models.Act_types(action_tyoe='speech')
db.session.add(actiontype2)
db.session.commit()
count = db.session.query(src.models.Act_types).count()
assert count is 2
def test_meeting_creation(db):
meeting = src.models.Meeting(
_date = datetime.datetime.strptime('2018-12-19', "%Y-%m-%d"),
)
db.session.add(meeting)
db.session.commit()
conftest fixture for the db:
import os
import pytest
import src.config
from src import create_app
from src import db as _db
#pytest.fixture(scope='session')
def db():
"""Session-wide test database."""
TESTDB_PATH = src.config.testDB
print(TESTDB_PATH)
if os.path.exists(TESTDB_PATH):
os.unlink(TESTDB_PATH)
app = create_app(config=src.config.TestingConfig)
with app.app_context():
_db.create_all()
yield _db
_db.drop_all()
os.unlink(TESTDB_PATH)
app factory:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app(config=None):
"""Construct the core application."""
app = Flask(__name__, instance_relative_config=True)
db.init_app(app)
if config is None:
app.config.from_object(config.BaseConfig)
else:
app.config.from_object(config)
with app.app_context():
# Imports
from . import routes
db.create_all()
return app
config.py:
basedir = os.path.abspath(os.path.dirname(__file__))
baseDB = os.path.join(basedir, 'app.db')
devDB = os.path.join(basedir, 'dev_app.db')
testDB = os.path.join(basedir, 'testing_app.db')
class BaseConfig(object):
DEBUG = False
TESTING = False
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + baseDB
SQLALCHEMY_TRACK_MODIFICATIONS = False
class TestingConfig(BaseConfig):
DEBUG = False
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + testDB
The issue is with the order of configuration of the components of your application in create_app().
When you call db.init_app(app) the first operations it performs are (source):
if (
'SQLALCHEMY_DATABASE_URI' not in app.config and
'SQLALCHEMY_BINDS' not in app.config
):
warnings.warn(
'Neither SQLALCHEMY_DATABASE_URI nor SQLALCHEMY_BINDS is set. '
'Defaulting SQLALCHEMY_DATABASE_URI to "sqlite:///:memory:".'
)
Recognize that warning?
Immediately it looks in app.config for required configurations. The method goes on to either accept the supplied configuration from the app or set a default, in this case the default is the in memory database.
In your implementation of create_app() the call to db.init_app() comes before the app itself is configured, with this:
db.init_app(app)
if config is None:
app.config.from_object(config.BaseConfig)
else:
app.config.from_object(config)
Until app.config is populated, none of the SQLALCHEMY_ prefixed configurations exist on the app and so when db.init_app() goes looking for them, they aren't found and the defaults are used. Moving the config of db to after the config of the app fixes the issue:
if config is None:
app.config.from_object(config.BaseConfig)
else:
app.config.from_object(config)
db.init_app(app)
This is quite similar to this question, however I think yours is a better example of a typical setup (scope of create_app() and configuration method) so worth answering.
Ensure the app.config dictionary has the following:
app = Flask(__name__) # The URI config should be initialized after flask
['SQLALCHEMY_DATABASE_URI'] = 'to your database string'
then:
db = SQAlchemy(app)
was having the same issue because I had the Flask initialization after the database Uri connection.
I want to add a new table or add data by calling db, but i got some problem when i try to import db
db return like this <SQLAlchemy engine=None>
which mean i didnt already doing this db.init_app(app)
this is my file struckture
Root
run.py
------>server/__init__.py
Config.py
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY')
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:#localhost/flask_py'
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USERNAME = os.environ.get('EMAIL_USER')
MAIL_PASSWORD = os.environ.get('EMAIL_PASS')
Run.py
from server import server_app, db
app = server_app()
if __name__ == '__main__':
app.run(debug=True)
__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from server.config import Config
db = SQLAlchemy()
def server_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(Config)
db.init_app(app)
from server.users.routes import users
app.register_blueprint(users)
return app
in my command line using windows i want to import db, i try like this :
D:\PYTHON\root>python
from run import db
db.create_all()
but when i check is :
<SQLAlchemy engine=None>
I have a full working solution here very similar to what you're doing. Check my layout and then look at the create_db.py.
https://github.com/researcher2/stackoverflow_56885380
It appears in your case the "server_app" is not being executed in your interactive shell. Assuming you are running interactive shell in root directory, you would want to do the following:
from server import db, create_app
app = server_app()
with app.app_context():
db.create_all()
The annoying thing about flask-sqlalchemy as opposed to plain sqlalchemy is the db is coupled to a flask app. The db config comes from the flask config and the initiation is done during db_init or just SqlAlchemy(db) if you want the simpler version for single app setup.
Your models would also need to be setup properly. In my example above I just had them in the create_db script.
This post may help you as well regarding factories and blueprints. I created the above github to answer it.
Reflecting different databases in Flask factory setup
I have a similar problem to the user here: SQLAlchemy Object already attached to session
And I'm getting basically the same error:
'<Link at 0x7f31a785f630>' is already attached to session '1' (this is '15')
I'm really trying to figure out why multiple sessions are being created, when I only want one. I have two files __init__.py and models.py:
Lines of interest from __init__.py:
from .models import User, draft_new_link_message, load_history, load_messages, db
# Initialize app and such
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///my.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.secret_key = 'super secret keyssss'
socketio = SocketIO(app)
db.init_app(app)
app.app_context().push()
...
db.create_all()
From models.py:
db = SQLAlchemy()
class Link(db.Model):
__tablename__ = 'link'
id = db.Column(db.Integer, primary_key=True, nullable=False)
url = db.Column(db.String(500), nullable=False)
originator_id = db.Column(db.Integer, db.ForeignKey('user.id'))
originator = db.relationship("User", back_populates='history')
From these lines alone, it seems that I should be on one session. If I'm not, how do I format my code correctly to reduce headaches and make sure I don't have to transfer objects between sessions? Thanks!
Edit: Solution
The reason I structured my project this way was because a few pieces of documentation said this was the correct pattern (creating the db inside your models file and then callng db.init_app() to get it into the main file). But I guess this was a bad idea. I thought maybe I had to because I can't have both the files reference each other. But to get around this I wrote a method in the main file to get the db and called the import on the models function
My new __init__.py:
# Initialize app and such
app = Flask(name)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///browse_together.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.secret_key = 'super secret keysssss'
socketio = SocketIO(app)
db = SQLAlchemy(app)
# Provide a way for models.py (and any other files that needs it) to get access to the database
def get_db():
return db
# Now you can import models.py because it can use this database
from . import urltils, models
from .models import User, Group, get_groups, create_group, \
draft_new_link_message, load_history, load_messages, toggle_send
The new first few lines from models.py:
from flask_login import UserMixin
from . import urltils
from . import get_db
# Get an instance of the db from __init__
db = get_db()
I think this is more correct.
The reason I structured my project this way was because a few pieces of documentation said this was the correct pattern (creating the db inside your models file and then callng db.init_app() to get it into the main file). But I guess this was a bad idea. I thought maybe I had to because I can't have both the files reference each other. But to get around this I wrote a method in the main file to get the db and called the import on the models function
My new __init__.py:
# Other imports...
# Initialize app and such
app = Flask(name)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///my.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
app.secret_key = 'super secret keysssss'
db = SQLAlchemy(app)
# Provide a way for models.py (and any other files that needs it) to get access to the database
def get_db():
return db
# Now you can import models.py because it can use this database
from . import urltils, models
from .models import User, Group, get_groups, create_group, \
draft_new_link_message, load_history, load_messages, toggle_send
The new first few lines from models.py:
from flask_login import UserMixin
from . import urltils
from . import get_db
# Get an instance of the db from __init__
db = get_db()
I think this is more correct.