python-3.xsqlalchemypyramidwebtest

How do I properly set up a single SQLAlchemy session for each unit test?


When testing my Pyramid application using WebTest, I have not been able to create/use a separate Session in my tests without getting warnings about a scoped session already being present.

Here is the main() function of the Pyramid application, which is where the database is configured.

# __init__.py of Pyramid application

from pyramid_sqlalchemy import init_sqlalchemy
from sqlalchemy import create_engine


def main(global_config, **settings):
    ...
    db_url = 'some-url'
    engine = create_engine(db_url)
    init_sqlalchemy(engine)  # Warning thrown here.

Here is the test code.

# test.py (Functional tests)

import transaction
from unittest import TestCase
from pyramid.paster import get_appsettings
from pyramid_sqlalchemy import init_sqlalchemy, Session
from sqlalchemy import create_engine
from webtest import TestApp

from app import main
from app.models.users import User


class BaseTestCase(TestCase):
    def base_set_up(self):
        # Create app using WebTest
        settings = get_appsettings('test.ini', name='main')
        app = main({}, **settings)
        self.test_app = TestApp(app)

        # Create session for tests.
        db_url = 'same-url-as-above'
        engine = create_engine(db_url)
        init_sqlalchemy(engine)
        # Note: I've tried both using pyramid_sqlalchemy approach here and 
        # creating a "plain, old" SQLAlchemy session here using sessionmaker.

    def base_tear_down(self):
        Session.remove()


class MyTests(BaseTestCase):
    def setUp(self):
        self.base_set_up()

        with transaction.manager:
            self.user = User('user@email.com', 'John', 'Smith')
            Session.add(self.user)
            Session.flush()

            Session.expunge_all()
        ...

    def tearDown(self):
        self.base_tear_down()

    def test_1(self):
        # This is a typical workflow on my tests.
        response = self.test_app.patch_json('/users/{0}'.format(self.user.id), {'email': 'new.email@email.com')
        self.assertEqual(response.status_code, 200)

        user = Session.query(User).filter_by(id=self.user.id).first()
        self.assertEqual(user.email, 'new.email@email.com')
    ...
    def test_8(self):
        ...

Running the tests gives me 8 passed, 7 warnings, where every test except the first one gives the following warning:

From Pyramid application: __init__.py -> main -> init_sqlalchemy(engine): sqlalchemy.exc.SAWarning: At least one scoped session is already present. configure() can not affect sessions that have already been created.

If this is of any use, I believe I am seeing the same issue here, except I am using pyramid_sqlalchemy rather than creating my own DBSession.

https://github.com/Pylons/webtest/issues/5


Solution

  • Answering my own question: I'm not sure if this is the best approach, but one that worked for me.

    Instead of trying to create a separate session within my tests, I am instead using the pyramid_sqlalchemy Session factory, which is configured in the application. As far as I can tell, calls to Session in both test and application code return the same registered scoped_session.

    My original intent with creating a separate session for my tests was to confirm that records were being written to the database, and not just updated in the active SQLAlchemy session. With this new approach, I've managed to avoid these "caching" issues by issuing Session.expire_all() at points in the tests where I transition between test transactions and application transactions.

    # test.py (Functional tests)
    
    import transaction
    from unittest import TestCase
    from pyramid.paster import get_appsettings
    from pyramid_sqlalchemy import Session
    from webtest import TestApp
    
    from app import main
    from app.models.users import User
    
    
    class BaseTestCase(TestCase):
        def base_set_up(self):
            # Create app using WebTest
            settings = get_appsettings('test.ini', name='main')
            app = main({}, **settings)
            self.test_app = TestApp(app)
    
            # Don't set up an additional session in the tests. Instead import
            # and use pyramid_sqlalchemy.Session, which is set up in the application.
    
        def base_tear_down(self):
            Session.remove()
    
    
    class MyTests(BaseTestCase):
        def setUp(self):
            self.base_set_up()
    
            with transaction.manager:
                self.user = User('user@email.com', 'John', 'Smith')
                Session.add(self.user)
                Session.flush()
    
                Session.expunge_all()
                Session.expire_all()  # "Reset" your session.
    
        def tearDown(self):
            self.base_tear_down()