7

I'm using Flask-Testing which says:

Another gotcha is that Flask-SQLAlchemy also removes the session instance at the end of every request (as should any threadsafe application using SQLAlchemy with scoped_session). Therefore the session is cleared along with any objects added to it every time you call client.get() or another client method.

However, I'm not seeing that. This test fails:

from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.testing import TestCase

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = SQLAlchemy(app)
@app.route('/')
def index():
  print 'before request:', `db.session`
  u = db.session.query(User).first()
  u.name = 'bob'
  return ''

class User(db.Model):
  id = db.Column(db.Integer, primary_key=True)
  name = db.Column(db.String)

class SessionTest(TestCase):

  def create_app(self):
    return app

  def test_remove(self):
    db.drop_all()
    db.create_all()

    u = User()
    u.name = 'joe'
    db.session.add(u)
    db.session.commit()
    client = app.test_client()
    client.get('/')
    print 'after request:', `db.session`
    print u.name
    assert u not in db.session

(Run with $ nosetests test_file.py to see it in action.)

stdout:

-------------------- >> begin captured stdout << ---------------------
before request: <sqlalchemy.orm.scoping.ScopedSession object at 0x10224c610>
after request: <sqlalchemy.orm.scoping.ScopedSession object at 0x10224c610>
bob

--------------------- >> end captured stdout << ----------------------

According to the docs, user u should not be in the session after a get request, but it is! Does anybody know why this is happening?

Furthermore, u.name is bob and not joe, even though the request never committed! (So I'm convinced it's the same session.)

For the record,

$ pip freeze | grep Flask
Flask==0.10.1
Flask-Bcrypt==0.5.2
Flask-DebugToolbar==0.8.0
Flask-Failsafe==0.1
Flask-SQLAlchemy==0.16
Flask-Script==0.6.2
Flask-Testing==0.4
Flask-Uploads==0.1.3
Flask-WTF==0.8
munchybunch
  • 6,033
  • 11
  • 48
  • 62

3 Answers3

10

I'm pretty sure the confusion comes from the fact that sessions in SQLAlchemy are scoped, meaning that each request handler creates and destroys its own session.

This is necessary because web servers can be multi-threaded, so multiple requests might be served at the same time, each working with a different database session.

For this reason, the session that you used outside of the context of a request is likely not the same session that the view function that handles the '/' route gets and then destroys at the end.

UPDATE: I dug around a bit and figured this thing out.

Flask-SQLAlchemy installs a hook on app.teardown_appcontext, and here is where it calls db.session.remove().

The testing environment does not fully replicate the environment of a real request because it does not push/pop the application context. Because of that the session is never removed at the end of the request.

As a side note, keep in mind that functions registered with before_request and after_request are also not called when you call client.get().

You can force an application context push and pop with a small change to your test:

def test_remove(self):
  db.drop_all()
  db.create_all()

  u = User()
  u.name = 'joe'
  db.session.add(u)
  db.session.commit()
  with app.app_context():
      client = app.test_client()
      client.get('/')
  print 'after request:', `db.session`
  print u.name
  assert u not in db.session

with this change the test passes.

The documentation for Flask-Testing seems to be wrong or more likely outdated. Maybe things worked like they describe at some point, but that isn't accurate for current Flask and Flask-SQLAlchemy versions.

I hope this helps!

Miguel Grinberg
  • 65,299
  • 14
  • 133
  • 152
  • That was my first guess, but the session is the same in both: during request: after request: – munchybunch Oct 21 '13 at 02:27
  • Sorry I misread your answer - what I am expecting IS the scoping behavior, and the problem here is that it ISN'T scoped like I expected. `u` should not be in `db.session` after the request, but it actually is. – munchybunch Oct 21 '13 at 02:36
  • Interesting - I was aware of the extra app_context, but I assumed a client request would push on its own. Thanks for the help; I've created an issue at https://github.com/jarus/flask-testing/issues/32 – munchybunch Oct 21 '13 at 22:23
  • I told about it in my answer – Anton Egorov Oct 22 '13 at 08:58
  • Anyone coming here from the future, I made some interesting discoveries about Flask's request- and app-context handling here: http://stackoverflow.com/a/23478870/388033 – Vanessa Phipps May 26 '16 at 16:32
0

FlaskClient works with request context while Flask-SQLAlchemy calls it's shutdown_session on app.teardown_appcontext since Flask version 0.9. Thats why nothing happens after test client call, bacause app context started by flask.ext.testing.TestCase even before test's setUp and will be closed after tearDown.

Anton Egorov
  • 1,174
  • 1
  • 11
  • 21
  • Hm, are the Flask-Testing docs just wrong, then? It's hard to believe this is expected behavior, since test requests will see each others' uncommitted changes. – munchybunch Oct 21 '13 at 02:31
0

I got the same problem when tried to use Flask-Manage to run my tests. Running tests in a separate thread solved the problem.

import threading
# some code omited
runner = unittest.TextTestRunner(verbosity=2)
t = threading.Thread(target=runner.run, args=[test_suite])
t.start()
t.join()
# other code omited
user2626972
  • 539
  • 2
  • 7
  • 14