From 3a35c4b565374e4971c9c03a044aba4478543cde Mon Sep 17 00:00:00 2001 From: Carel van Dam Date: Fri, 20 Sep 2019 10:37:41 +0200 Subject: [PATCH 1/4] Sphinx Documentation This provides a documentation folder, ./docs, and the linux, Makefile, and windows, make.bat, scripts to build it. The output directory is currently setup as ./build but this can be changed. --- Makefile | 20 +++++ docs/conf.py | 198 +++++++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 20 +++++ make.bat | 36 +++++++++ 4 files changed, 274 insertions(+) create mode 100644 Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 make.bat diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2538762 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = PyTest-Flask-SQLAlchemy +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..83440f1 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'PyTest-Flask-SQLAlchemy' +copyright = '2019, Jean Cochrane' +author = 'Jean Cochrane' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '1.0.2' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode', + 'sphinx.ext.githubpages', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['.templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['.static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'PyTest-Flask-SQLAlchemydoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'PyTest-Flask-SQLAlchemy.tex', 'PyTest-Flask-SQLAlchemy Documentation', + 'Jean Cochrane', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'pytest-flask-sqlalchemy', 'PyTest-Flask-SQLAlchemy Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'PyTest-Flask-SQLAlchemy', 'PyTest-Flask-SQLAlchemy Documentation', + author, 'PyTest-Flask-SQLAlchemy', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project +epub_author = author +epub_publisher = author +epub_copyright = copyright + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} + +# -- Options for todo extension ---------------------------------------------- + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..50a9f16 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,20 @@ +.. PyTest-Flask-SQLAlchemy documentation master file, created by + sphinx-quickstart on Fri Sep 20 10:22:51 2019. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to PyTest-Flask-SQLAlchemy's documentation! +=================================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/make.bat b/make.bat new file mode 100644 index 0000000..4263936 --- /dev/null +++ b/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=docs +set BUILDDIR=build +set SPHINXPROJ=PyTest-Flask-SQLAlchemy + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd From 42c204d8beb40096bb26e852fdb9cce81ce02456 Mon Sep 17 00:00:00 2001 From: Carel van Dam Date: Fri, 20 Sep 2019 10:39:54 +0200 Subject: [PATCH 2/4] Sphinx-Build through Setup script This removes the sphinx build scripts, Makefile and make.bat, in favour of building the documentation through the package setup script, setup.py. --- Makefile | 20 -------------------- make.bat | 36 ------------------------------------ setup.py | 14 ++++++++++++-- 3 files changed, 12 insertions(+), 58 deletions(-) delete mode 100644 Makefile delete mode 100644 make.bat diff --git a/Makefile b/Makefile deleted file mode 100644 index 2538762..0000000 --- a/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = PyTest-Flask-SQLAlchemy -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/make.bat b/make.bat deleted file mode 100644 index 4263936..0000000 --- a/make.bat +++ /dev/null @@ -1,36 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=docs -set BUILDDIR=build -set SPHINXPROJ=PyTest-Flask-SQLAlchemy - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% - -:end -popd diff --git a/setup.py b/setup.py index dac565e..f41f503 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,15 @@ from setuptools import setup +project = 'pytest-flask-sqlalchemy' +release = '1.0.2' +version = '.'.join(release.split('.')[:-1]) def readme(): with open('README.md') as f: return f.read() setup( - name='pytest-flask-sqlalchemy', + name=project, author='Jean Cochrane', author_email='jean@jeancochrane.com', url='https://github.com/jeancochrane/pytest-flask-sqlalchemy', @@ -14,7 +17,7 @@ def readme(): long_description=readme(), long_description_content_type='text/markdown', license='MIT', - version='1.0.2', + version= release, packages=['pytest_flask_sqlalchemy'], install_requires=['pytest>=3.2.1', 'pytest-mock>=1.6.2', @@ -40,4 +43,11 @@ def readme(): 'pytest-flask-sqlalchemy = pytest_flask_sqlalchemy.plugin', ] }, + + # Sphinx + command_options={'build_sphinx': { # Pull this info from a PROJECT.__meta__ module + 'project': ('setup.py', project), + 'version': ('setup.py', version), + 'release': ('setup.py', release), + 'source_dir': ('setup.py', 'docs')}}, ) From 48972306c081069716102ba21cfc9d437afe131c Mon Sep 17 00:00:00 2001 From: Carel van Dam Date: Fri, 20 Sep 2019 14:43:59 +0200 Subject: [PATCH 3/4] Documentation Ported the initial documentation from MarkDown into RST. --- docs/conf.py | 32 ++++-- docs/databases.rst | 11 +++ docs/design.rst | 28 ++++++ docs/fixtures.rst | 82 ++++++++++++++++ docs/index.rst | 40 +++++++- docs/installation.rst | 42 ++++++++ docs/quickstart.rst | 74 ++++++++++++++ docs/utilization.rst | 224 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 522 insertions(+), 11 deletions(-) create mode 100644 docs/databases.rst create mode 100644 docs/design.rst create mode 100644 docs/fixtures.rst create mode 100644 docs/installation.rst create mode 100644 docs/quickstart.rst create mode 100644 docs/utilization.rst diff --git a/docs/conf.py b/docs/conf.py index 83440f1..8677d0b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,19 +12,19 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import os +import sys +sys.path.insert(0, os.path.abspath('../..')) # -- Project information ----------------------------------------------------- project = 'PyTest-Flask-SQLAlchemy' -copyright = '2019, Jean Cochrane' +copyright = '2019, Jean Cochrane and DataMade. Released under the MIT License.' author = 'Jean Cochrane' # The short X.Y version -version = '' +version = '1.0' # The full version, including alpha/beta/rc tags release = '1.0.2' @@ -39,15 +39,21 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + # Cross Referencing + 'sphinx.ext.intersphinx', + 'sphinx.ext.autosectionlabel', # Explicit Referencing e.g. DOCUMENT:SECTION + # Code Documentation + 'sphinx.ext.viewcode', 'sphinx.ext.autodoc', 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', + # Project Management 'sphinx.ext.todo', 'sphinx.ext.coverage', + 'sphinx.ext.githubpages', + # Typography 'sphinx.ext.mathjax', + # Document Control 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', ] # Add any paths that contain templates here, relative to this directory. @@ -77,6 +83,8 @@ # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' +# -- Auto-Section Label Configuration ---------------------------------------- +autosectionlabel_prefix_document = True # -- Options for HTML output ------------------------------------------------- @@ -89,7 +97,13 @@ # further. For a list of options available for each theme, see the # documentation. # -# html_theme_options = {} +html_theme_options = { + # 'logo': 'logo.png', + 'github_user': 'jeancochrane', + 'github_repo': 'pytest-flask-sqlalchemy', + 'github_banner': 'true', + 'travis_button': 'true', +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/docs/databases.rst b/docs/databases.rst new file mode 100644 index 0000000..f78de9e --- /dev/null +++ b/docs/databases.rst @@ -0,0 +1,11 @@ +--------- +Databases +--------- +.. Originally : Supported Back Ends + +So 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 is `planned 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 issue `_ if something isn't working as you expect. + diff --git a/docs/design.rst b/docs/design.rst new file mode 100644 index 0000000..8cc56f8 --- /dev/null +++ b/docs/design.rst @@ -0,0 +1,28 @@ +------ +Design +------ + +Development +=========== + +Running the tests +----------------- + +To run the tests, start by installing a development version of the plugin that includes test dependencies: + +.. code-block:: console + + pip install -e .[tests] + + +Next, export a `database connection string `_ that the tests can use (the database referenced by the string will be created during test setup, so it does not need to exist): + +.. code-block:: console + + export TEST_DATABASE_URL= + +Finally, run the tests using pytest: + +.. code-block:: console + + pytest diff --git a/docs/fixtures.rst b/docs/fixtures.rst new file mode 100644 index 0000000..df5600b --- /dev/null +++ b/docs/fixtures.rst @@ -0,0 +1,82 @@ +-------- +Fixtures +-------- + +This plugin provides two fixtures for performing database updates inside nested transactions that get rolled back at the end of a test: :ref:`db_session ` and :ref:`db_engine `. +The fixtures provide similar functionality, but with different APIs. + +.. _db_session: + +``db_session`` +============== + +The :ref:`db_session ` fixture allows you to perform direct updates that will be rolled back when the test exits. +It exposes the same API as `SQLAlchemy's scoped_session object `_. + +Including this fixture as a function argument of a test will activate any mocks that are defined by the configuration properties :ref:`mocked-engines `, :ref:`mocked-sessions `, or :ref:`mocked-sessionmakers ` in the test configuration file for the duration of that test. + +.. rubric:: Example: + +.. code-block:: Python + + def test_a_transaction(db_session): + row = db_session.query(Table).get(1) + row.name = 'testing' + + db_session.add(row) + db_session.commit() + + def test_transaction_doesnt_persist(db_session): + row = db_session.query(Table).get(1) + assert row.name != 'testing' + + +.. _db_engine: + +``db_engine`` +============= + +Like :ref:`db_session `, the `db_engine` fixture 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-in `MagicMock `_ class, with a spec set to match the API of `SQLAlchemy's Engine `_ object. + +Only a few `Engine` methods 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 `_) + +Since `db_engine` is an instance of `MagicMock` with an `Engine` spec, other methods of the `Engine` API 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 properties :ref:`mocked-engines `, :ref:`mocked-sessions `, or :ref:`mocked-sessionmakers ` in the test configuration file for the duration of that test. + +.. rubric:: Example: + +.. code-block:: Python + + def test_a_transaction_using_engine(db_engine): + with db_engine.begin() as conn: + row = conn.execute('''UPDATE table SET name = 'testing' WHERE id = 1''') + + def test_transaction_doesnt_persist(db_engine): + row_name = db_engine.execute('''SELECT name FROM table WHERE id = 1''').fetchone()[0] + assert row_name != 'testing' + +.. _enabling-transactions-without-fixtures: + +Enabling transactions without fixtures +-------------------------------------- + +If you know you want to make all of your tests transactional, it can be annoying to have to specify one of the :ref:`fixtures ` in every test signature. + +The best way to automatically enable transactions without having to include an extra fixture in every test is to wire up an `autouse fixture `_ for your test suite. +This can be as simple as :: + + # Automatically enable transactions for all tests, without importing any extra fixtures. + @pytest.fixture(autouse=True) + def enable_transactional_tests(db_session): + pass + + +In this configuration, the `enable_transactional_tests` fixture will be automatically used in all tests, meaning that `db_session` will also be used. +This way, all tests will be wrapped in transactions without having to explicitly require either `db_session` or `enable_transactional_tests`. + diff --git a/docs/index.rst b/docs/index.rst index 50a9f16..2765d12 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,14 +3,50 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to PyTest-Flask-SQLAlchemy's documentation! -=================================================== +======================= +PyTest-Flask-SQLAlchemy +======================= + +.. image:: https://badge.fury.io/py/pytest-flask-sqlalchemy.svg + :target: https://badge.fury.io/py/pytest-flask-sqlalchemy .. toctree:: :maxdepth: 2 :caption: Contents: + Quick Start + Install + Usage + Fixtures + Databases + Design + +A `pytest `_ plugin providing fixtures for running tests in transactions using `Flask-SQLAlchemy `_. + +Motivation +---------- + +Inspired by `Django's built-in support for transactional tests `_, this plugin seeks to provide comprehensive, easy-to-use Pytest fixtures for wrapping tests in database transactions for `Flask-SQLAlchemy `_ apps. +The goal is to make testing stateful Flask-SQLAlchemy applications easier by providing fixtures that permit the developer to **make arbitrary database updates with the confidence that any changes made during a test will roll back** once the test exits. + +Acknowledgements +---------------- + +This plugin was initially developed for testing `Dedupe.io `_, a web app for record linkage and entity resolution using machine learning. +Dedupe.io is built and maintained by `DataMade `_. + +The code is greatly indebted to `Alex Michael `_, whose blog post `"Delightful testing with pytest and Flask-SQLAlchemy" `_ helped establish the basic approach on which this plugin builds. + +Many thanks to `Igor 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. + +Copyright +---------------- + +Copyright (c) 2019 Jean Cochrane and DataMade. Released under the MIT License. +Third-party copyright in this distribution is noted where applicable. Indices and tables ================== diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..43ea043 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,42 @@ +------------ +Installation +------------ + +Python package Index +==================== + +Install using pip: + +.. code-block:: console + + pip install pytest-flask-sqlalchemy + + +Once installed, pytest will detect the plugin automatically during test collection. +For basic background on using third-party plugins with pytest, see the `pytest +documentation `_. + +Development +=========== + +Clone the repo from GitHub and switch into the new directory: + +.. code-block:: console + + git clone git@github.com:jeancochrane/pytest-flask-sqlalchemy.git + cd pytest-flask-sqlalchemy + +You can install using pip: + +.. code-block:: console + + pip install . + +Removal +======= + +You can uninstall the package using pip; irrespective of how it was first installed : + +.. code-block:: console + + pip uninstall pytest-flask-sqlalchemy diff --git a/docs/quickstart.rst b/docs/quickstart.rst new file mode 100644 index 0000000..cf7b031 --- /dev/null +++ b/docs/quickstart.rst @@ -0,0 +1,74 @@ +-------------- +Quick examples +-------------- + +Use the :ref:`db_session fixture ` to make **database updates that won't persist beyond the body of the test**:: + + def test_a_transaction(db_session): + row = db_session.query(Table).get(1) + row.name = 'testing' + + db_session.add(row) + db_session.commit() + + def test_transaction_doesnt_persist(db_session): + row = db_session.query(Table).get(1) + assert row.name != 'testing' + +The :ref:`db_engine fixture ` works the same way, but **copies the API of SQLAlchemy's** `Engine object `_:: + + def test_a_transaction_using_engine(db_engine): + with db_engine.begin() as conn: + row = conn.execute('''UPDATE table SET name = 'testing' WHERE id = 1''') + + def test_transaction_doesnt_persist(db_engine): + row_name = db_engine.execute('''SELECT name FROM table WHERE id = 1''').fetchone()[0] + assert row_name != 'testing' + +Use :ref:`configuration properties ` to **mock 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: + +.. code-block:: ini + :caption: :file:`setup.cfg` + :name: configuration + + [tool:pytest] + mocked-sessions=database.db.session + mocked-engines=database.engine + + +.. code-block:: python + :caption: :file:`database.py` + :name: database + + db = flask_sqlalchemy.SQLAlchemy() + engine = sqlalchemy.create_engine('DATABASE_URI') + + +.. code-block:: python + :caption: :file:`models.py` + :name: models + + class Table(db.Model): + __tablename__ = 'table' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(80)) + + def set_name(new_name) + self.name = new_name + db.session.add(self) + db.session.commit() + + +.. code-block:: python + :caption: :file:`tests/test_set_name.py` + :name: test:set name + + def test_set_name(db_session): + row = db_session.query(Table).get(1) + row.set_name('testing') + assert row.name == 'testing' + + def test_transaction_doesnt_persist(db_session): + row = db_session.query(Table).get(1) + assert row.name != 'testing' + diff --git a/docs/utilization.rst b/docs/utilization.rst new file mode 100644 index 0000000..5bdd06e --- /dev/null +++ b/docs/utilization.rst @@ -0,0 +1,224 @@ +----- +Usage +----- + +Configuration +============= + +.. _conftest-setup: + +Conftest Setup +-------------- + +This plugin assumes that a fixture called `_db` has been defined in the root conftest file for your tests. +The `_db` fixture should expose access to a valid SQLAlchemy `Session object `_ that can interact with your database, +for example via the `SQLAlchemy initialization class `_ that configures Flask-SQLAlchemy. + +The fixtures in this plugin depend on this ``_db`` fixture to access your database and create nested transactions to run tests in. +**You must define this fixture in your `conftest.py` file for the plugin to work.** + +An example setup that will produce a valid ``_db`` fixture could look like this (this example comes from the :ref:`test setup ` for this repo): + +.. literalinclude:: ../tests/_conftest.py + :name: test setup:database + :lines: 25-39 + +.. @pytest.fixture(scope='session') +.. def database(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.addfinalizer +.. def drop_database(): +.. drop_postgresql_database(pg_user, pg_host, pg_port, pg_db, 9.6) + +.. literalinclude:: ../tests/_conftest.py + :name: test setup:app + :lines: 42-51 + +.. @pytest.fixture(scope='session') +.. def app(database): +.. ''' +.. Create a Flask app context for the tests. +.. ''' +.. app = Flask(__name__) +.. app.config['SQLALCHEMY_DATABASE_URI'] = DB_CONN +.. return app + + +.. literalinclude:: ../tests/_conftest.py + :name: test setup:_db + :lines: 53-61 + +.. @pytest.fixture(scope='session') +.. def _db(app): +.. ''' +.. Provide the transactional fixtures with access to the database via a Flask-SQLAlchemy +.. database connection. +.. ''' +.. db = SQLAlchemy(app=app) +.. +.. return db + + +Alternatively, if you already have a fixture that sets up database access for +your tests, you can define `_db` to return that fixture directly: + +.. code-block:: + + @pytest.fixture(scope='session') + def database(): + # Set up all your database stuff here + # ... + return db + + @pytest.fixture(scope='session') + def _db(database): + return database + +.. _test-configuration: + +Test configuration +------------------ + +This plugin allows you to configure a few different properties in a :file:`setup.cfg` test configuration file in order to handle the specific database connection needs of your app. +For basic background on setting up pytest configuration files, see the `pytest docs `_. + +All three configuration properties (:ref:`mocked-engines `), :ref:`mocked-sessions `, and :ref:`mocked-sessionmakers `) +work by `patching `_ **one 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 (either :ref:`db_session ` or :ref:`db_engine `) is included in the test function arguments. + +.. _mocked-engines: + +``mocked-engines`` +~~~~~~~~~~~~~~~~~~ + +The `mocked-engines` property directs the plugin to `patch `_ objects in your codebase, typically SQLAlchemy `Engine `_ instances, +replacing them with the :ref:`db_engine fixture ` 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, like `database.engine`. This property is **optional**. + +.. rubric:: Example: + +.. code-block:: Python + :caption: :file:`database.py` + + engine = sqlalchemy.create_engine(DATABASE_URI) + +.. code-block:: ini + :caption: :file:`setup.cfg` + + [tool:pytest] + mocked-engines=database.engine + +To patch multiple objects at once, separate the paths with a whitespace: + +.. code-block:: ini + :caption: :file:`setup.cfg` + + [tool:pytest] + mocked-engines=database.engine database.second_engine + +.. _mocked-sessions: + +``mocked-sessions`` +~~~~~~~~~~~~~~~~~~~ + +The `mocked-sessions` property directs the plugin to `patch `_ objects in your codebase, +typically SQLAlchemy `Session `_ instances, +replacing them with the :ref:`db_session ` fixture 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, like `database.db.session`. This property is **optional**. + +Example: + +.. code-block:: Python + :caption: :file:`database.py` + + db = SQLAlchemy() + +.. code-block:: ini + :caption: :file:`setup.cfg` + + [tool:pytest] + mocked-sessions=database.db.session + +To patch multiple objects at once, separate the paths with a whitespace: + +.. code-block:: ini + :caption: :file:`setup.cfg` + + [tool:pytest] + mocked-sessions=database.db.session database.second_db.session + +.. _mocked-sessionmakers: + +``mocked-sessionmakers`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +The `mocked-sessionmakers` property directs the plugin to `patch `_ objects in your codebase, +typically instances of `SQLAlchemy's sessionmaker factory `_, +replacing them with a mocked class that will return the transactional :ref:`db_session ` fixture. +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, like `database.WorkerSessionmaker`. +This property is **optional**. + +.. rubric:: Example: + +.. code-block:: Python + :caption: :file:`database.py` + + WorkerSessionmaker = sessionmaker() + + +.. code-block:: ini + :caption: :file:`setup.cfg` + + [tool:pytest] + mocked-sessionmakers=database.WorkerSessionmaker + +To patch multiple objects at once, separate the paths with a whitespace. + +.. code-block:: ini + :caption: :file:`setup.cfg` + + [tool:pytest] + mocked-sessionmakers=database.WorkerSessionmaker database.SecondWorkerSessionmaker + +Writing transactional tests +--------------------------- + +Once you have your :ref:`conftest file set up ` and you've :ref:`overrided the necessary connectables in your test configuration `, you're ready to write some transactional tests. +Simply import one of the module's :ref:`transactional fixtures ` in 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 the** :ref:`transactional fixtures ` **provided by this module.** +Tests that do not import the fixture will interact with your database without opening a transaction: + +.. code-block:: Python + + # This test will be wrapped in a transaction. + def transactional_test(db_session): + ... + + # This test **will not** be wrapped in a transaction, since it does not import a + # transactional fixture. + def non_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 the :ref:`fixtures section `. +For guidance on how to automatically enable transactions without having to specify fixtures, +see the section on :ref:`enabling transactions without fixtures `. From 381347fa5d63a48f459abd4978d1a1a587d51f95 Mon Sep 17 00:00:00 2001 From: Carel van Dam Date: Fri, 20 Sep 2019 14:51:28 +0200 Subject: [PATCH 4/4] sphinx folders the sphinx folders .static and .templates are required for sphinx to compile but where ignored by git. --- docs/.static/placeholder.txt | 0 docs/.templates/placeholder.txt | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/.static/placeholder.txt create mode 100644 docs/.templates/placeholder.txt diff --git a/docs/.static/placeholder.txt b/docs/.static/placeholder.txt new file mode 100644 index 0000000..e69de29 diff --git a/docs/.templates/placeholder.txt b/docs/.templates/placeholder.txt new file mode 100644 index 0000000..e69de29