11# pytest-flask-sqlalchemy-transactions
22
3+ [ ![ Build Status] ( https://travis-ci.org/jeancochrane/pytest-flask-sqlalchemy-transactions.svg?branch=master )] ( https://travis-ci.org/jeancochrane/pytest-flask-sqlalchemy-transactions ) ![ PyPI - Python Version] ( https://img.shields.io/pypi/pyversions/Django.svg )
4+
35A [ pytest] ( https://docs.pytest.org/en/latest/ ) plugin providing fixtures for running tests in
46transactions using Flask and SQLAlchemy.
57
8+ ## Contents
9+
10+ - [ ** Motivation** ] ( #motivation )
11+ - [ ** Quick examples** ] ( #quick-examples )
12+ - [ ** Usage** ] ( #usage )
13+ - [ Installation] ( #installation )
14+ - [ From PyPi] ( #from-pypi )
15+ - [ Development version] ( #development-version )
16+ - [ Configuration] ( #configuration )
17+ - [ Conftest setup] ( #conftest-setup )
18+ - [ Test configuration (.ini or .cfg file)] ( #test-configuration-ini-or-cfg-file )
19+ - [ ` db-connection-string ` ] ( #db-connection-string )
20+ - [ ` mocked-engines ` ] ( #mocked-engines )
21+ - [ ` mocked-sessions ` ] ( #mocked-sessions )
22+ - [ ` mocked-sessionmakers ` ] ( #mocked-sessionmakers )
23+ - [ Fixtures] ( #fixtures )
24+ - [ ` db_session ` ] ( #db_session )
25+ - [ ` db_engine ` ] ( #db_engine )
26+ - [ ` module_engine ` ] ( #module_engine )
27+ - [ Using the ` transactional ` mark] ( #using-the-transactional-mark )
28+ - [ ** Development** ] ( #development )
29+ - [ Running the tests] ( #running-the-tests )
30+ - [ Acknowledgements] ( #acknowledgements )
31+ - [ Copyright] ( #copyright )
32+
633## Motivation
734
835Inspired by [ Django's built-in support for transactional
936tests] ( https://jeancochrane.com/blog/django-test-transactions ) , this plugin
1037seeks to provide comprehensive, easy-to-use fixtures for wrapping tests in
11- database transactions using Pytest, Flask, and SQLAlchemy.
12-
13- In order to test database operations in the context of stateful web applications,
14- developers must pay an inordinate amount of attention to the database changes incurred by their
15- tests, since unanticipated changes can leak state between tests and inhibit
16- test isolation. While some test engineers advocate avoiding database
17- interactions entirely (see Harry Percival's [ "Fast Tests, Slow Tests, and Hot
18- Lava"] ( https://www.obeythetestinggoat.com/book/chapter_hot_lava.html ) for more
19- on this perspective), this approach is infeasible in data-intensive web applications,
20- where much of the business logic involves state changes to a database. For
21- these kinds of applications, a solution is needed that will allow the developer
22- to trigger and verify database updates while still ensuring test isolation.
23-
24- The goal of this plugin is to make testing stateful Flask applications easier
25- by providing fixtures that permit the developer to ** make arbitrary
26- database updates with the confidence that any changes made during a test will
27- roll back** once the test exits.
38+ database transactions using Pytest, Flask, and SQLAlchemy. We aim to make
39+ testing stateful Flask applications easier by providing fixtures that permit
40+ the developer to ** make arbitrary database updates with the confidence that
41+ any changes made during a test will roll back** once the test exits.
2842
2943## Quick examples
3044
31- Use the ` db_session ` fixture to make ** database updates that won't persist beyond
45+ Use the [ ` db_session ` fixture] ( #db_session ) to make ** database updates that won't persist beyond
3246the body of the test** :
3347
3448``` python
@@ -44,9 +58,9 @@ def test_transaction_doesnt_persist(db_session):
4458 assert row.name != ' testing'
4559```
4660
47- The ` db_engine ` fixture works the same way, but ** copies the API of
61+ The [ ` db_engine ` fixture] ( #db_engine ) works the same way, but ** copies the API of
4862SQLAlchemy's [ Engine
49- object] ( http://docs.sqlalchemy.org/en/latest/core/connections.html#sqlalchemy.engine.Engine ) ** .
63+ object] ( http://docs.sqlalchemy.org/en/latest/core/connections.html#sqlalchemy.engine.Engine ) ** :
5064
5165``` python
5266def test_a_transaction_using_engine (db_engine ):
@@ -58,10 +72,10 @@ def test_transaction_doesnt_persist(db_engine):
5872 assert row_name != ' testing'
5973```
6074
61- Use configuration properties to ** mock database connections in an app with fixtures that enforce
62- nested transactions ** , allowing any method from the codebase to run inside
63- a test that imports a transactional fixture ( ` db_session ` or ` db_engine ` ) with
64- the assurance that any database changes made will be rolled back at the end of the test:
75+ Use [ configuration properties] ( #test-configuration-ini-or-cfg-file ) to
76+ ** mock database connections in an app and enforce nested transactions ** ,
77+ allowing any method from the codebase to run inside a test with the assurance
78+ that any database changes made will be rolled back at the end of the test:
6579
6680``` ini
6781# tests/tox.ini
@@ -105,8 +119,8 @@ def test_transaction_doesnt_persist(db_session):
105119 assert row.name != ' testing'
106120```
107121
108- Use the ** ` @pytest.mark.transactional ` mark to enforce that a test gets run
109- inside a transaction** :
122+ Use the [ ` @pytest.mark.transactional ` mark] ( #using-the-transactional-mark )
123+ to ** enforce that a test gets run inside a transaction** :
110124
111125``` python
112126from api.database import db
@@ -136,18 +150,23 @@ Install using pip:
136150pip install pytest-flask-sqlalchemy-transactions
137151```
138152
153+ Once installed, pytest will detect the plugin automatically during test collection.
154+ For basic background on using third-party plugins with pytest, see the [ pytest
155+ documentation] ( https://docs.pytest.org/en/latest/plugins.html?highlight=plugins ) .
156+
139157### Development version
140158
141159Clone the repo from GitHub and switch into the new directory:
142160
143161```
144162git clone git@github.com:jeancochrane/pytest-flask-sqlalchemy-transactions.git
163+ cd pytest-flask-sqlalchemy-transactions
145164```
146165
147- You can install using pip if you'd like :
166+ You can install using pip:
148167
149168```
150- pip install -e .
169+ pip install .
151170```
152171
153172Or install the plugin dependencies manually:
@@ -161,18 +180,19 @@ pip install -r requirements/main.txt
161180### Conftest setup
162181
163182This plugin assumes that a fixture called ` _db ` has been
164- defined in the root conftest file for your tests. As a fixture, ` _db ` should
165- expose access to a valid Session object that can interact with your database,
166- for example via the [ ` SQLAlchemy ` initialization object ] ( http://flask-sqlalchemy.pocoo.org/2.3/api/#flask_sqlalchemy.SQLAlchemy )
183+ defined in the root conftest file for your tests. The ` _db ` fixture should
184+ expose access to a valid [ SQLAlchemy ` Session ` object] ( http://docs.sqlalchemy.org/en/latest/orm/session_api.html?highlight=session#sqlalchemy.orm.session.Session ) that can interact with your database,
185+ for example via the [ ` SQLAlchemy ` initialization class ] ( http://flask-sqlalchemy.pocoo.org/2.3/api/#flask_sqlalchemy.SQLAlchemy )
167186that configures Flask-SQLAlchemy.
168187
169188The fixtures in this plugin depend on this ` _db ` fixture to access your
170- database and create nested transactions to run tests in.
189+ database and create nested transactions to run tests in. ** You must define
190+ this fixture in your ` conftest.py ` file for the plugin to work.**
171191
172192An example setup that will produce a valid ` _db ` fixture could look like this
173193(this example comes from the [ test setup] ( ./tests/_conftest.py#L25-L61 ) for this repo):
174194
175- ```
195+ ``` python
176196@pytest.fixture (scope = ' session' )
177197def database (request ):
178198 '''
@@ -231,13 +251,13 @@ def _db(database):
231251### Test configuration (.ini or .cfg file)
232252
233253This plugin requires that you set up a test configuration file with a few
234- specific properties under the ` [pytest] ` section. For background on pytest
254+ specific properties under the ` [pytest] ` section. For basic background on pytest
235255configuration, see the [ pytest docs] ( https://docs.pytest.org/en/latest/customize.html#adding-default-options ) .
236256
237257#### ` db-connection-string `
238258
239259The ` db-connection-string ` property allows the plugin to access a test
240- database. ** This property is required.**
260+ database. ** This property is required.**
241261
242262Example:
243263
@@ -250,9 +270,11 @@ db-connection-string=postgresql://postgres@localhost:5432/pytest_test
250270
251271The ` mocked-engines ` property directs the plugin to [ patch] ( https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch )
252272objects in your codebase, typically SQLAlchemy [ Engine] ( http://docs.sqlalchemy.org/en/latest/core/connections.html#sqlalchemy.engine.Engine )
253- instances, replacing them with the ` db_engine ` fixture in tests where either
254- of the transactional fixtures (` db_session ` or ` db_engine ` ) are listed as dependencies.
255- Objects should be formatted as standard Python import paths, like ` api.database.engine ` .
273+ instances, replacing them with the [ ` db_engine ` fixture] ( #db_engine ) in tests where either
274+ of the transactional fixtures ([ ` db_session ` ] ( #db_session ) or [ ` db_engine ` ] ( #db_engine ) )
275+ are listed as dependencies. The values for this property should be formatted as standard
276+ Python import paths, like ` api.database.engine ` .
277+
256278This property is optional.
257279
258280Example:
@@ -273,12 +295,12 @@ mocked-engines=api.database.engine api.database.second_engine
273295
274296The ` mocked-sessions ` property directs the plugin to [ patch] ( https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch )
275297objects in your codebase, typically SQLAlchemy [ Session] ( http://docs.sqlalchemy.org/en/latest/core/connections.html#sqlalchemy.engine.Engine )
276- instances, replacing them with the ` db_session ` fixture in tests where either
277- of the transactional fixtures (` db_session ` or ` db_engine ` ) are listed as dependencies.
278- Objects should be formatted as standard Python import paths, like ` api.database.db.session ` .
279- This property is optional .
298+ instances, replacing them with the [ ` db_session ` ] ( #db_session ) fixture in tests where either
299+ of the transactional fixtures ([ ` db_session ` ] ( #db_session ) or [ ` db_engine ` ] ( #db_engine ) ) are
300+ listed as dependencies. Values for this property should be formatted as standard Python import
301+ paths, like ` api.database.db.session ` .
280302
281- To patch multiple objects at once, separate the paths with a whitespace .
303+ This property is optional .
282304
283305Example:
284306
@@ -287,16 +309,23 @@ Example:
287309mocked-sessions =api.database.db.session
288310```
289311
312+ To patch multiple objects at once, separate the paths with a whitespace:
313+
314+ ``` ini
315+ [pytest]
316+ mocked-sessions =api.database.db.session api.database.second_db.session
317+ ```
318+
290319#### ` mocked-sessionmakers `
291320
292321The ` mocked-sessionmakers ` property directs the plugin to [ patch] ( https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch )
293322objects in your codebase, typically SQLAlchemy [ sessionmaker] ( http://docs.sqlalchemy.org/en/latest/orm/session_api.html?highlight=sessionmaker#sqlalchemy.orm.session.sessionmaker )
294- factories, replacing them with a mocked class that will return the ` db_session ` fixture
295- in tests where either of the transactional fixtures (` db_session ` or ` db_engine ` )
296- are listed as dependencies. Objects should be formatted as standard Python import paths,
297- like ` api.database.WorkerSessionmaker ` . This property is optional.
323+ factories, replacing them with a mocked class that will return the [ ` db_session ` ] ( #db_session ) fixture
324+ in tests where either of the transactional fixtures ([ ` db_session ` ] ( #db_session ) or [ ` db_engine ` ] ( #db_engine ) )
325+ are listed as dependencies. Values for this property should be formatted as standard Python import paths,
326+ like ` api.database.WorkerSessionmaker ` .
298327
299- To patch multiple objects at once, separate the paths with a whitespace .
328+ This property is optional .
300329
301330Example:
302331
@@ -305,12 +334,19 @@ Example:
305334mocked-sessionmakers =api.database.WorkerSessionmaker
306335```
307336
337+ To patch multiple objects at once, separate the paths with a whitespace.
338+
339+ ``` ini
340+ [pytest]
341+ mocked-sessionmakers =api.database.WorkerSessionmaker api.database.SecondWorkerSessionmaker
342+ ```
343+
308344## Fixtures
309345
310346This plugin provides two fixtures for performing database updates inside nested
311- transactions that get rolled back at the end of a test: ` db_engine ` and
312- ` db_session ` . In addition, the plugin provides a fixture for direct database
313- changes that will not get rolled back: ` module_engine ` . This fixture can be
347+ transactions that get rolled back at the end of a test: [ ` db_session ` ] ( #db_session ) and
348+ [ ` db_engine ` ] ( #db_engine ) . In addition, the plugin provides a fixture for direct database
349+ changes that will not get rolled back: [ ` module_engine ` ] ( #module_engine ) . This fixture can be
314350useful for setting up module- or session-scoped state.
315351
316352### ` db_session `
@@ -320,8 +356,8 @@ rolled back when the test exits. It exposes the same API as [SQLAlchemy's
320356` scoped_session ` object] ( http://docs.sqlalchemy.org/en/latest/orm/contextual.html#sqlalchemy.orm.scoping.scoped_session ) .
321357
322358Listing this fixture as a dependency will activate any mocks that are specified
323- by the configuration properties ` mocked-engines ` , ` mocked-sessions ` , or
324- ` mocked-sessionmakers ` in a configuration file.
359+ by the configuration properties [ ` mocked-engines ` ] ( #mocked-engines ) , [ ` mocked-sessions ` ] ( #mocked-sessions ) ,
360+ or [ ` mocked-sessionmakers ` ] ( #mocked-sessionmakers ) in a configuration file.
325361
326362Example:
327363
@@ -340,7 +376,7 @@ def test_transaction_doesnt_persist(db_session):
340376
341377### ` db_engine `
342378
343- Like ` db_session ` , the ` db_engine ` fixture allows you to perform direct updates
379+ Like [ ` db_session ` ] ( #db_session ) , the ` db_engine ` fixture allows you to perform direct updates
344380against the test database that will be rolled back when the test exits. It is
345381an instance of Python's built-in [ ` MagicMock ` ] ( https://docs.python.org/3/library/unittest.mock.html#unittest.mock.MagicMock )
346382class, with a spec set to match the API of [ SQLAlchemy's
@@ -357,8 +393,9 @@ methods of the `Engine` API can be called, but they will not perform any useful
357393work.
358394
359395Listing this fixture as a dependency will activate any mocks that are specified
360- by the configuration properties ` mocked-engines ` , ` mocked-sessions ` , or
361- ` mocked-sessionmakers ` in a configuration file.
396+ by the configuration properties [ ` mocked-engines ` ] ( #mocked-engines ) ,
397+ [ ` mocked-sessions ` ] ( #mocked-sessions ) , or [ ` mocked-sessionmakers ` ] ( #mocked-sessionmakers )
398+ in a configuration file.
362399
363400Example:
364401
@@ -374,15 +411,15 @@ def test_transaction_doesnt_persist(db_engine):
374411
375412### ` module_engine `
376413
377- In contrast to ` db_session ` and ` db_engine ` , the ` module_engine ` fixture does
378- not wrap its test in a database transaction. Instead, this fixture returns a
379- SQLAlchemy ` Engine ` object that can be used to set up persistent state in tests
380- or fixtures. Its API is identical to the [ SQLAlchemy ` Engine `
381- API] ( http://docs.sqlalchemy.org/en/latest/core/connections.html#sqlalchemy.engine.Engine ) .
414+ In contrast to [ ` db_session ` ] ( #db_session ) and [ ` db_engine ` ] ( #db_engine ) ,
415+ the ` module_engine ` fixture does not wrap its test in a database transaction.
416+ Instead, this fixture returns a SQLAlchemy ` Engine ` object that can be used to
417+ set up persistent state in tests or fixtures. Its API is identical to the
418+ [ SQLAlchemy ` Engine ` API] ( http://docs.sqlalchemy.org/en/latest/core/connections.html#sqlalchemy.engine.Engine ) .
382419
383420Listing this fixture as a dependency ** will not** activate any mocks that are specified
384- by the configuration properties ` mocked-engines ` , ` mocked-sessions ` , or
385- ` mocked-sessionmakers ` in a configuration file.
421+ by the configuration properties [ ` mocked-engines ` ] ( #mocked-engines ) , [ ` mocked-sessions ` ] ( #mocked-sessions ) ,
422+ or [ ` mocked-sessionmakers ` ] ( #mocked-sessionmakers ) in a configuration file.
386423
387424Example:
388425
@@ -399,16 +436,17 @@ def test_module_engine_changes_persist(db_engine):
399436## Using the ` transactional ` mark
400437
401438If you want to enforce transactional context but you don't need to use either
402- of the built-in transactional fixtures (` db_session ` or ` db_engine ` ), you can
403- use the ` **@pytest.mark.transactional** ` decorator to mark that a test should
404- be run inside a transaction.
439+ of the built-in transactional fixtures ([ ` db_session ` ] ( #db_session ) or [ ` db_engine ` ] ( #db_engine ) ),
440+ you can use the ** ` @pytest.mark.transactional ` ** decorator to mark that a test should
441+ be run inside a transaction. For basic background on marks, see the [ pytest
442+ documentation] ( https://docs.pytest.org/en/latest/mark.html ) .
405443
406444Note that since this approach assumes that you'll be performing database
407445updates using connections defined in your app, you ** must** mock the
408- appropriate connections using the configuration properties ` mocked-sessions ` ,
409- ` mocked-engines ` , or ` mocked-sessionmakers ` . The ` transactional ` mark is, in
410- essence, a way of triggering these mocks, so the mocks themselves are
411- important .
446+ appropriate connections using the configuration properties [ ` mocked-sessions ` ] ( #mocked-sessions ) ,
447+ [ ` mocked-engines ` ] ( #mocked-engines ) , or [ ` mocked-sessionmakers ` ] ( #mocked-sessionmakers ) .
448+ The ` transactional ` mark is, in essence, a way of triggering these mocks, so the mocks
449+ themselves are necessary to create the transactional context that you expect .
412450
413451Example:
414452
0 commit comments