@pytest.fixturedef session(request, testapp):
factory = testapp.app.registry['dbsession_factory'] engine = factory.kw['bind']
# create all the tables in db Base.metadata.create_all(engine)
log.info ("Creating root transaction for the test session") with transaction.manager as tx: from plantoeducate_data.models import get_tm_session session=get_tm_session(factory, transaction.manager)
brief=DocumentTemplateModel () brief.DocumentDescription='brief' brief.DocumentTypeID=1 brief.DocumentFilePath='brief.ott' feedback=DocumentTemplateModel () feedback.DocumentDescription='feedback' feedback.DocumentTypeID=2 feedback.DocumentFilePath='feedback.ott'
session.add_all([brief, feedback]) #session.flush()
yield session
log.info("Rolling back root transaction") transaction.abort() session.close()
def test_delete_document(self, testapp, session): doc=session.query(DocumentTemplateModel).first() import pdb; pdb.set_trace() # delete the document res = testapp.delete('/documents/templates/{}'.format(doc.DocumentID), status=204)
def test_filter_documents(self, testapp, session): res = testapp.get('/documents/templates/1', status=200)
expectedTemplatesCount = 2 import pdb; pdb.set_trace()Hi dcs3spp,
I remember it was pretty complicated for me to setup something as you describe: one sqla session per test, which will be automatically rolled back.
You are right, that in the TestApp there is a separate transaction handling, which destroys your approach. To solve that problem for me (no idea if this was elegant or not, but it does what you describe) I did the following.
Created a TestApp with a custom registry field:
@pytest.fixture(scope="function")
def _test_app(router: Router) -> TestApp:
return TestApp(router)
@pytest.fixture(scope="function")
def test_app(_test_app: TestApp, _sqla_session) -> TestApp:
"""wrapper to ensure the fixture-created `sqla_session` will be picked up in `test_app`"""
_test_app.app.registry.settings['paste.testing'] = True
_test_app.app.registry['paste.testing.session'] = _sqla_session
return _test_app
along with the usual sqla fixure
@pytest.fixture(scope="session")
def _sqla_session(pyramid_config, sqla_engine) -> Session:
"""
Depending on this fixture is comparable to a integration test, which has nothing more than
the orm properly defined. Which is helpful, but not the full application configured.
"""
session_factory = get_session_factory(sqla_engine, db_session)
return session_factory()
@pytest.fixture(scope="function")
def sqla_session(_sqla_session: Session, test_app):
"""
wrap a transaction
"""
# TODO andi: magically there is a transaction active here. why.
# t = _sqla_session.begin()
yield _sqla_session
# this is the important `rollback`
_sqla_session.transaction.rollback()
Then I have something, that probably `pyramid_tm` does under the hood: a request listener, which handles a “per request session”.
def add_tm_session(req):
# this property is set in `webtest.app.TestApp#do_request`
if 'paste.testing' in req.environ and req.environ['paste.testing'] is True:
from pyramid.threadlocal import get_current_registry
registry = get_current_registry()
# in case of integration testing, we set this registry key with the `sqla_session` from
# around. this allows us to operate in the same session, so we don't need any commits.
return registry.get('paste.testing.session', None)
# request.tm is the transaction manager used by pyramid_tm
return get_tm_session(session_factory, req.tm)
# make request.dbsession available for use in Pyramid
config.add_request_method(add_tm_session, 'dbsession', reify=True)
You can see the “magic” in there.
What I found in my documentation was this link (another was dead already), which I found helpful: https://gist.github.com/inklesspen/4504383
The good thing about this pattern for me was, that I could apply this to Celery jobs in tests or Spyne integration - aka pretty flexible.
Hope this helps.
Andi
--
You received this message because you are subscribed to the Google Groups "pylons-discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pylons-discus...@googlegroups.com.
To post to this group, send email to pylons-...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/pylons-discuss/bf247ef1-1a48-4934-9bce-205d166a5c64%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Welcome dcs3spp!
One thing I noted in your snippet was you where using pdb (or ipdb). That looks like you’re not using an IDE that is able to debug ;)
I would really recommend using PyCharm for such tinkering, cause even the community version has a well integrated debugger and supports py.test (once you configured your project to use it – one click in the configuration).
Best, Andi
To view this discussion on the web visit https://groups.google.com/d/msgid/pylons-discuss/8daf90a7-3485-4a1c-8cbf-caa9153a054f%40googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/pylons-discuss/A1460691-D307-4B5C-846C-E8CD20BE8FA3%40googlemail.com.
Thanks Michael,
I’d be glad to have this appear in a cookiecutter – let me know how I can help.
For the record, what I missed to mention (but is actually the bigger selling point for me):
Apart from the automatic rollback functionality, that approach allows putting anything to the session, no matter where. I.e. you can `add` and `flush` something in a test, before calling the `TestApp` in the same test, which will have access to what was `add`ed to that session. Still without anything being committed. You can imagine how easy writing integration tests is in the end.
Best, Andi
From: <pylons-...@googlegroups.com> on behalf of Michael Merickel <mmer...@gmail.com>
Reply-To: <pylons-...@googlegroups.com>
Date: Tuesday, 6. November 2018 at 16:58
To: <pylons-...@googlegroups.com>
Subject: Re: [pylons-discuss] Understanding how to perform functional testing using transactions with pyramid + SQLAlchemy + pytest + webtest for views that use a postgreSQL database.
Andi, I think this is a fantastic approach. The key here is to override request.dbsession to a mocked out version which is not connected to pyramid_tm at all, giving you full control of commit/rollback from the outside. One extra step would be to disable pyramid_tm entirely by setting environ['tm.active'] = True which would let you mock out request.tm if you were using it for any other purposes. This can be done from the TestApp constructor (you could also use this for the dbsession to avoid using the get_current_request threadlocal) via TestApp(app, extra_environ={'tm.active': True, 'dbsession': dbsession}).
To view this discussion on the web visit https://groups.google.com/d/msgid/pylons-discuss/CAKdhhwFcagH3GzLMdJZ2qCa%3D9csv5nEzcRR47%2Bg5k%2BYFMH1kRA%40mail.gmail.com.
@pytest.fixture(scope="session")
def settings():
""" Return a dictionary of application settings defined in test.ini, including expanded environment variables.
:return: Dictionary of application settings defined in test.ini.
"""
testconfigfile = project_path(pytest.config.rootdir.strpath, 'test.ini')
settings = get_appsettings(testconfigfile, name='main')
settings = load_environment(testconfigfile, settings)
conf = {'global_config': {'__file__': testconfigfile}}
settings.update(conf)
return settings
@pytest.fixture(scope="function")
def testapp(settings, session):
""" Wrap `webtest.TestApp` request within a doomed transaction that aborts when a test completes.
Attempts to commit will raise a `DoomedTransaction` exception. Doomed transactions can only be aborted.
The test app instance is initialised with environment tm.active set to True to instruct pyramid_tm to bypass
creating transaction for request.
:param settings: dictionary of application settings contained within `test.ini`.
:param session: SQLAlchemy session.
:return: webtest.TestApp object.
"""
testing.setUp()
log.info("testapp fixture :: starting a doomed transaction")
with transaction.manager as tx:
tx.doom()
from plantoeducate_data.models import set_tm_session
app = main(**settings)
testapp = webtest.TestApp(app, extra_environ={'db.session': session, 'tm.active': True})
set_tm_session(session, transaction.manager)
yield testapp
log.info("testapp fixture :: aborting transaction")
tx.abort()
testing.tearDown()
@pytest.fixture(scope="session")
def session(settings):
""" Return a SQLAlchemy session for the database referenced by `sqlalchemy.url` within test.ini file.
When tests have been run the session is closed and the associated engine is disposed.
:param settings: dictionary of application settings contained within `test.ini`.
:return: SQLAlchemy session object.
"""
engine = get_engine(settings, prefix='sqlalchemy.')
factory = get_session_factory(engine)
Base.metadata.create_all(engine)
session = factory()
yield session
session.close()
engine.dispose()
def set_tm_session(session, transaction_manager):
zope.sqlalchemy.register(session, transaction_manager=transaction_manager)
return sessiondef includeme(config):...
# use pyramid_tm to hook the transaction lifecycle to the request
config.include('pyramid_tm')
session_factory = get_session_factory(get_engine(filtered_settings))
config.registry['dbsession_factory'] = session_factory
def add_tm_session(request):
""" Request method that returns a SQLAlchemy session for a request.
The SQLAlchemy session is managed by a Zope transaction, unless the request has been generated from a
webtest.TestApp instance for functional testing. In this case:
- Inspect request.environ dictionary for the SQLAlchemy session referenced by key db.session. Remove the
session from the request's environment dictionary and return the session.
- Use the session factory to generate and return a new SQLAlchemy session if there is no entry for db.session
in the request environment dictionary.
When functional testing it is the responsibility of the test client to configure the Zope transaction
manager for the SQLAlchemy session. For example:
with transaction.manager as tx:
set_tm_session(session, tx)
The webtest.TestApp instance should configure the environment dictionary as follows:
`testapp = webtest.TestApp(app, extra_environ={'db.session': session, 'tm.active': True})`
Setting tm.active to True causes the pyramid_tm tween to bypass generating a transaction for the SQLAlchemy
session on the request.
:param request: Pyramid Request instance
:return: SQLAlchemy session.
"""
if 'paste.testing' in request.environ and request.environ['paste.testing'] is True:
if 'db.session' in request.environ:# and 'db.tm' in request.environ:
dbsession = request.environ['db.session']
del request.environ['db.session']
return dbsession
#return set_tm_session(request.environ['db.session'], request.environ['db.tm'])
else:
return session_factory()
return get_tm_session(session_factory, request.tm)w
config.add_request_method(add_tm_session, 'dbsession', reify = True)
def test_filter_documents_feedback(self, testapp, documents):
res = testapp.get('/documents/templates/2', status=200)
log.info("test_filter_documents_feedback :: completed filter request")
expected_feedback_templates_count = 1
expected_templates_count = 2
assert(len(res.json['data']) == expected_feedback_templates_count)
assert(documents.query(DocumentTemplateModel).count() == expected_templates_count)