4

I have an app with SQLAlchemy, initialized:

config_name = os.getenv('FLASK_CONFIG') or 'default'
app = Flask(__name__)
db = SQLAlchemy(app)
db.init_app(app)

and a view I'm working on that just returns all users in json format:

@app.route('/users', methods=['GET'])
def users():
    users = db.session.query(User)

    output = {'users': [user.to_json() for user in users]}
    return jsonify(**output)

And my test:

class UserViewTest(BaseTestCase, CreateUserMixin):
    def test_users(self):
        user1 = self._make_user() # defined in the above mixin
        user2 = self._make_user()
        user2.email = 'hello@abc.com'
        user2.username = 'hello@abc.com'
        db.session.add_all([user1, user2])
        db.session.flush()
        response = app.test_client().get('/users')

I run my tests with FLASK_CONFIG=testing nosetests (and I checked, app.testing is set to true).

Running this against my psql database, I'm finding these users I added to the database are being saved!

triller_social_test=# SELECT username FROM users;
   username    
---------------
 foobar
 hello@abc.com
(2 rows)

How do I stop this from happening? I tried overwriting db.session.commit() to do nothing, but the database rollsback when I call the view. Is there an alternative to using test_client()? I can't call the method directly, since jsonify doesn't allow me to return data anywhere besides a response.

Update: Here's my temporary(?) solution:

def json_response_converter(dict_):
    if config_name == 'testing':
        return dict_
    else:
        return jsonify(**dict_)

Now I don't need to use jsonify() in my tests

limasxgoesto0
  • 4,555
  • 8
  • 31
  • 38
  • You can call `db.session.delete(object)` as part of the test case cleanup. Or call `session.rollback()` should revert any transaction changes that haven't been committed. – Aaron D Apr 01 '16 at 14:39
  • I actually do a rollback() on both my setUp() and tearDown() just in case. I could do a delete(), but I'd prefer not having to keep track of every object created and then having to delete it. – limasxgoesto0 Apr 01 '16 at 14:40
  • The alternative is to override the SQLAlchemy configuration to use a separate database for testing. Look at [Miguel Grinberg's Flask Megatutorial](http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-vii-unit-testing) on unit testing. In his test case setup he changes the SQLAlchemy URI to use a SQLite test database. Of course, if you're trying to test psql, that defeats the purpose, but this way you don't have to worry about tests getting left in your production database. – Aaron D Apr 01 '16 at 14:44
  • I actually do use a separate database for testing. And unfortunately, I am testing certain things I've done in raw SQL, namely once a user is created (on a separate application - this is the "admin" app), a profile for that user is created via a trigger. So using ```db.create_all()``` and ```db._drop_all()``` doesn't cover everything for me. I actually keep my test database's migrations up to date with my main database – limasxgoesto0 Apr 01 '16 at 15:01
  • Does calling `session.begin()` before the start of the transaction make a difference? If you've configured SQLAlchemy with autocommit set to True (not the default), it's possible the call to `flush()` might also commit the transaction, but it's difficult to understand the docs on that point. More information [here](http://docs.sqlalchemy.org/en/rel_0_9/orm/session_transaction.html) and [here](http://stackoverflow.com/questions/25197871/autocommit-in-flask-sqlalchemy). I don't know but you could try it. – Aaron D Apr 01 '16 at 15:13
  • Calling ```session.begin()``` raises an error, as the session has already begun. ```flush()``` doesn't commit, and my other tests using flush don't write to the database. autocommit has not been modified, and actually I don't even have a property db.session.autocommit, only autoflush – limasxgoesto0 Apr 01 '16 at 16:28
  • You need test `setup` and `teardown` methods that create a session/transaction/savepoint in your database and then roll it back when you're done. Alternatively, just create a test database for use when testing. – Wayne Werner Apr 01 '16 at 20:13
  • Creating a savepoint is so far working, but lemme see if I can get everything working 100% – limasxgoesto0 Apr 01 '16 at 20:52
  • @WayneWerner, as far as I can tell, the savepoint solution is not saving anything to the database. Feel free to write up an answer when you get the chance – limasxgoesto0 Apr 01 '16 at 20:59
  • Actually scratch that... when making a request with test\_client more than once, even when using a ```with``` block, all data that existed the first time around disappears, and is unusable. – limasxgoesto0 Apr 01 '16 at 21:17
  • What do you mean using `db.create_all()` and `db._drop_all()` doesn't cover everything? Do you mean it doesn't create the trigger or do you mean it doesn't create the user that's normally created in a separate app? – univerio Apr 01 '16 at 23:48
  • It doesn't create the trigger. I do that via a migration. – limasxgoesto0 Apr 03 '16 at 02:01

1 Answers1

1

Using test_client alongside flush and rollback is the right approach. What are your concerns about jsonify? I don't see the connection to your main question.

Perhaps your teardown rollback isn't being called as you'd like. Including an in-line rollback allows you to verify the state is clear as part of the test.

Here's a gist showing that this approach works. https://gist.github.com/bhtucker/58a90e59265a0994bf42a23921d69718

You should be able to simply run bash setup_and_test.sh in an environment with psql tools and proper python dependencies.

bhtucker
  • 11
  • 1
  • rollback is being run (I double checked by putting it inline - data is still being saved) and I only ever explicitly call flush, never commit. Commit is being called behind the scenes when I make a request via ```test_client```, which I verified by overwriting commit and by querying the database while stepping through the test via ipdb. Once the database can query it, rollback() won't be erasing anything. My gripes with jsonify are because I'd rather just test the view method itself, which would easily solve my problem. – limasxgoesto0 Apr 01 '16 at 21:07
  • I can doublecheck later, but so far rollback has not been solving my issues – limasxgoesto0 Apr 01 '16 at 21:07
  • Before committing, the information is only visible within the same transaction. `db.session.add` is starting a transaction, and the view's query runs from inside this transaction as well (see the gist). To me the more disturbing thing is that you're seeing a commit during the request via `test_client`! That's not normal – bhtucker Apr 01 '16 at 21:31
  • I agree it's not normal, but I tested it out by just doing ```db.session.commit = lambda *args, **kwargs``` when my tests begin (in the same file/scope where db is declared), and with no other changes the data was no longer being saved (but it was being rolled back when ```test_client``` made one request, which I still didn't want) – limasxgoesto0 Apr 03 '16 at 02:03