Package: pytest-flask-sqlalchemy
pytest-flask-sqlalchemyApytestplugin providing fixtures for running tests in
transactions usingFlask-SQLAlchemy.ContentsMotivationQuick examplesUsageInstallationFrom PyPiDevelopment versionSupported backendsConfigurationConftest setupTest configurationmocked-enginesmocked-sessionsmocked-sessionmakersWriting transactional testsFixturesdb_sessiondb_engineEnabling transactions without fixturesDevelopmentRunning the testsAcknowledgementsCopyrightMotivationInspired byDjango's built-in support for transactional
tests, this plugin
seeks to provide comprehensive, easy-to-use Pytest fixtures for wrapping tests in
database transactions forFlask-SQLAlchemyapps. The goal is to make testing stateful Flask-SQLAlchemy applications easier by
providing fixtures that permit the developer tomake arbitrary database updates
with the confidence that any changes made during a test will roll backonce the test exits.Quick examplesUse thedb_sessionfixtureto makedatabase updates that won't persist beyond
the body of the test:deftest_a_transaction(db_session):row=db_session.query(Table).get(1)row.name='testing'db_session.add(row)db_session.commit()deftest_transaction_doesnt_persist(db_session):row=db_session.query(Table).get(1)assertrow.name!='testing'Thedb_enginefixtureworks the same way, butcopies the API of
SQLAlchemy'sEngine
object:deftest_a_transaction_using_engine(db_engine):withdb_engine.begin()asconn:row=conn.execute('''UPDATE table SET name = 'testing' WHERE id = 1''')deftest_transaction_doesnt_persist(db_engine):row_name=db_engine.execute('''SELECT name FROM table WHERE id = 1''').fetchone()[0]assertrow_name!='testing'Useconfiguration propertiestomock database connections in an app and enforce nested transactions,
allowing any method from the codebase to run inside a test with the assurance
that any database changes made will be rolled back at the end of the test:# In setup.cfg[tool:pytest]mocked-sessions=database.db.sessionmocked-engines=database.engine# In database.pydb=flask_sqlalchemy.SQLAlchemy()engine=sqlalchemy.create_engine('DATABASE_URI')# In models.pyclassTable(db.Model):__tablename__='table'id=db.Column(db.Integer,primary_key=True)name=db.Column(db.String(80))defset_name(new_name):self.name=new_namedb.session.add(self)db.session.commit()# In tests/test_set_name.pydeftest_set_name(db_session):row=db_session.query(Table).get(1)row.set_name('testing')assertrow.name=='testing'deftest_transaction_doesnt_persist(db_session):row=db_session.query(Table).get(1)assertrow.name!='testing'UsageInstallationFrom PyPiInstall using pip:pip install pytest-flask-sqlalchemyOnce installed, pytest will detect the plugin automatically during test collection.
For basic background on using third-party plugins with pytest, see thepytest
documentation.Development versionClone the repo from GitHub and switch into the new directory:git clone git@github.com:jeancochrane/pytest-flask-sqlalchemy.git
cd pytest-flask-sqlalchemyYou can install using pip:pip install .Supported backendsSo far, pytest-flask-sqlalchemy has been most extensively tested against
PostgreSQL 9.6. It should theoretically work with any backend that is supported
by SQLAlchemy, but Postgres is the only backend that is currently tested by the
test suite.Official support for SQLite and MySQL isplanned for a future
release.
In the meantime, if you're using one of those backends and you run in to
problems, we would greatly appreciate your help!Open an
issueif
something isn't working as you expect.ConfigurationConftest setupThis plugin assumes that a fixture called_dbhas been
defined in the root conftest file for your tests. The_dbfixture should
expose access to a validSQLAlchemySessionobjectthat can interact with your database,
for example via theSQLAlchemyinitialization classthat configures Flask-SQLAlchemy.The fixtures in this plugin depend on this_dbfixture to access your
database and create nested transactions to run tests in.You must define
this fixture in yourconftest.pyfile for the plugin to work.An example setup that will produce a valid_dbfixture could look like this
(this example comes from thetest setupfor this repo):@pytest.fixture(scope='session')defdatabase(request):'''Create a Postgres database for the tests, and drop it when the tests are done.'''pg_host=DB_OPTS.get("host")pg_port=DB_OPTS.get("port")pg_user=DB_OPTS.get("username")pg_db=DB_OPTS["database"]init_postgresql_database(pg_user,pg_host,pg_port,pg_db)@request.addfinalizerdefdrop_database():drop_postgresql_database(pg_user,pg_host,pg_port,pg_db,9.6)@pytest.fixture(scope='session')defapp(database):'''Create a Flask app context for the tests.'''app=Flask(__name__)app.config['SQLALCHEMY_DATABASE_URI']=DB_CONNreturnapp@pytest.fixture(scope='session')def_db(app):'''Provide the transactional fixtures with access to the database via a Flask-SQLAlchemydatabase connection.'''db=SQLAlchemy(app=app)returndbAlternatively, if you already have a fixture that sets up database access for
your tests, you can define_dbto return that fixture directly:@pytest.fixture(scope='session')defdatabase():# Set up all your database stuff here# ...returndb@pytest.fixture(scope='session')def_db(database):returndatabaseTest configurationThis plugin allows you to configure a few different properties in asetup.cfgtest configuration file in order to handle the specific database connection needs
of your app. For basic background on setting up pytest configuration files, see
thepytest docs.All three configuration properties (mocked-engines,mocked-sessions, andmocked-sessionmakers)
work bypatchingone or more specified objects during a test, replacing them with equivalent objects whose
database interactions will run inside of a transaction and ultimately be
rolled back when the test exits. Using these patches, you can call methods from
your codebase that alter database state with the knowledge that no changes will persist
beyond the body of the test.The configured patches are only applied in tests where a transactional fixture
(eitherdb_sessionordb_engine) is included
in the test function arguments.mocked-enginesThemocked-enginesproperty directs the plugin topatchobjects in your codebase, typically SQLAlchemyEngineinstances, replacing them with thedb_enginefixturesuch that
any database updates performed by the objects get rolled back at the end of
the test.The value for this property should be formatted as a whitespace-separated list
of standard Python import paths, likedatabase.engine. This property isoptional.Example:# In database.pyengine=sqlalchemy.create_engine(DATABASE_URI)# In setup.cfg[tool:pytest]mocked-engines=database.engineTo patch multiple objects at once, separate the paths with a whitespace:# In setup.cfg[tool:pytest]mocked-engines=database.engine database.second_enginemocked-sessionsThemocked-sessionsproperty directs the plugin topatchobjects in your codebase, typically SQLAlchemySessioninstances, replacing them with thedb_sessionfixture such that
any database updates performed by the objects get rolled back at the end of
the test.The value for this property should be formatted as a whitespace-separated list
of standard Python import paths, likedatabase.db.session. This property isoptional.Example:# In database.pydb=SQLAlchemy()# In setup.cfg[tool:pytest]mocked-sessions=database.db.sessionTo patch multiple objects at once, separate the paths with a whitespace:# In setup.cfg[tool:pytest]mocked-sessions=database.db.session database.second_db.sessionmocked-sessionmakersThemocked-sessionmakersproperty directs the plugin topatchobjects in your codebase, typically instances ofSQLAlchemy'ssessionmakerfactory,
replacing them with a mocked class that will return the transactionaldb_sessionfixture. This can be useful if you have
pre-configured instances of sessionmaker objects that you import in the code
to spin up sessions on the fly.The value for this property should be formatted as a whitespace-separated list
of standard Python import paths, likedatabase.WorkerSessionmaker. This property isoptional.Example:# In database.pyWorkerSessionmaker=sessionmaker()[tool:pytest]mocked-sessionmakers=database.WorkerSessionmakerTo patch multiple objects at once, separate the paths with a whitespace.[tool:pytest]mocked-sessionmakers=database.WorkerSessionmaker database.SecondWorkerSessionmakerWriting transactional testsOnce you have yourconftest file set upand you'veoverrided the
necessary connectables in your test configuration, you're ready
to write some transactional tests. Simply import one of the module'stransactional
fixturesin your test signature, and the test will be wrapped in a transaction.Note that by default,tests are only wrapped in transactions if they import one of
thetransactional fixturesprovided by this module.Tests that do not
import the fixture will interact with your database without opening a transaction:# This test will be wrapped in a transaction.deftransactional_test(db_session):...# This test **will not** be wrapped in a transaction, since it does not import a# transactional fixture.defnon_transactional_test():...The fixtures provide a way for you to control which tests require transactions and
which don't. This is often useful, since avoiding transaction setup can speed up
tests that don't interact with your database.For more information about the transactional fixtures provided by this module, read on
to thefixtures section. For guidance on how to automatically enable
transactions without having to specify fixtures, see the section onenabling transactions
without fixtures.FixturesThis plugin provides two fixtures for performing database updates inside nested
transactions that get rolled back at the end of a test:db_sessionanddb_engine. The fixtures provide similar functionality, but
with different APIs.db_sessionThedb_sessionfixture allows you to perform direct updates that will be
rolled back when the test exits. It exposes the same API asSQLAlchemy'sscoped_sessionobject.Including this fixture as a function argument of a test will activate any mocks that are defined
by the configuration propertiesmocked-engines,mocked-sessions,
ormocked-sessionmakersin the test configuration file for
the duration of that test.Example:deftest_a_transaction(db_session):row=db_session.query(Table).get(1)row.name='testing'db_session.add(row)db_session.commit()deftest_transaction_doesnt_persist(db_session):row=db_session.query(Table).get(1)assertrow.name!='testing'db_engineLikedb_session, thedb_enginefixture allows you to perform direct updates
against the test database that will be rolled back when the test exits. It is
an instance of Python's built-inMagicMockclass, with a spec set to match the API ofSQLAlchemy'sEngineobject.Only a fewEnginemethods are exposed on this fixture:db_engine.begin: begin a new nested transaction (API docs)db_engine.execute: execute a raw SQL query (API docs)db_engine.raw_connection: return a raw DBAPI connection (API docs)Sincedb_engineis an instance ofMagicMockwith anEnginespec, other
methods of theEngineAPI can be called, but they will not perform any useful
work.Including this fixture as a function argument of a test will activate any mocks that are defined
by the configuration propertiesmocked-engines,mocked-sessions,
ormocked-sessionmakersin the test configuration file for
the duration of that test.Example:deftest_a_transaction_using_engine(db_engine):withdb_engine.begin()asconn:row=conn.execute('''UPDATE table SET name = 'testing' WHERE id = 1''')deftest_transaction_doesnt_persist(db_engine):row_name=db_engine.execute('''SELECT name FROM table WHERE id = 1''').fetchone()[0]assertrow_name!='testing'Enabling transactions without fixturesIf you know you want to make all of your tests transactional, it can be annoying to have
to specify one of thefixturesin every test signature.The best way to automatically enable transactions without having to include an extra fixture
in every test is to wire up anautouse fixturefor your test suite. This can be as simple as:# Automatically enable transactions for all tests, without importing any extra fixtures.@pytest.fixture(autouse=True)defenable_transactional_tests(db_session):passIn this configuration, theenable_transactional_testsfixture will be automatically used in
all tests, meaning thatdb_sessionwill also be used. This way, all tests will be wrapped
in transactions without having to explicitly require eitherdb_sessionorenable_transactional_tests.DevelopmentRunning the testsTo run the tests, start by installing a development version of the plugin that
includes test dependencies:pip install -e .[tests]Next, export adatabase connection stringthat the tests can use (the database referenced by the string will be created
during test setup, so it does not need to exist):export TEST_DATABASE_URL=<db_connection_string>Finally, run the tests using pytest:pytestAcknowledgementsThis plugin was initially developed for testingDedupe.io, a web app for record linkage and entity
resolution using machine learning. Dedupe.io is built and maintained
byDataMade.The code is greatly indebted toAlex Michael,
whose blog post"Delightful testing with pytest and
Flask-SQLAlchemy"helped
establish the basic approach on which this plugin builds.Many thanks toIgor Ghisi, who donated the PyPi
package name. Igor had been working on a similar plugin and proposed combining
efforts. Thanks to Igor, the plugin name is much stronger.CopyrightCopyright (c) 2019 Jean Cochrane and DataMade. Released under the MIT License.Third-party copyright in this distribution is noted where applicable.