├── .git-blame-ignore-revs ├── .github ├── dependabot.yml └── workflows │ ├── deploy.yml │ └── main.yml ├── .gitignore ├── .readthedocs.yml ├── AUTHORS ├── LICENSE ├── Makefile ├── README.rst ├── codecov.yml ├── docs ├── Makefile ├── _ext │ └── pytestdocs.py ├── changelog.rst ├── conf.py ├── configuring_django.rst ├── contributing.rst ├── database.rst ├── faq.rst ├── helpers.rst ├── index.rst ├── make.bat ├── managing_python_path.rst ├── tutorial.rst └── usage.rst ├── pyproject.toml ├── pytest_django ├── __init__.py ├── asserts.py ├── django_compat.py ├── fixtures.py ├── lazy_django.py ├── live_server_helper.py ├── plugin.py ├── py.typed └── runner.py ├── pytest_django_test ├── __init__.py ├── app │ ├── __init__.py │ ├── fixtures │ │ └── items.json │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── static │ │ └── a_file.txt │ └── views.py ├── db_helpers.py ├── db_router.py ├── settings_base.py ├── settings_mysql.py ├── settings_postgres.py ├── settings_sqlite.py ├── settings_sqlite_file.py ├── urls.py └── urls_overridden.py ├── tests ├── __init__.py ├── conftest.py ├── helpers.py ├── test_asserts.py ├── test_database.py ├── test_db_access_in_repr.py ├── test_db_setup.py ├── test_django_configurations.py ├── test_django_settings_module.py ├── test_doctest.txt ├── test_environment.py ├── test_fixtures.py ├── test_initialization.py ├── test_manage_py_scan.py ├── test_runner.py ├── test_unittest.py ├── test_urls.py └── test_without_django_loaded.py └── tox.ini /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Format code with Ruff 2 | 6939b232a4b204deb3464615d9868db56eb5384a 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | # Set permissions at the job level. 9 | permissions: {} 10 | 11 | jobs: 12 | package: 13 | runs-on: ubuntu-24.04 14 | timeout-minutes: 10 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | 21 | - name: Build and Check Package 22 | uses: hynek/build-and-inspect-python-package@b5076c307dc91924a82ad150cdd1533b444d3310 # v2.12.0 23 | 24 | deploy: 25 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest-django' 26 | needs: [package] 27 | runs-on: ubuntu-24.04 28 | environment: deploy 29 | timeout-minutes: 15 30 | permissions: 31 | contents: read 32 | # For trusted publishing. 33 | id-token: write 34 | 35 | steps: 36 | - name: Download Package 37 | uses: actions/download-artifact@v4 38 | with: 39 | name: Packages 40 | path: dist 41 | 42 | - name: Publish package 43 | uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 44 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | concurrency: 12 | group: ci-main-${{ github.ref }} 13 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 14 | 15 | env: 16 | PYTEST_ADDOPTS: "--color=yes" 17 | 18 | # Set permissions at the job level. 19 | permissions: {} 20 | 21 | jobs: 22 | test: 23 | runs-on: ubuntu-24.04 24 | continue-on-error: ${{ matrix.allow_failure }} 25 | timeout-minutes: 15 26 | permissions: 27 | contents: read 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | persist-credentials: false 32 | 33 | - uses: actions/setup-python@v5 34 | with: 35 | python-version: ${{ matrix.python }} 36 | 37 | - name: Setup mysql 38 | if: contains(matrix.name, 'mysql') 39 | run: | 40 | sudo systemctl start mysql.service 41 | echo "TEST_DB_USER=root" >> $GITHUB_ENV 42 | echo "TEST_DB_PASSWORD=root" >> $GITHUB_ENV 43 | 44 | - name: Setup postgresql 45 | if: contains(matrix.name, 'postgres') 46 | run: | 47 | sudo systemctl start postgresql.service 48 | sudo -u postgres createuser --createdb $USER 49 | 50 | - name: Install dependencies 51 | run: | 52 | python -m pip install --upgrade pip 53 | pip install tox==4.11.1 54 | 55 | - name: Run tox 56 | run: tox -e ${{ matrix.name }} 57 | 58 | - name: Report coverage 59 | if: contains(matrix.name, 'coverage') 60 | uses: codecov/codecov-action@v5 61 | with: 62 | fail_ci_if_error: true 63 | files: ./coverage.xml 64 | token: ${{ secrets.CODECOV_TOKEN }} 65 | 66 | strategy: 67 | fail-fast: false 68 | matrix: 69 | include: 70 | - name: linting,docs 71 | python: '3.13' 72 | allow_failure: false 73 | 74 | # Explicitly test min pytest. 75 | - name: py313-dj52-sqlite-pytestmin-coverage 76 | python: '3.13' 77 | allow_failure: false 78 | 79 | - name: py313-dj52-postgres-xdist-coverage 80 | python: '3.13' 81 | allow_failure: false 82 | 83 | - name: py313-dj51-postgres-xdist-coverage 84 | python: '3.13' 85 | allow_failure: false 86 | 87 | - name: py312-dj42-postgres-xdist-coverage 88 | python: '3.12' 89 | allow_failure: false 90 | 91 | - name: py311-dj50-postgres-xdist-coverage 92 | python: '3.11' 93 | allow_failure: false 94 | 95 | - name: py311-dj42-postgres-xdist-coverage 96 | python: '3.11' 97 | allow_failure: false 98 | 99 | - name: py310-dj52-postgres-xdist-coverage 100 | python: '3.10' 101 | allow_failure: false 102 | 103 | - name: py310-dj51-postgres-xdist-coverage 104 | python: '3.10' 105 | allow_failure: false 106 | 107 | - name: py310-dj42-postgres-xdist-coverage 108 | python: '3.10' 109 | allow_failure: false 110 | 111 | - name: py311-dj51-mysql-coverage 112 | python: '3.11' 113 | allow_failure: false 114 | 115 | - name: py310-dj42-mysql-coverage 116 | python: '3.10' 117 | allow_failure: false 118 | 119 | - name: py39-dj42-mysql-xdist-coverage 120 | python: '3.9' 121 | allow_failure: false 122 | 123 | - name: py313-djmain-sqlite-coverage 124 | python: '3.13' 125 | allow_failure: true 126 | 127 | - name: py313-dj52-sqlite-coverage 128 | python: '3.13' 129 | allow_failure: true 130 | 131 | - name: py312-dj51-sqlite-xdist-coverage 132 | python: '3.12' 133 | allow_failure: false 134 | 135 | - name: py311-dj42-sqlite-xdist-coverage 136 | python: '3.11' 137 | allow_failure: false 138 | 139 | # pypy3: not included with coverage reports (much slower then). 140 | - name: pypy3-dj42-postgres 141 | python: 'pypy3.9' 142 | allow_failure: false 143 | 144 | check: # This job does nothing and is only used for the branch protection 145 | if: always() 146 | 147 | needs: 148 | - test 149 | 150 | runs-on: ubuntu-24.04 151 | 152 | steps: 153 | - name: Decide whether the needed jobs succeeded or failed 154 | uses: re-actors/alls-green@223e4bb7a751b91f43eda76992bcfbf23b8b0302 155 | with: 156 | jobs: ${{ toJSON(needs) }} 157 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | /dist/ 4 | *.egg-info 5 | _build 6 | .tox 7 | .DS_Store 8 | *~ 9 | .env 10 | /.coverage.* 11 | /.coverage 12 | /coverage.xml 13 | /htmlcov/ 14 | .cache 15 | .pytest_cache/ 16 | .Python 17 | .eggs 18 | *.egg 19 | # autogenerated by setuptools-scm 20 | /pytest_django/_version.py 21 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: docs/conf.py 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3" 10 | 11 | python: 12 | install: 13 | - method: pip 14 | path: . 15 | extra_requirements: 16 | - docs 17 | 18 | formats: 19 | - epub 20 | - pdf 21 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Ben Firshman created the original version of pytest-django. 2 | 3 | This project is currently maintained by Ran Benita . 4 | 5 | Previous maintainers are: 6 | 7 | Andreas Pelme 8 | Daniel Hahler 9 | 10 | These people have provided bug fixes, new features, improved the documentation 11 | or just made pytest-django more awesome: 12 | 13 | Ruben Bakker 14 | Ralf Schmitt 15 | Rob Berry 16 | Floris Bruynooghe 17 | Rafal Stozek 18 | Donald Stufft 19 | Nicolas Delaby 20 | Hasan Ramezani 21 | Michael Howitz 22 | Mark Gensler 23 | Pavel Taufer 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | pytest-django is released under the BSD (3-clause) license 2 | ---------------------------------------------------------- 3 | Copyright (c) 2015-2018, pytest-django authors (see AUTHORS file) 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | * The names of its contributors may not be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | 29 | This version of pytest-django is a fork of pytest_django created by Ben Firshman. 30 | --------------------------------------------------------------------------------- 31 | Copyright (c) 2009, Ben Firshman 32 | All rights reserved. 33 | 34 | Redistribution and use in source and binary forms, with or without 35 | modification, are permitted provided that the following conditions are met: 36 | 37 | * Redistributions of source code must retain the above copyright notice, this 38 | list of conditions and the following disclaimer. 39 | * Redistributions in binary form must reproduce the above copyright notice, 40 | this list of conditions and the following disclaimer in the documentation 41 | and/or other materials provided with the distribution. 42 | * The names of its contributors may not be used to endorse or promote products 43 | derived from this software without specific prior written permission. 44 | 45 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 46 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 47 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 48 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 49 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 50 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 51 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 52 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 53 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 54 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 55 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs test clean fix 2 | 3 | test: 4 | tox -e py-dj42-sqlite_file 5 | 6 | docs: 7 | tox -e docs 8 | 9 | fix: 10 | ruff check --fix pytest_django pytest_django_test tests 11 | 12 | clean: 13 | rm -rf bin include/ lib/ man/ pytest_django.egg-info/ build/ 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/pypi/v/pytest-django.svg?style=flat 2 | :alt: PyPI Version 3 | :target: https://pypi.python.org/pypi/pytest-django 4 | 5 | .. image:: https://img.shields.io/pypi/pyversions/pytest-django.svg 6 | :alt: Supported Python versions 7 | :target: https://pypi.python.org/pypi/pytest-django 8 | 9 | .. image:: https://github.com/pytest-dev/pytest-django/workflows/main/badge.svg 10 | :alt: Build Status 11 | :target: https://github.com/pytest-dev/pytest-django/actions 12 | 13 | .. image:: https://img.shields.io/pypi/djversions/pytest-django.svg 14 | :alt: Supported Django versions 15 | :target: https://pypi.org/project/pytest-django/ 16 | 17 | .. image:: https://img.shields.io/codecov/c/github/pytest-dev/pytest-django.svg?style=flat 18 | :alt: Coverage 19 | :target: https://codecov.io/gh/pytest-dev/pytest-django 20 | 21 | Welcome to pytest-django! 22 | ========================= 23 | 24 | pytest-django allows you to test your Django project/applications with the 25 | `pytest testing tool `_. 26 | 27 | * `Quick start / tutorial 28 | `_ 29 | * `Changelog `_ 30 | * Full documentation: https://pytest-django.readthedocs.io/en/latest/ 31 | * `Contribution docs 32 | `_ 33 | * Version compatibility: 34 | 35 | * Django: 4.2, 5.1, 5.2 and latest main branch (compatible at the time 36 | of each release) 37 | * Python: CPython>=3.9 or PyPy 3 38 | * pytest: >=7.0 39 | 40 | For compatibility with older versions, use previous pytest-django releases. 41 | 42 | * Licence: BSD 43 | * `All contributors `_ 44 | * GitHub repository: https://github.com/pytest-dev/pytest-django 45 | * `Issue tracker `_ 46 | * `Python Package Index (PyPI) `_ 47 | 48 | Install pytest-django 49 | --------------------- 50 | 51 | :: 52 | 53 | pip install pytest-django 54 | 55 | Why would I use this instead of Django's `manage.py test` command? 56 | ------------------------------------------------------------------ 57 | 58 | Running your test suite with pytest-django allows you to tap into the features 59 | that are already present in pytest. Here are some advantages: 60 | 61 | * `Manage test dependencies with pytest fixtures. `_ 62 | * Less boilerplate tests: no need to import unittest, create a subclass with methods. Write tests as regular functions. 63 | * Database re-use: no need to re-create the test database for every test run. 64 | * Run tests in multiple processes for increased speed (with the pytest-xdist plugin). 65 | * Make use of other `pytest plugins `_. 66 | * Works with both worlds: Existing unittest-style TestCase's still work without any modifications. 67 | 68 | See the `pytest documentation `_ for more information on pytest itself. 69 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # reference: https://docs.codecov.io/docs/codecovyml-reference 2 | coverage: 3 | status: 4 | patch: true 5 | project: false 6 | comment: false 7 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= "" 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_ext/pytestdocs.py: -------------------------------------------------------------------------------- 1 | def setup(app): 2 | app.add_crossref_type( 3 | directivename="fixture", 4 | rolename="fixture", 5 | indextemplate="pair: %s; fixture", 6 | ) 7 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import sys 4 | 5 | 6 | # If extensions (or modules to document with autodoc) are in another directory, 7 | # add these directories to sys.path here. If the directory is relative to the 8 | # documentation root, use os.path.abspath to make it absolute, like shown here. 9 | 10 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "_ext"))) 11 | 12 | # Add any Sphinx extension module names here, as strings. They can be extensions 13 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 14 | extensions = [ 15 | "sphinx.ext.doctest", 16 | "sphinx.ext.intersphinx", 17 | "pytestdocs", 18 | ] 19 | 20 | # Add any paths that contain templates here, relative to this directory. 21 | templates_path = ["_templates"] 22 | 23 | # The suffix of source filenames. 24 | source_suffix = ".rst" 25 | 26 | # The master toctree document. 27 | master_doc = "index" 28 | 29 | # General information about the project. 30 | project = "pytest-django" 31 | copyright = ( 32 | f"{datetime.datetime.now(tz=datetime.timezone.utc).year}, Andreas Pelme and contributors" 33 | ) 34 | 35 | exclude_patterns = ["_build"] 36 | 37 | pygments_style = "sphinx" 38 | 39 | html_theme = "sphinx_rtd_theme" 40 | 41 | # Output file base name for HTML help builder. 42 | htmlhelp_basename = "pytest-djangodoc" 43 | 44 | intersphinx_mapping = { 45 | "python": ("https://docs.python.org/3", None), 46 | "django": ( 47 | "https://docs.djangoproject.com/en/stable/", 48 | "https://docs.djangoproject.com/en/stable/_objects/", 49 | ), 50 | "pytest": ("https://docs.pytest.org/en/stable/", None), 51 | } 52 | 53 | # Warn about all references where the target cannot be found 54 | nitpicky = True 55 | 56 | 57 | def setup(app): 58 | # Allow linking to pytest's confvals. 59 | app.add_object_type( 60 | "confval", 61 | "pytest-confval", 62 | objname="configuration value", 63 | indextemplate="pair: %s; configuration value", 64 | ) 65 | -------------------------------------------------------------------------------- /docs/configuring_django.rst: -------------------------------------------------------------------------------- 1 | .. _configuring_django_settings: 2 | 3 | Configuring Django settings 4 | =========================== 5 | 6 | There are a couple of different ways Django settings can be provided for 7 | the tests. 8 | 9 | The environment variable ``DJANGO_SETTINGS_MODULE`` 10 | --------------------------------------------------- 11 | 12 | Running the tests with ``DJANGO_SETTINGS_MODULE`` defined will find the 13 | Django settings the same way Django does by default. 14 | 15 | Example:: 16 | 17 | $ export DJANGO_SETTINGS_MODULE=test.settings 18 | $ pytest 19 | 20 | or:: 21 | 22 | $ DJANGO_SETTINGS_MODULE=test.settings pytest 23 | 24 | 25 | Command line option ``--ds=SETTINGS`` 26 | ------------------------------------- 27 | 28 | Example:: 29 | 30 | $ pytest --ds=test.settings 31 | 32 | 33 | ``pytest.ini`` settings 34 | ----------------------- 35 | 36 | Example contents of pytest.ini:: 37 | 38 | [pytest] 39 | DJANGO_SETTINGS_MODULE = test.settings 40 | 41 | ``pyproject.toml`` settings 42 | --------------------------- 43 | 44 | Example contents of pyproject.toml:: 45 | 46 | [tool.pytest.ini_options] 47 | DJANGO_SETTINGS_MODULE = "test.settings" 48 | 49 | Order of choosing settings 50 | -------------------------- 51 | 52 | The order of precedence is, from highest to lowest: 53 | 54 | * The command line option ``--ds`` 55 | * The environment variable ``DJANGO_SETTINGS_MODULE`` 56 | * The ``DJANGO_SETTINGS_MODULE`` option in the configuration file - 57 | ``pytest.ini``, or other file that Pytest finds such as ``tox.ini`` or ``pyproject.toml`` 58 | 59 | If you want to use the highest precedence in the configuration file, you can 60 | use ``addopts = --ds=yourtestsettings``. 61 | 62 | Using django-configurations 63 | --------------------------- 64 | 65 | There is support for using `django-configurations `_. 66 | 67 | To do so configure the settings class using an environment variable, the 68 | ``--dc`` flag, ``pytest.ini`` option ``DJANGO_CONFIGURATION`` or ``pyproject.toml`` option ``DJANGO_CONFIGURATION``. 69 | 70 | Environment Variable:: 71 | 72 | $ export DJANGO_CONFIGURATION=MySettings 73 | $ pytest 74 | 75 | Command Line Option:: 76 | 77 | $ pytest --dc=MySettings 78 | 79 | INI File Contents:: 80 | 81 | [pytest] 82 | DJANGO_CONFIGURATION=MySettings 83 | 84 | pyproject.toml File Contents:: 85 | 86 | [tool.pytest.ini_options] 87 | DJANGO_CONFIGURATION = "MySettings" 88 | 89 | Using ``django.conf.settings.configure()`` 90 | ------------------------------------------ 91 | 92 | In case there is no ``DJANGO_SETTINGS_MODULE``, the ``settings`` object can be 93 | created by calling ``django.conf.settings.configure()``. 94 | 95 | This can be done from your project's ``conftest.py`` file:: 96 | 97 | from django.conf import settings 98 | 99 | def pytest_configure(): 100 | settings.configure(DATABASES=...) 101 | 102 | Overriding individual settings 103 | ------------------------------ 104 | 105 | Settings can be overridden by using the :fixture:`settings` fixture:: 106 | 107 | @pytest.fixture(autouse=True) 108 | def use_dummy_cache_backend(settings): 109 | settings.CACHES = { 110 | "default": { 111 | "BACKEND": "django.core.cache.backends.dummy.DummyCache", 112 | } 113 | } 114 | 115 | Here `autouse=True` is used, meaning the fixture is automatically applied to all tests, 116 | but it can also be requested individually per-test. 117 | 118 | Changing your app before Django gets set up 119 | ------------------------------------------- 120 | 121 | pytest-django calls :func:`django.setup` automatically. If you want to do 122 | anything before this, you have to create a pytest plugin and use 123 | the :func:`~_pytest.hookspec.pytest_load_initial_conftests` hook, with 124 | ``tryfirst=True``, so that it gets run before the hook in pytest-django 125 | itself:: 126 | 127 | @pytest.hookimpl(tryfirst=True) 128 | def pytest_load_initial_conftests(early_config, parser, args): 129 | import project.app.signals 130 | 131 | def noop(*args, **kwargs): 132 | pass 133 | 134 | project.app.signals.something = noop 135 | 136 | This plugin can then be used e.g. via ``-p`` in :pytest-confval:`addopts`. 137 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | ############################# 2 | Contributing to pytest-django 3 | ############################# 4 | 5 | Like every open-source project, pytest-django is always looking for motivated 6 | individuals to contribute to its source code. However, to ensure the highest 7 | code quality and keep the repository nice and tidy, everybody has to follow a 8 | few rules (nothing major, I promise :) ) 9 | 10 | 11 | ********* 12 | Community 13 | ********* 14 | 15 | The fastest way to get feedback on contributions/bugs is usually to open an 16 | issue in the `issue tracker`_. 17 | 18 | Discussions also happen via IRC in #pytest `on irc.libera.chat 19 | `_ (join using an IRC client, `via webchat 20 | `_, or `via Matrix 21 | `_). 22 | You may also be interested in following `@andreaspelme`_ on Twitter. 23 | 24 | ************* 25 | In a nutshell 26 | ************* 27 | 28 | Here's what the contribution process looks like, in a bullet-points fashion: 29 | 30 | #. pytest-django is hosted on `GitHub`_, at 31 | https://github.com/pytest-dev/pytest-django 32 | #. The best method to contribute back is to create an account there and fork 33 | the project. You can use this fork as if it was your own project, and should 34 | push your changes to it. 35 | #. When you feel your code is good enough for inclusion, "send us a `pull 36 | request`_", by using the nice GitHub web interface. 37 | 38 | 39 | ***************** 40 | Contributing Code 41 | ***************** 42 | 43 | 44 | Getting the source code 45 | ======================= 46 | 47 | - Code will be reviewed and tested by at least one core developer, preferably 48 | by several. Other community members are welcome to give feedback. 49 | - Code *must* be tested. Your pull request should include unit-tests (that 50 | cover the piece of code you're submitting, obviously). 51 | - Documentation should reflect your changes if relevant. There is nothing worse 52 | than invalid documentation. 53 | - Usually, if unit tests are written, pass, and your change is relevant, then 54 | your pull request will be merged. 55 | 56 | Since we're hosted on GitHub, pytest-django uses `git`_ as a version control 57 | system. 58 | 59 | The `GitHub help`_ is very well written and will get you started on using git 60 | and GitHub in a jiffy. It is an invaluable resource for newbies and oldtimers 61 | alike. 62 | 63 | 64 | Syntax and conventions 65 | ====================== 66 | 67 | We try to conform to `PEP8`_ as much as possible. A few highlights: 68 | 69 | - Indentation should be exactly 4 spaces. Not 2, not 6, not 8. **4**. Also, 70 | tabs are evil. 71 | - We try (loosely) to keep the line length at 79 characters. Generally the rule 72 | is "it should look good in a terminal-based editor" (eg vim), but we try not 73 | be [Godwin's law] about it. 74 | 75 | 76 | Process 77 | ======= 78 | 79 | This is how you fix a bug or add a feature: 80 | 81 | #. `fork`_ the repository on GitHub. 82 | #. Checkout your fork. 83 | #. Hack hack hack, test test test, commit commit commit, test again. 84 | #. Push to your fork. 85 | #. Open a pull request. 86 | 87 | 88 | Tests 89 | ===== 90 | 91 | Having a wide and comprehensive library of unit-tests and integration tests is 92 | of exceeding importance. Contributing tests is widely regarded as a very 93 | prestigious contribution (you're making everybody's future work much easier by 94 | doing so). Good karma for you. Cookie points. Maybe even a beer if we meet in 95 | person :) 96 | 97 | Generally tests should be: 98 | 99 | - Unitary (as much as possible). I.E. should test as much as possible only on 100 | one function/method/class. That's the very definition of unit tests. 101 | Integration tests are also interesting obviously, but require more time to 102 | maintain since they have a higher probability of breaking. 103 | - Short running. No hard numbers here, but if your one test doubles the time it 104 | takes for everybody to run them, it's probably an indication that you're 105 | doing it wrong. 106 | 107 | In a similar way to code, pull requests will be reviewed before pulling 108 | (obviously), and we encourage discussion via code review (everybody learns 109 | something this way) or in the IRC channel. 110 | 111 | Running the tests 112 | ----------------- 113 | 114 | There is a Makefile in the repository which aids in setting up a virtualenv 115 | and running the tests:: 116 | 117 | $ make test 118 | 119 | You can manually create the virtualenv using:: 120 | 121 | $ make testenv 122 | 123 | This will install a virtualenv with pytest and the latest stable version of 124 | Django. The virtualenv can then be activated with:: 125 | 126 | $ source bin/activate 127 | 128 | Then, simply invoke pytest to run the test suite:: 129 | 130 | $ pytest --ds=pytest_django_test.settings_sqlite 131 | 132 | 133 | tox can be used to run the test suite under different configurations by 134 | invoking:: 135 | 136 | $ tox 137 | 138 | There is a huge number of unique test configurations (98 at the time of 139 | writing), running them all will take a long time. All valid configurations can 140 | be found in `tox.ini`. To test against a few of them, invoke tox with the `-e` 141 | flag:: 142 | 143 | $ tox -e py39-dj42-postgres,py310-dj52-mysql 144 | 145 | This will run the tests on Python 3.9/Django 4.2/PostgeSQL and Python 146 | 3.10/Django 5.2/MySQL. 147 | 148 | 149 | Measuring test coverage 150 | ----------------------- 151 | 152 | Some of the tests are executed in subprocesses. Because of that regular 153 | coverage measurements (using pytest-cov plugin) are not reliable. 154 | 155 | If you want to measure coverage you'll need to create .pth file as described in 156 | `subprocess section of coverage documentation`_. If you're using 157 | editable mode you should uninstall pytest_django (using pip) 158 | for the time of measuring coverage. 159 | 160 | You'll also need mysql and postgres databases. There are predefined settings 161 | for each database in the tests directory. You may want to modify these files 162 | but please don't include them in your pull requests. 163 | 164 | After this short initial setup you're ready to run tests:: 165 | 166 | $ COVERAGE_PROCESS_START=`pwd`/pyproject.toml COVERAGE_FILE=`pwd`/.coverage pytest --ds=pytest_django_test.settings_postgres 167 | 168 | You should repeat the above step for sqlite and mysql before the next step. 169 | This step will create a lot of ``.coverage`` files with additional suffixes for 170 | every process. 171 | 172 | The final step is to combine all the files created by different processes and 173 | generate the html coverage report:: 174 | 175 | $ coverage combine 176 | $ coverage html 177 | 178 | Your coverage report is now ready in the ``htmlcov`` directory. 179 | 180 | 181 | Continuous integration 182 | ---------------------- 183 | 184 | `GitHub Actions`_ is used to automatically run all tests against all supported versions 185 | of Python, Django and different database backends. 186 | 187 | The `pytest-django Actions`_ page shows the latest test run. The CI will 188 | automatically pick up pull requests, test them and report the result directly 189 | in the pull request. 190 | 191 | ************************** 192 | Contributing Documentation 193 | ************************** 194 | 195 | Perhaps considered "boring" by hard-core coders, documentation is sometimes 196 | even more important than code! This is what brings fresh blood to a project, 197 | and serves as a reference for oldtimers. On top of this, documentation is the 198 | one area where less technical people can help most - you just need to write a 199 | semi-decent English. People need to understand you. We don't care about style 200 | or correctness. 201 | 202 | Documentation should be: 203 | 204 | - We use `Sphinx`_/`restructuredText`_. So obviously this is the format you 205 | should use :) File extensions should be .rst. 206 | - Written in English. We can discuss how it would bring more people to the 207 | project to have a Klingon translation or anything, but that's a problem we 208 | will ask ourselves when we already have a good documentation in English. 209 | - Accessible. You should assume the reader to be moderately familiar with 210 | Python and Django, but not anything else. Link to documentation of libraries 211 | you use, for example, even if they are "obvious" to you (South is the first 212 | example that comes to mind - it's obvious to any Django programmer, but not 213 | to any newbie at all). 214 | A brief description of what it does is also welcome. 215 | 216 | Pulling of documentation is pretty fast and painless. Usually somebody goes 217 | over your text and merges it, since there are no "breaks" and that GitHub 218 | parses rst files automagically it's really convenient to work with. 219 | 220 | Also, contributing to the documentation will earn you great respect from the 221 | core developers. You get good karma just like a test contributor, but you get 222 | double cookie points. Seriously. You rock. 223 | 224 | 225 | .. note:: 226 | 227 | This very document is based on the contributing docs of the `django CMS`_ 228 | project. Many thanks for allowing us to steal it! 229 | 230 | 231 | .. _fork: https://github.com/pytest-dev/pytest-django 232 | .. _issue tracker: https://github.com/pytest-dev/pytest-django/issues 233 | .. _Sphinx: https://www.sphinx-doc.org/ 234 | .. _PEP8: https://www.python.org/dev/peps/pep-0008/ 235 | .. _GitHub : https://www.github.com 236 | .. _GitHub help : https://help.github.com 237 | .. _freenode : https://freenode.net/ 238 | .. _@andreaspelme : https://twitter.com/andreaspelme 239 | .. _pull request : https://help.github.com/send-pull-requests/ 240 | .. _git : https://git-scm.com/ 241 | .. _restructuredText: https://docutils.sourceforge.io/docs/ref/rst/introduction.html 242 | .. _django CMS: https://www.django-cms.org/ 243 | .. _GitHub Actions: https://github.com/features/actions 244 | .. _pytest-django Actions: https://github.com/pytest-dev/pytest-django/actions 245 | .. _`subprocess section of coverage documentation`: https://coverage.readthedocs.io/en/latest/subprocess.html 246 | -------------------------------------------------------------------------------- /docs/database.rst: -------------------------------------------------------------------------------- 1 | Database access 2 | =============== 3 | 4 | ``pytest-django`` takes a conservative approach to enabling database 5 | access. By default your tests will fail if they try to access the 6 | database. Only if you explicitly request database access will this be 7 | allowed. This encourages you to keep database-needing tests to a 8 | minimum which makes it very clear what code uses the database. 9 | 10 | Enabling database access in tests 11 | --------------------------------- 12 | 13 | You can use :ref:`pytest marks ` to tell ``pytest-django`` your 14 | test needs database access:: 15 | 16 | import pytest 17 | 18 | @pytest.mark.django_db 19 | def test_my_user(): 20 | me = User.objects.get(username='me') 21 | assert me.is_superuser 22 | 23 | It is also possible to mark all tests in a class or module at once. 24 | This demonstrates all the ways of marking, even though they overlap. 25 | Just one of these marks would have been sufficient. See the :ref:`pytest 26 | documentation ` for detail:: 27 | 28 | import pytest 29 | 30 | pytestmark = pytest.mark.django_db 31 | 32 | @pytest.mark.django_db 33 | class TestUsers: 34 | pytestmark = pytest.mark.django_db 35 | def test_my_user(self): 36 | me = User.objects.get(username='me') 37 | assert me.is_superuser 38 | 39 | 40 | By default ``pytest-django`` will set up Django databases the 41 | first time a test needs them. Once setup, a database is cached to be 42 | used for all subsequent tests and rolls back transactions, to isolate 43 | tests from each other. This is the same way the standard Django 44 | :class:`~django.test.TestCase` uses the database. However 45 | ``pytest-django`` also caters for transaction test cases and allows 46 | you to keep the test databases configured across different test runs. 47 | 48 | 49 | Testing transactions 50 | -------------------- 51 | 52 | Django itself has the :class:`~django.test.TransactionTestCase` which 53 | allows you to test transactions and will flush the database between 54 | tests to isolate them. The downside of this is that these tests are 55 | much slower to set up due to the required flushing of the database. 56 | ``pytest-django`` also supports this style of tests, which you can 57 | select using an argument to the ``django_db`` mark:: 58 | 59 | @pytest.mark.django_db(transaction=True) 60 | def test_spam(): 61 | pass # test relying on transactions 62 | 63 | .. _`multi-db`: 64 | 65 | Tests requiring multiple databases 66 | ---------------------------------- 67 | 68 | .. versionadded:: 4.3 69 | 70 | ``pytest-django`` has support for multi-database configurations using the 71 | ``databases`` argument to the :func:`django_db ` mark:: 72 | 73 | @pytest.mark.django_db(databases=['default', 'other']) 74 | def test_spam(): 75 | assert MyModel.objects.using('other').count() == 0 76 | 77 | If you don't specify ``databases``, only the default database is requested. 78 | To request all databases, you may use the shortcut ``'__all__'``. 79 | 80 | For details see :attr:`django.test.TransactionTestCase.databases` and 81 | :attr:`django.test.TestCase.databases`. 82 | 83 | 84 | ``--reuse-db`` - reuse the testing database between test runs 85 | -------------------------------------------------------------- 86 | Using ``--reuse-db`` will create the test database in the same way as 87 | ``manage.py test`` usually does. 88 | 89 | However, after the test run, the test database will not be removed. 90 | 91 | The next time a test run is started with ``--reuse-db``, the database will 92 | instantly be re used. This will allow much faster startup time for tests. 93 | 94 | This can be especially useful when running a few tests, when there are a lot 95 | of database tables to set up. 96 | 97 | ``--reuse-db`` will not pick up schema changes between test runs. You must run 98 | the tests with ``--reuse-db --create-db`` to re-create the database according 99 | to the new schema. Running without ``--reuse-db`` is also possible, since the 100 | database will automatically be re-created. 101 | 102 | 103 | ``--create-db`` - force re creation of the test database 104 | -------------------------------------------------------- 105 | When used with ``--reuse-db``, this option will re-create the database, 106 | regardless of whether it exists or not. 107 | 108 | Example work flow with ``--reuse-db`` and ``--create-db``. 109 | ----------------------------------------------------------- 110 | A good way to use ``--reuse-db`` and ``--create-db`` can be: 111 | 112 | * Put ``--reuse-db`` in your default options (in your project's ``pytest.ini`` file):: 113 | 114 | [pytest] 115 | addopts = --reuse-db 116 | 117 | * Just run tests with ``pytest``, on the first run the test database will be 118 | created. The next test run it will be reused. 119 | 120 | * When you alter your database schema, run ``pytest --create-db``, to force 121 | re-creation of the test database. 122 | 123 | ``--no-migrations`` - Disable Django migrations 124 | ----------------------------------------------- 125 | 126 | Using ``--no-migrations`` (alias: ``--nomigrations``) will disable Django migrations and create the database 127 | by inspecting all models. It may be faster when there are several migrations to 128 | run in the database setup. You can use ``--migrations`` to force running 129 | migrations in case ``--no-migrations`` is used, e.g. in ``pyproject.toml``. 130 | 131 | .. _advanced-database-configuration: 132 | 133 | Advanced database configuration 134 | ------------------------------- 135 | 136 | pytest-django provides options to customize the way database is configured. The 137 | default database construction mostly follows Django's own test runner. You can 138 | however influence all parts of the database setup process to make it fit in 139 | projects with special requirements. 140 | 141 | This section assumes some familiarity with the Django test runner, Django 142 | database creation and pytest fixtures. 143 | 144 | Fixtures 145 | ######## 146 | 147 | There are some fixtures which will let you change the way the database is 148 | configured in your own project. These fixtures can be overridden in your own 149 | project by specifying a fixture with the same name and scope in ``conftest.py``. 150 | 151 | .. admonition:: Use the pytest-django source code 152 | 153 | The default implementation of these fixtures can be found in 154 | `fixtures.py `_. 155 | 156 | The code is relatively short and straightforward and can provide a 157 | starting point when you need to customize database setup in your own 158 | project. 159 | 160 | 161 | django_db_setup 162 | """"""""""""""" 163 | 164 | .. fixture:: django_db_setup 165 | 166 | This is the top-level fixture that ensures that the test databases are created 167 | and available. This fixture is session scoped (it will be run once per test 168 | session) and is responsible for making sure the test database is available for tests 169 | that need it. 170 | 171 | The default implementation creates the test database by applying migrations and removes 172 | databases after the test run. 173 | 174 | You can override this fixture in your own ``conftest.py`` to customize how test 175 | databases are constructed. 176 | 177 | django_db_modify_db_settings 178 | """""""""""""""""""""""""""" 179 | 180 | .. fixture:: django_db_modify_db_settings 181 | 182 | This fixture allows modifying 183 | `django.conf.settings.DATABASES `_ 184 | just before the databases are configured. 185 | 186 | If you need to customize the location of your test database, this is the 187 | fixture you want to override. 188 | 189 | The default implementation of this fixture requests the 190 | :fixture:`django_db_modify_db_settings_parallel_suffix` to provide compatibility 191 | with pytest-xdist. 192 | 193 | This fixture is by default requested from :fixture:`django_db_setup`. 194 | 195 | django_db_modify_db_settings_parallel_suffix 196 | """""""""""""""""""""""""""""""""""""""""""" 197 | 198 | .. fixture:: django_db_modify_db_settings_parallel_suffix 199 | 200 | Requesting this fixture will add a suffix to the database name when the tests 201 | are run via `pytest-xdist`, or via `tox` in parallel mode. 202 | 203 | This fixture is by default requested from 204 | :fixture:`django_db_modify_db_settings`. 205 | 206 | django_db_modify_db_settings_tox_suffix 207 | """"""""""""""""""""""""""""""""""""""" 208 | 209 | .. fixture:: django_db_modify_db_settings_tox_suffix 210 | 211 | Requesting this fixture will add a suffix to the database name when the tests 212 | are run via `tox` in parallel mode. 213 | 214 | This fixture is by default requested from 215 | :fixture:`django_db_modify_db_settings_parallel_suffix`. 216 | 217 | django_db_modify_db_settings_xdist_suffix 218 | """"""""""""""""""""""""""""""""""""""""" 219 | 220 | .. fixture:: django_db_modify_db_settings_xdist_suffix 221 | 222 | Requesting this fixture will add a suffix to the database name when the tests 223 | are run via `pytest-xdist`. 224 | 225 | This fixture is by default requested from 226 | :fixture:`django_db_modify_db_settings_parallel_suffix`. 227 | 228 | django_db_use_migrations 229 | """""""""""""""""""""""" 230 | 231 | .. fixture:: django_db_use_migrations 232 | 233 | Returns whether or not to use migrations to create the test 234 | databases. 235 | 236 | The default implementation returns the value of the 237 | ``--migrations``/``--no-migrations`` command line options. 238 | 239 | This fixture is by default requested from :fixture:`django_db_setup`. 240 | 241 | django_db_keepdb 242 | """""""""""""""" 243 | 244 | .. fixture:: django_db_keepdb 245 | 246 | Returns whether or not to re-use an existing database and to keep it after the 247 | test run. 248 | 249 | The default implementation handles the ``--reuse-db`` and ``--create-db`` 250 | command line options. 251 | 252 | This fixture is by default requested from :fixture:`django_db_setup`. 253 | 254 | django_db_createdb 255 | """""""""""""""""" 256 | 257 | .. fixture:: django_db_createdb 258 | 259 | Returns whether or not the database is to be re-created before running any 260 | tests. 261 | 262 | This fixture is by default requested from :fixture:`django_db_setup`. 263 | 264 | django_db_blocker 265 | """"""""""""""""" 266 | 267 | .. fixture:: django_db_blocker 268 | 269 | .. warning:: 270 | It does not manage transactions and changes made to the database will not 271 | be automatically restored. Using the ``pytest.mark.django_db`` marker 272 | or :fixture:`db` fixture, which wraps database changes in a transaction and 273 | restores the state is generally the thing you want in tests. This marker 274 | can be used when you are trying to influence the way the database is 275 | configured. 276 | 277 | Database access is by default not allowed. ``django_db_blocker`` is the object 278 | which can allow specific code paths to have access to the database. This 279 | fixture is used internally to implement the ``db`` fixture. 280 | 281 | 282 | :fixture:`django_db_blocker` can be used as a context manager to enable database 283 | access for the specified block:: 284 | 285 | @pytest.fixture 286 | def myfixture(django_db_blocker): 287 | with django_db_blocker.unblock(): 288 | ... # modify something in the database 289 | 290 | You can also manage the access manually via these methods: 291 | 292 | .. py:class:: pytest_django.DjangoDbBlocker 293 | 294 | .. py:method:: django_db_blocker.unblock() 295 | 296 | Enable database access. Should be followed by a call to 297 | :func:`~django_db_blocker.restore` or used as a context manager. 298 | 299 | .. py:method:: django_db_blocker.block() 300 | 301 | Disable database access. Should be followed by a call to 302 | :func:`~django_db_blocker.restore` or used as a context manager. 303 | 304 | .. py:method:: django_db_blocker.restore() 305 | 306 | Restore the previous state of the database blocking. 307 | 308 | Examples 309 | ######## 310 | 311 | Using a template database for tests 312 | """"""""""""""""""""""""""""""""""" 313 | 314 | This example shows how a pre-created PostgreSQL source database can be copied 315 | and used for tests. 316 | 317 | Put this into ``conftest.py``:: 318 | 319 | import pytest 320 | from django.db import connections 321 | 322 | import psycopg 323 | 324 | 325 | def run_sql(sql): 326 | with psycopg.connect(database='postgres') as conn: 327 | conn.execute(sql) 328 | 329 | 330 | @pytest.fixture(scope='session') 331 | def django_db_setup(): 332 | from django.conf import settings 333 | 334 | settings.DATABASES['default']['NAME'] = 'the_copied_db' 335 | 336 | run_sql('DROP DATABASE IF EXISTS the_copied_db') 337 | run_sql('CREATE DATABASE the_copied_db TEMPLATE the_source_db') 338 | 339 | yield 340 | 341 | for connection in connections.all(): 342 | connection.close() 343 | 344 | run_sql('DROP DATABASE the_copied_db') 345 | 346 | 347 | Using an existing, external database for tests 348 | """""""""""""""""""""""""""""""""""""""""""""" 349 | 350 | This example shows how you can connect to an existing database and use it for 351 | your tests. This example is trivial, you just need to disable all of 352 | pytest-django and Django's test database creation and point to the existing 353 | database. This is achieved by simply implementing a no-op 354 | :fixture:`django_db_setup` fixture. 355 | 356 | Put this into ``conftest.py``:: 357 | 358 | import pytest 359 | 360 | 361 | @pytest.fixture(scope='session') 362 | def django_db_setup(): 363 | from django.conf import settings 364 | 365 | settings.DATABASES['default'] = { 366 | 'ENGINE': 'django.db.backends.mysql', 367 | 'HOST': 'db.example.com', 368 | 'NAME': 'external_db', 369 | } 370 | 371 | 372 | Populate the database with initial test data 373 | """""""""""""""""""""""""""""""""""""""""""" 374 | 375 | In some cases you want to populate the test database before you start the 376 | tests. Because of different ways you may use the test database, there are 377 | different ways to populate it. 378 | 379 | Populate the test database if you don't use transactional or live_server 380 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 381 | 382 | If you are using the :func:`pytest.mark.django_db` marker or :fixture:`db` 383 | fixture, you probably don't want to explicitly handle transactions in your 384 | tests. In this case, it is sufficient to populate your database only 385 | once. You can put code like this in ``conftest.py``:: 386 | 387 | import pytest 388 | 389 | from django.core.management import call_command 390 | 391 | @pytest.fixture(scope='session') 392 | def django_db_setup(django_db_setup, django_db_blocker): 393 | with django_db_blocker.unblock(): 394 | call_command('loaddata', 'my_fixture.json') 395 | 396 | This loads the Django fixture ``my_fixture.json`` once for the entire test 397 | session. This data will be available to tests marked with the 398 | :func:`pytest.mark.django_db` mark, or tests which use the :fixture:`db` 399 | fixture. The test data will be saved in the database and will not be reset. 400 | This example uses Django's fixture loading mechanism, but it can be replaced 401 | with any way of loading data into the database. 402 | 403 | Notice :fixture:`django_db_setup` in the argument list. This triggers the 404 | original pytest-django fixture to create the test database, so that when 405 | ``call_command`` is invoked, the test database is already prepared and 406 | configured. 407 | 408 | Populate the test database if you use transactional or live_server 409 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 410 | 411 | In case you use transactional tests (you use the :func:`pytest.mark.django_db` 412 | marker with ``transaction=True``, or the :fixture:`transactional_db` fixture), 413 | you need to repopulate your database every time a test starts, because the 414 | database is cleared between tests. 415 | 416 | The :fixture:`live_server` fixture uses :fixture:`transactional_db`, so you 417 | also need to populate the test database this way when using it. 418 | 419 | You can put this code into ``conftest.py``. Note that while it it is similar to 420 | the previous one, the scope is changed from ``session`` to ``function``:: 421 | 422 | import pytest 423 | 424 | from myapp.models import Widget 425 | 426 | @pytest.fixture(scope='function') 427 | def django_db_setup(django_db_setup, django_db_blocker): 428 | with django_db_blocker.unblock(): 429 | Widget.objects.create(...) 430 | 431 | 432 | Use the same database for all xdist processes 433 | """"""""""""""""""""""""""""""""""""""""""""" 434 | 435 | By default, each xdist process gets its own database to run tests on. This is 436 | needed to have transactional tests that do not interfere with each other. 437 | 438 | If you instead want your tests to use the same database, override the 439 | :fixture:`django_db_modify_db_settings` to not do anything. Put this in 440 | ``conftest.py``:: 441 | 442 | import pytest 443 | 444 | 445 | @pytest.fixture(scope='session') 446 | def django_db_modify_db_settings(): 447 | pass 448 | 449 | Randomize database sequences 450 | """""""""""""""""""""""""""" 451 | 452 | You can customize the test database after it has been created by extending the 453 | :fixture:`django_db_setup` fixture. This example shows how to give a PostgreSQL 454 | sequence a random starting value. This can be used to detect and prevent 455 | primary key id's from being hard-coded in tests. 456 | 457 | Put this in ``conftest.py``:: 458 | 459 | import random 460 | import pytest 461 | from django.db import connection 462 | 463 | 464 | @pytest.fixture(scope='session') 465 | def django_db_setup(django_db_setup, django_db_blocker): 466 | with django_db_blocker.unblock(): 467 | cur = connection.cursor() 468 | cur.execute('ALTER SEQUENCE app_model_id_seq RESTART WITH %s;', 469 | [random.randint(10000, 20000)]) 470 | 471 | Create the test database from a custom SQL script 472 | """"""""""""""""""""""""""""""""""""""""""""""""" 473 | 474 | You can replace the :fixture:`django_db_setup` fixture and run any code in its 475 | place. This includes creating your database by hand by running a SQL script 476 | directly. This example shows sqlite3's executescript method. In a more 477 | general use case, you probably want to load the SQL statements from a file or 478 | invoke the ``psql`` or the ``mysql`` command line tool. 479 | 480 | Put this in ``conftest.py``:: 481 | 482 | import pytest 483 | from django.db import connection 484 | 485 | 486 | @pytest.fixture(scope='session') 487 | def django_db_setup(django_db_blocker): 488 | with django_db_blocker.unblock(): 489 | with connection.cursor() as c: 490 | c.executescript(''' 491 | DROP TABLE IF EXISTS theapp_item; 492 | CREATE TABLE theapp_item (id, name); 493 | INSERT INTO theapp_item (name) VALUES ('created from a sql script'); 494 | ''') 495 | 496 | .. warning:: 497 | This snippet shows ``cursor().executescript()`` which is `sqlite` specific, for 498 | other database engines this method might differ. For instance, psycopg uses 499 | ``cursor().execute()``. 500 | 501 | 502 | Use a read only database 503 | """""""""""""""""""""""" 504 | 505 | You can replace the ordinary `django_db_setup` to completely avoid database 506 | creation/migrations. If you have no need for rollbacks or truncating tables, 507 | you can simply avoid blocking the database and use it directly. When using this 508 | method you must ensure that your tests do not change the database state. 509 | 510 | 511 | Put this in ``conftest.py``:: 512 | 513 | import pytest 514 | 515 | 516 | @pytest.fixture(scope='session') 517 | def django_db_setup(): 518 | """Avoid creating/setting up the test database""" 519 | pass 520 | 521 | 522 | @pytest.fixture 523 | def db_access_without_rollback_and_truncate(request, django_db_setup, django_db_blocker): 524 | django_db_blocker.unblock() 525 | yield 526 | django_db_blocker.restore() 527 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | === 3 | 4 | .. _faq-import-error: 5 | 6 | I see an error saying "could not import myproject.settings" 7 | ----------------------------------------------------------- 8 | 9 | pytest-django tries to automatically add your project to the Python path by 10 | looking for a ``manage.py`` file and adding its path to the Python path. 11 | 12 | If this for some reason fails for you, you have to manage your Python paths 13 | explicitly. See the documentation on :ref:`managing_the_python_path_explicitly` 14 | for more information. 15 | 16 | .. _faq-test-tags: 17 | 18 | Are Django test tags supported? 19 | ------------------------------- 20 | 21 | Yes, Django :ref:`test tagging ` is supported. 22 | The Django test tags are automatically converted to :ref:`Pytest markers 23 | `. 24 | 25 | How can I make sure that all my tests run with a specific locale? 26 | ----------------------------------------------------------------- 27 | 28 | Create a :ref:`pytest fixture ` that is 29 | automatically run before each test case. To run all tests with the English 30 | locale, put the following code in your project's 31 | :ref:`conftest.py ` file: 32 | 33 | .. code-block:: python 34 | 35 | from django.utils.translation import activate 36 | 37 | @pytest.fixture(autouse=True) 38 | def set_default_language(): 39 | activate('en') 40 | 41 | .. _faq-tests-not-being-picked-up: 42 | 43 | My tests are not being found. Why? 44 | ---------------------------------- 45 | 46 | By default, pytest looks for tests in files named ``test_*.py`` (note that 47 | this is not the same as ``test*.py``) and ``*_test.py``. If you have your 48 | tests in files with other names, they will not be collected. Note that 49 | Django's ``startapp`` manage command creates an ``app_dir/tests.py`` file. 50 | Also, it is common to put tests under ``app_dir/tests/views.py``, etc. 51 | 52 | To find those tests, create a ``pytest.ini`` file in your project root and add 53 | an appropriate ``python_files`` line to it: 54 | 55 | .. code-block:: ini 56 | 57 | [pytest] 58 | python_files = tests.py test_*.py *_tests.py 59 | 60 | See the `related pytest docs`_ for more details. 61 | 62 | When debugging test collection problems, the ``--collectonly`` flag and 63 | ``-rs`` (report skipped tests) can be helpful. 64 | 65 | .. _related pytest docs: 66 | https://docs.pytest.org/en/stable/example/pythoncollection.html#changing-naming-conventions 67 | 68 | Does pytest-django work with the pytest-xdist plugin? 69 | ----------------------------------------------------- 70 | 71 | Yes. pytest-django supports running tests in parallel with pytest-xdist. Each 72 | process created by xdist gets its own separate database that is used for the 73 | tests. This ensures that each test can run independently, regardless of whether 74 | transactions are tested or not. 75 | 76 | .. _faq-getting-help: 77 | 78 | How can I use ``manage.py test`` with pytest-django? 79 | ---------------------------------------------------- 80 | 81 | pytest-django is designed to work with the ``pytest`` command, but if you 82 | really need integration with ``manage.py test``, you can add this class path 83 | in your Django settings: 84 | 85 | .. code-block:: python 86 | 87 | TEST_RUNNER = 'pytest_django.runner.TestRunner' 88 | 89 | Usage: 90 | 91 | .. code-block:: bash 92 | 93 | ./manage.py test -- 94 | 95 | **Note**: the pytest-django command line options ``--ds`` and ``--dc`` are not 96 | compatible with this approach, you need to use the standard Django methods of 97 | setting the ``DJANGO_SETTINGS_MODULE``/``DJANGO_CONFIGURATION`` environment 98 | variables or the ``--settings`` command line option. 99 | 100 | How can I give database access to all my tests without the `django_db` marker? 101 | ------------------------------------------------------------------------------ 102 | 103 | Create an autouse fixture and put it in ``conftest.py`` in your project root: 104 | 105 | .. code-block:: python 106 | 107 | @pytest.fixture(autouse=True) 108 | def enable_db_access_for_all_tests(db): 109 | pass 110 | 111 | How/where can I get help with pytest/pytest-django? 112 | --------------------------------------------------- 113 | 114 | Usage questions can be asked on StackOverflow with the `pytest tag`_. 115 | 116 | If you think you've found a bug or something that is wrong in the 117 | documentation, feel free to `open an issue on the GitHub project`_ for 118 | pytest-django. 119 | 120 | Direct help can be found in the #pytest IRC channel `on irc.libera.chat 121 | `_ (using an IRC client, `via webchat 122 | `_, or `via Matrix 123 | `_). 124 | 125 | .. _pytest tag: https://stackoverflow.com/search?q=pytest 126 | .. _open an issue on the GitHub project: 127 | https://github.com/pytest-dev/pytest-django/issues/ 128 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | pytest-django Documentation 3 | =========================== 4 | 5 | pytest-django is a plugin for `pytest`_ that provides a set of useful tools 6 | for testing `Django`_ applications and projects. 7 | 8 | .. _pytest: https://pytest.org/ 9 | .. _Django: https://www.djangoproject.com/ 10 | 11 | Quick Start 12 | =========== 13 | 14 | .. code-block:: bash 15 | 16 | $ pip install pytest-django 17 | 18 | Make sure ``DJANGO_SETTINGS_MODULE`` is defined (see 19 | :ref:`configuring_django_settings`) and make your tests discoverable 20 | (see :ref:`faq-tests-not-being-picked-up`): 21 | 22 | Example using pytest.ini or tox.ini 23 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 24 | 25 | .. code-block:: ini 26 | 27 | # -- FILE: pytest.ini (or tox.ini) 28 | [pytest] 29 | DJANGO_SETTINGS_MODULE = test.settings 30 | # -- recommended but optional: 31 | python_files = tests.py test_*.py *_tests.py 32 | 33 | Example using pyproject.toml 34 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 35 | 36 | .. code-block:: toml 37 | 38 | # -- Example FILE: pyproject.toml 39 | [tool.pytest.ini_options] 40 | DJANGO_SETTINGS_MODULE = "test.settings" 41 | # -- recommended but optional: 42 | python_files = ["test_*.py", "*_test.py", "testing/python/*.py"] 43 | 44 | Run your tests with ``pytest``: 45 | 46 | .. code-block:: bash 47 | 48 | $ pytest 49 | 50 | Why would I use this instead of Django's manage.py test command? 51 | ================================================================ 52 | 53 | Running the test suite with pytest offers some features that are not present in Django's standard test mechanism: 54 | 55 | * Less boilerplate: no need to import unittest, create a subclass with methods. Just write tests as regular functions. 56 | * :ref:`Manage test dependencies with fixtures `. 57 | * Run tests in multiple processes for increased speed. 58 | * There are a lot of other nice plugins available for pytest. 59 | * Easy switching: Existing unittest-style tests will still work without any modifications. 60 | 61 | See the `pytest documentation`_ for more information on pytest. 62 | 63 | .. _pytest documentation: https://docs.pytest.org/ 64 | 65 | Bugs? Feature Suggestions? 66 | ========================== 67 | 68 | Report issues and feature requests at the `GitHub issue tracker`_. 69 | 70 | .. _GitHub issue tracker: https://github.com/pytest-dev/pytest-django/issues 71 | 72 | Table of Contents 73 | ================= 74 | 75 | .. toctree:: 76 | :maxdepth: 3 77 | 78 | tutorial 79 | configuring_django 80 | managing_python_path 81 | usage 82 | database 83 | helpers 84 | faq 85 | contributing 86 | changelog 87 | 88 | Indices and Tables 89 | ================== 90 | 91 | * :ref:`genindex` 92 | * :ref:`modindex` 93 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pytest-django.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pytest-django.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/managing_python_path.rst: -------------------------------------------------------------------------------- 1 | .. _managing_python_path: 2 | 3 | Managing the Python path 4 | ======================== 5 | 6 | pytest needs to be able to import the code in your project. Normally, when 7 | interacting with Django code, the interaction happens via ``manage.py``, which 8 | will implicitly add that directory to the Python path. 9 | 10 | However, when Python is started via the ``pytest`` command, some extra care is 11 | needed to have the Python path setup properly. There are two ways to handle 12 | this problem, described below. 13 | 14 | Automatic looking for Django projects 15 | ------------------------------------- 16 | 17 | By default, pytest-django tries to find Django projects by automatically 18 | looking for the project's ``manage.py`` file and adding its directory to the 19 | Python path. 20 | 21 | Looking for the ``manage.py`` file uses the same algorithm as pytest uses to 22 | find ``pyproject.toml``, ``pytest.ini``, ``tox.ini`` and ``setup.cfg``: Each 23 | test root directories parents will be searched for ``manage.py`` files, and it 24 | will stop when the first file is found. 25 | 26 | If you have a custom project setup, have none or multiple ``manage.py`` files 27 | in your project, the automatic detection may not be correct. See 28 | :ref:`managing_the_python_path_explicitly` for more details on how to configure 29 | your environment in that case. 30 | 31 | .. _managing_the_python_path_explicitly: 32 | 33 | Managing the Python path explicitly 34 | ----------------------------------- 35 | 36 | First, disable the automatic Django project finder. Add this to 37 | ``pytest.ini``, ``setup.cfg`` or ``tox.ini``:: 38 | 39 | [pytest] 40 | django_find_project = false 41 | 42 | 43 | Next, you need to make sure that your project code is available on the Python 44 | path. There are multiple ways to achieve this: 45 | 46 | Managing your project with virtualenv, pip and editable mode 47 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 48 | 49 | The easiest way to have your code available on the Python path when using 50 | virtualenv and pip is to install your project in editable mode when developing. 51 | 52 | If you don't already have a pyproject.toml file, creating a pyproject.toml file 53 | with this content will get you started:: 54 | 55 | # pyproject.toml 56 | [build-system] 57 | requires = [ 58 | "setuptools>=61.0.0", 59 | ] 60 | build-backend = "setuptools.build_meta" 61 | 62 | This ``pyproject.toml`` file is not sufficient to distribute your package to PyPI or 63 | more general packaging, but it should help you get started. Please refer to the 64 | `Python Packaging User Guide 65 | `_ 66 | for more information on packaging Python applications. 67 | 68 | To install the project afterwards:: 69 | 70 | pip install --editable . 71 | 72 | Your code should then be importable from any Python application. You can also 73 | add this directly to your project's requirements.txt file like this:: 74 | 75 | # requirements.txt 76 | -e . 77 | django 78 | pytest-django 79 | 80 | 81 | Using pytest's ``pythonpath`` option 82 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 83 | 84 | You can explicitly add paths to the Python search path using pytest's 85 | :pytest-confval:`pythonpath` option. 86 | 87 | Example: project with src layout 88 | ```````````````````````````````` 89 | 90 | For a Django package using the ``src`` layout, with test settings located in a 91 | ``tests`` package at the top level:: 92 | 93 | myproj 94 | ├── pytest.ini 95 | ├── src 96 | │ └── myproj 97 | │ ├── __init__.py 98 | │ └── main.py 99 | └── tests 100 | ├── testapp 101 | | ├── __init__.py 102 | | └── apps.py 103 | ├── __init__.py 104 | ├── settings.py 105 | └── test_main.py 106 | 107 | You'll need to specify both the top level directory and ``src`` for things to work:: 108 | 109 | [pytest] 110 | DJANGO_SETTINGS_MODULE = tests.settings 111 | pythonpath = . src 112 | 113 | If you don't specify ``.``, the settings module won't be found and 114 | you'll get an import error: ``ImportError: No module named 'tests'``. 115 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | Getting started with pytest and pytest-django 2 | ============================================= 3 | 4 | Introduction 5 | ------------ 6 | 7 | pytest and pytest-django are compatible with standard Django test suites and 8 | Nose test suites. They should be able to pick up and run existing tests without 9 | any or little configuration. This section describes how to get started quickly. 10 | 11 | Talks, articles and blog posts 12 | ------------------------------ 13 | 14 | * Talk from DjangoCon Europe 2014: `pytest: helps you write better Django apps, by Andreas Pelme `_ 15 | 16 | * Talk from EuroPython 2013: `Testing Django application with pytest, by Andreas Pelme `_ 17 | 18 | * Three part blog post tutorial (part 3 mentions Django integration): `pytest: no-boilerplate testing, by Daniel Greenfeld `_ 19 | 20 | * Blog post: `Django Projects to Django Apps: Converting the Unit Tests, by 21 | John Costa 22 | `_. 23 | 24 | For general information and tutorials on pytest, see the `pytest tutorial page `_. 25 | 26 | 27 | Step 1: Installation 28 | -------------------- 29 | 30 | pytest-django can be obtained directly from `PyPI 31 | `_, and can be installed with 32 | ``pip``: 33 | 34 | .. code-block:: bash 35 | 36 | pip install pytest-django 37 | 38 | Installing pytest-django will also automatically install the latest version of 39 | pytest. ``pytest-django`` uses ``pytest``'s plugin system and can be used right away 40 | after installation, there is nothing more to configure. 41 | 42 | Step 2: Point pytest to your Django settings 43 | -------------------------------------------- 44 | 45 | You need to tell pytest which Django settings should be used for test 46 | runs. The easiest way to achieve this is to create a pytest configuration file 47 | with this information. 48 | 49 | Create a file called ``pytest.ini`` in your project root directory that 50 | contains: 51 | 52 | .. code-block:: ini 53 | 54 | [pytest] 55 | DJANGO_SETTINGS_MODULE = yourproject.settings 56 | 57 | Another options for people that use ``pyproject.toml`` is add the following code: 58 | 59 | .. code-block:: toml 60 | 61 | [tool.pytest.ini_options] 62 | DJANGO_SETTINGS_MODULE = "yourproject.settings" 63 | 64 | You can also specify your Django settings by setting the 65 | ``DJANGO_SETTINGS_MODULE`` environment variable or specifying the 66 | ``--ds=yourproject.settings`` command line flag when running the tests. 67 | See the full documentation on :ref:`configuring_django_settings`. 68 | 69 | Optionally, also add the following line to the ``[pytest]`` section to 70 | instruct pytest to collect tests in Django's default app layouts, too. 71 | See the FAQ at :ref:`faq-tests-not-being-picked-up` for more infos. 72 | 73 | .. code-block:: ini 74 | 75 | python_files = tests.py test_*.py *_tests.py 76 | 77 | Step 3: Run your test suite 78 | --------------------------- 79 | 80 | Tests are invoked directly with the ``pytest`` command, instead of ``manage.py 81 | test``, that you might be used to: 82 | 83 | .. code-block:: bash 84 | 85 | pytest 86 | 87 | Do you have problems with pytest not finding your code? See the FAQ 88 | :ref:`faq-import-error`. 89 | 90 | Next steps 91 | ---------- 92 | 93 | The :ref:`usage` section describes more ways to interact with your test suites. 94 | 95 | pytest-django also provides some :ref:`helpers` to make it easier to write 96 | Django tests. 97 | 98 | Consult the `pytest documentation `_ for more information 99 | on pytest itself. 100 | 101 | Stuck? Need help? 102 | ----------------- 103 | 104 | No problem, see the FAQ on :ref:`faq-getting-help` for information on how to 105 | get help. 106 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. _usage: 2 | 3 | Usage and invocations 4 | ===================== 5 | 6 | Basic usage 7 | ----------- 8 | 9 | When using pytest-django, django-admin.py or manage.py is not used to run 10 | tests. This makes it possible to invoke pytest and other plugins with all its 11 | different options directly. 12 | 13 | Running a test suite is done by invoking the pytest command directly:: 14 | 15 | pytest 16 | 17 | Specific test files or directories can be selected by specifying the test file names directly on 18 | the command line:: 19 | 20 | pytest test_something.py a_directory 21 | 22 | See the `pytest documentation on Usage and invocations 23 | `_ for more help on available parameters. 24 | 25 | Additional command line options 26 | ------------------------------- 27 | 28 | ``--fail-on-template-vars`` - fail for invalid variables in templates 29 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 30 | Fail tests that render templates which make use of invalid template variables. 31 | 32 | You can switch it on in `pytest.ini`:: 33 | 34 | [pytest] 35 | FAIL_INVALID_TEMPLATE_VARS = True 36 | 37 | Additional pytest.ini settings 38 | ------------------------------ 39 | 40 | ``django_debug_mode`` - change how DEBUG is set 41 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 42 | 43 | By default tests run with the 44 | `DEBUG `_ 45 | setting set to ``False``. This is to ensure that the observed output of your 46 | code matches what will be seen in a production setting. 47 | 48 | If you want ``DEBUG`` to be set:: 49 | 50 | [pytest] 51 | django_debug_mode = true 52 | 53 | You can also use ``django_debug_mode = keep`` to disable the overriding and use 54 | whatever is already set in the Django settings. 55 | 56 | Running tests in parallel with pytest-xdist 57 | ------------------------------------------- 58 | pytest-django supports running tests on multiple processes to speed up test 59 | suite run time. This can lead to significant speed improvements on multi 60 | core/multi CPU machines. 61 | 62 | This requires the pytest-xdist plugin to be available, it can usually be 63 | installed with:: 64 | 65 | pip install pytest-xdist 66 | 67 | You can then run the tests by running:: 68 | 69 | pytest -n 70 | 71 | When tests are invoked with xdist, pytest-django will create a separate test 72 | database for each process. Each test database will be given a suffix 73 | (something like "gw0", "gw1") to map to a xdist process. If your database name 74 | is set to "foo", the test database with xdist will be "test_foo_gw0", 75 | "test_foo_gw1" etc. 76 | 77 | See the full documentation on `pytest-xdist 78 | `_ for more 79 | information. Among other features, pytest-xdist can distribute/coordinate test 80 | execution on remote machines. 81 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=61.0.0", 4 | "setuptools-scm[toml]>=5.0.0", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | 8 | [project] 9 | name = "pytest-django" 10 | description = "A Django plugin for pytest." 11 | readme = "README.rst" 12 | requires-python = ">=3.9" 13 | dynamic = ["version"] 14 | authors = [ 15 | { name = "Andreas Pelme", email = "andreas@pelme.se" }, 16 | ] 17 | maintainers = [ 18 | { name = "Andreas Pelme", email = "andreas@pelme.se" }, 19 | ] 20 | license = {file = "LICENSE"} 21 | classifiers = [ 22 | "Development Status :: 5 - Production/Stable", 23 | "Framework :: Django", 24 | "Framework :: Django :: 4.2", 25 | "Framework :: Django :: 5.1", 26 | "Framework :: Django :: 5.2", 27 | "Intended Audience :: Developers", 28 | "License :: OSI Approved :: BSD License", 29 | "Operating System :: OS Independent", 30 | "Programming Language :: Python", 31 | "Programming Language :: Python :: 3.9", 32 | "Programming Language :: Python :: 3.10", 33 | "Programming Language :: Python :: 3.11", 34 | "Programming Language :: Python :: 3.12", 35 | "Programming Language :: Python :: 3.13", 36 | "Programming Language :: Python :: Implementation :: CPython", 37 | "Programming Language :: Python :: Implementation :: PyPy", 38 | "Topic :: Software Development :: Testing", 39 | ] 40 | dependencies = [ 41 | "pytest>=7.0.0", 42 | ] 43 | [project.optional-dependencies] 44 | docs = [ 45 | "sphinx", 46 | "sphinx_rtd_theme", 47 | ] 48 | testing = [ 49 | "Django", 50 | "django-configurations>=2.0", 51 | ] 52 | [project.urls] 53 | Documentation = "https://pytest-django.readthedocs.io/" 54 | Repository = "https://github.com/pytest-dev/pytest-django" 55 | Changelog = "https://pytest-django.readthedocs.io/en/latest/changelog.html" 56 | [project.entry-points.pytest11] 57 | django = "pytest_django.plugin" 58 | 59 | [tool.setuptools] 60 | packages = ["pytest_django"] 61 | [tool.setuptools.package-data] 62 | pytest_django = ["py.typed"] 63 | 64 | [tool.setuptools_scm] 65 | write_to = "pytest_django/_version.py" 66 | 67 | [tool.pytest.ini_options] 68 | addopts = [ 69 | # Error on using unregistered marker. 70 | "--strict-markers", 71 | # Show extra test summary info for everything. 72 | "-ra", 73 | ] 74 | pythonpath = ["."] 75 | DJANGO_SETTINGS_MODULE = "pytest_django_test.settings_sqlite_file" 76 | testpaths = ["tests"] 77 | markers = ["tag1", "tag2", "tag3", "tag4", "tag5"] 78 | 79 | [tool.mypy] 80 | strict = true 81 | disallow_incomplete_defs = false 82 | disallow_untyped_defs = false 83 | disallow_subclassing_any = false 84 | files = [ 85 | "pytest_django", 86 | "pytest_django_test", 87 | "tests", 88 | ] 89 | [[tool.mypy.overrides]] 90 | module = [ 91 | "django.*", 92 | "configurations.*", 93 | ] 94 | ignore_missing_imports = true 95 | 96 | [tool.coverage.run] 97 | parallel = true 98 | source = ["${PYTESTDJANGO_COVERAGE_SRC}."] 99 | branch = true 100 | [tool.coverage.report] 101 | include = [ 102 | "pytest_django/*", 103 | "pytest_django_test/*", 104 | "tests/*", 105 | ] 106 | skip_covered = true 107 | exclude_lines = [ 108 | "pragma: no cover", 109 | "if TYPE_CHECKING:", 110 | ] 111 | 112 | [tool.ruff] 113 | line-length = 99 114 | extend-exclude = [ 115 | "pytest_django/_version.py", 116 | ] 117 | 118 | [tool.ruff.lint] 119 | extend-select = [ 120 | "B", # flake8-bugbear 121 | "BLE", # flake8-blind-except 122 | "DTZ", # flake8-datetimez 123 | "FA", # flake8-future-annotations 124 | "G", # flake8-logging-format 125 | "I", # isort 126 | "PGH", # pygrep-hooks 127 | "PIE", # flake8-pie 128 | "PL", # pylint 129 | "PT", # flake8-pytest-style 130 | "PYI", # flake8-pyi 131 | "RUF", # Ruff-specific rules 132 | "SLOT", # flake8-slots 133 | "T10", # flake8-debugger 134 | "UP", # pyupgrade 135 | "YTT", # flake8-2020 136 | ] 137 | ignore = [ 138 | "PLR0913", # Too many arguments in function definition 139 | "PLR2004", # Magic value used in comparison, consider replacing 3 with a constant variable 140 | "PT001", # Use `@pytest.fixture()` over `@pytest.fixture` 141 | "PT023", # Use `@pytest.mark.django_db()` over `@pytest.mark.django_db` 142 | ] 143 | 144 | [tool.ruff.lint.isort] 145 | forced-separate = [ 146 | "tests", 147 | "pytest_django", 148 | "pytest_django_test", 149 | ] 150 | combine-as-imports = true 151 | split-on-trailing-comma = false 152 | lines-after-imports = 2 153 | -------------------------------------------------------------------------------- /pytest_django/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from ._version import version as __version__ 3 | except ImportError: # pragma: no cover 4 | # Broken installation, we don't even try. 5 | __version__ = "unknown" 6 | 7 | 8 | from .fixtures import DjangoAssertNumQueries, DjangoCaptureOnCommitCallbacks 9 | from .plugin import DjangoDbBlocker 10 | 11 | 12 | __all__ = [ 13 | "DjangoAssertNumQueries", 14 | "DjangoCaptureOnCommitCallbacks", 15 | "DjangoDbBlocker", 16 | "__version__", 17 | ] 18 | -------------------------------------------------------------------------------- /pytest_django/asserts.py: -------------------------------------------------------------------------------- 1 | """ 2 | Dynamically load all Django assertion cases and expose them for importing. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | from collections.abc import Sequence 8 | from functools import wraps 9 | from typing import TYPE_CHECKING, Any, Callable 10 | 11 | from django import VERSION 12 | from django.test import LiveServerTestCase, SimpleTestCase, TestCase, TransactionTestCase 13 | 14 | 15 | USE_CONTRIB_MESSAGES = VERSION >= (5, 0) 16 | 17 | if USE_CONTRIB_MESSAGES: 18 | from django.contrib.messages import Message 19 | from django.contrib.messages.test import MessagesTestMixin 20 | 21 | class MessagesTestCase(MessagesTestMixin, TestCase): 22 | pass 23 | 24 | test_case = MessagesTestCase("run") 25 | else: 26 | test_case = TestCase("run") 27 | 28 | 29 | def _wrapper(name: str): 30 | func = getattr(test_case, name) 31 | 32 | @wraps(func) 33 | def assertion_func(*args, **kwargs): 34 | return func(*args, **kwargs) 35 | 36 | return assertion_func 37 | 38 | 39 | __all__ = [] 40 | assertions_names: set[str] = set() 41 | assertions_names.update( 42 | {attr for attr in vars(TestCase) if attr.startswith("assert")}, 43 | {attr for attr in vars(SimpleTestCase) if attr.startswith("assert")}, 44 | {attr for attr in vars(LiveServerTestCase) if attr.startswith("assert")}, 45 | {attr for attr in vars(TransactionTestCase) if attr.startswith("assert")}, 46 | ) 47 | 48 | if USE_CONTRIB_MESSAGES: 49 | assertions_names.update( 50 | {attr for attr in vars(MessagesTestMixin) if attr.startswith("assert")}, 51 | ) 52 | 53 | for assert_func in assertions_names: 54 | globals()[assert_func] = _wrapper(assert_func) 55 | __all__.append(assert_func) # noqa: PYI056 56 | 57 | 58 | if TYPE_CHECKING: 59 | from django import forms 60 | from django.http.response import HttpResponseBase 61 | 62 | def assertRedirects( 63 | response: HttpResponseBase, 64 | expected_url: str, 65 | status_code: int = ..., 66 | target_status_code: int = ..., 67 | msg_prefix: str = ..., 68 | fetch_redirect_response: bool = ..., 69 | ) -> None: ... 70 | 71 | def assertURLEqual( 72 | url1: str, 73 | url2: str, 74 | msg_prefix: str = ..., 75 | ) -> None: ... 76 | 77 | def assertContains( 78 | response: HttpResponseBase, 79 | text: object, 80 | count: int | None = ..., 81 | status_code: int = ..., 82 | msg_prefix: str = ..., 83 | html: bool = False, 84 | ) -> None: ... 85 | 86 | def assertNotContains( 87 | response: HttpResponseBase, 88 | text: object, 89 | status_code: int = ..., 90 | msg_prefix: str = ..., 91 | html: bool = False, 92 | ) -> None: ... 93 | 94 | def assertFormError( 95 | form: forms.BaseForm, 96 | field: str | None, 97 | errors: str | Sequence[str], 98 | msg_prefix: str = ..., 99 | ) -> None: ... 100 | 101 | def assertFormSetError( 102 | formset: forms.BaseFormSet, 103 | form_index: int | None, 104 | field: str | None, 105 | errors: str | Sequence[str], 106 | msg_prefix: str = ..., 107 | ) -> None: ... 108 | 109 | def assertTemplateUsed( 110 | response: HttpResponseBase | str | None = ..., 111 | template_name: str | None = ..., 112 | msg_prefix: str = ..., 113 | count: int | None = ..., 114 | ): ... 115 | 116 | def assertTemplateNotUsed( 117 | response: HttpResponseBase | str | None = ..., 118 | template_name: str | None = ..., 119 | msg_prefix: str = ..., 120 | ): ... 121 | 122 | def assertRaisesMessage( 123 | expected_exception: type[Exception], 124 | expected_message: str, 125 | *args, 126 | **kwargs, 127 | ): ... 128 | 129 | def assertWarnsMessage( 130 | expected_warning: Warning, 131 | expected_message: str, 132 | *args, 133 | **kwargs, 134 | ): ... 135 | 136 | def assertFieldOutput( 137 | fieldclass, 138 | valid, 139 | invalid, 140 | field_args=..., 141 | field_kwargs=..., 142 | empty_value: str = ..., 143 | ) -> None: ... 144 | 145 | def assertHTMLEqual( 146 | html1: str, 147 | html2: str, 148 | msg: str | None = ..., 149 | ) -> None: ... 150 | 151 | def assertHTMLNotEqual( 152 | html1: str, 153 | html2: str, 154 | msg: str | None = ..., 155 | ) -> None: ... 156 | 157 | def assertInHTML( 158 | needle: str, 159 | haystack: str, 160 | count: int | None = ..., 161 | msg_prefix: str = ..., 162 | ) -> None: ... 163 | 164 | # Added in Django 5.1. 165 | def assertNotInHTML( 166 | needle: str, 167 | haystack: str, 168 | msg_prefix: str = ..., 169 | ) -> None: ... 170 | 171 | def assertJSONEqual( 172 | raw: str, 173 | expected_data: Any, 174 | msg: str | None = ..., 175 | ) -> None: ... 176 | 177 | def assertJSONNotEqual( 178 | raw: str, 179 | expected_data: Any, 180 | msg: str | None = ..., 181 | ) -> None: ... 182 | 183 | def assertXMLEqual( 184 | xml1: str, 185 | xml2: str, 186 | msg: str | None = ..., 187 | ) -> None: ... 188 | 189 | def assertXMLNotEqual( 190 | xml1: str, 191 | xml2: str, 192 | msg: str | None = ..., 193 | ) -> None: ... 194 | 195 | # Removed in Django 5.1: use assertQuerySetEqual. 196 | def assertQuerysetEqual( 197 | qs, 198 | values, 199 | transform=..., 200 | ordered: bool = ..., 201 | msg: str | None = ..., 202 | ) -> None: ... 203 | 204 | def assertQuerySetEqual( 205 | qs, 206 | values, 207 | transform=..., 208 | ordered: bool = ..., 209 | msg: str | None = ..., 210 | ) -> None: ... 211 | 212 | def assertNumQueries( 213 | num: int, 214 | func=..., 215 | *args, 216 | using: str = ..., 217 | **kwargs, 218 | ): ... 219 | 220 | # Added in Django 5.0. 221 | def assertMessages( 222 | response: HttpResponseBase, 223 | expected_messages: Sequence[Message], 224 | *args, 225 | ordered: bool = ..., 226 | ) -> None: ... 227 | 228 | # Fallback in case Django adds new asserts. 229 | def __getattr__(name: str) -> Callable[..., Any]: ... 230 | -------------------------------------------------------------------------------- /pytest_django/django_compat.py: -------------------------------------------------------------------------------- 1 | # Note that all functions here assume django is available. So ensure 2 | # this is the case before you call them. 3 | from __future__ import annotations 4 | 5 | import pytest 6 | 7 | 8 | def is_django_unittest(request_or_item: pytest.FixtureRequest | pytest.Item) -> bool: 9 | """Returns whether the request or item is a Django test case.""" 10 | from django.test import SimpleTestCase 11 | 12 | cls = getattr(request_or_item, "cls", None) 13 | 14 | if cls is None: 15 | return False 16 | 17 | return issubclass(cls, SimpleTestCase) 18 | -------------------------------------------------------------------------------- /pytest_django/lazy_django.py: -------------------------------------------------------------------------------- 1 | """ 2 | Helpers to load Django lazily when Django settings can't be configured. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import os 8 | import sys 9 | from typing import Any 10 | 11 | import pytest 12 | 13 | 14 | def skip_if_no_django() -> None: 15 | """Raises a skip exception when no Django settings are available""" 16 | if not django_settings_is_configured(): 17 | pytest.skip("no Django settings") 18 | 19 | 20 | def django_settings_is_configured() -> bool: 21 | """Return whether the Django settings module has been configured. 22 | 23 | This uses either the DJANGO_SETTINGS_MODULE environment variable, or the 24 | configured flag in the Django settings object if django.conf has already 25 | been imported. 26 | """ 27 | ret = bool(os.environ.get("DJANGO_SETTINGS_MODULE")) 28 | 29 | if not ret and "django.conf" in sys.modules: 30 | django_conf: Any = sys.modules["django.conf"] 31 | ret = django_conf.settings.configured 32 | 33 | return ret 34 | 35 | 36 | def get_django_version() -> tuple[int, int, int, str, int]: 37 | import django 38 | 39 | version: tuple[int, int, int, str, int] = django.VERSION 40 | return version 41 | -------------------------------------------------------------------------------- /pytest_django/live_server_helper.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | 5 | 6 | class LiveServer: 7 | """The liveserver fixture 8 | 9 | This is the object that the ``live_server`` fixture returns. 10 | The ``live_server`` fixture handles creation and stopping. 11 | """ 12 | 13 | def __init__(self, addr: str, *, start: bool = True) -> None: 14 | from django.db import connections 15 | from django.test.testcases import LiveServerThread 16 | from django.test.utils import modify_settings 17 | 18 | liveserver_kwargs: dict[str, Any] = {} 19 | 20 | connections_override = {} 21 | for conn in connections.all(): 22 | # If using in-memory sqlite databases, pass the connections to 23 | # the server thread. 24 | if conn.vendor == "sqlite" and conn.is_in_memory_db(): 25 | connections_override[conn.alias] = conn 26 | 27 | liveserver_kwargs["connections_override"] = connections_override 28 | from django.conf import settings 29 | 30 | if "django.contrib.staticfiles" in settings.INSTALLED_APPS: 31 | from django.contrib.staticfiles.handlers import StaticFilesHandler 32 | 33 | liveserver_kwargs["static_handler"] = StaticFilesHandler 34 | else: 35 | from django.test.testcases import _StaticFilesHandler 36 | 37 | liveserver_kwargs["static_handler"] = _StaticFilesHandler 38 | 39 | try: 40 | host, port = addr.split(":") 41 | except ValueError: 42 | host = addr 43 | else: 44 | liveserver_kwargs["port"] = int(port) 45 | self.thread = LiveServerThread(host, **liveserver_kwargs) 46 | 47 | self._live_server_modified_settings = modify_settings( 48 | ALLOWED_HOSTS={"append": host}, 49 | ) 50 | # `_live_server_modified_settings` is enabled and disabled by 51 | # `_live_server_helper`. 52 | 53 | self.thread.daemon = True 54 | 55 | if start: 56 | self.start() 57 | 58 | def start(self) -> None: 59 | """Start the server""" 60 | for conn in self.thread.connections_override.values(): 61 | # Explicitly enable thread-shareability for this connection. 62 | conn.inc_thread_sharing() 63 | 64 | self.thread.start() 65 | self.thread.is_ready.wait() 66 | 67 | if self.thread.error: 68 | error = self.thread.error 69 | self.stop() 70 | raise error 71 | 72 | def stop(self) -> None: 73 | """Stop the server""" 74 | # Terminate the live server's thread. 75 | self.thread.terminate() 76 | # Restore shared connections' non-shareability. 77 | for conn in self.thread.connections_override.values(): 78 | conn.dec_thread_sharing() 79 | 80 | @property 81 | def url(self) -> str: 82 | return f"http://{self.thread.host}:{self.thread.port}" 83 | 84 | def __str__(self) -> str: 85 | return self.url 86 | 87 | def __add__(self, other) -> str: 88 | return f"{self}{other}" 89 | 90 | def __repr__(self) -> str: 91 | return f"" 92 | -------------------------------------------------------------------------------- /pytest_django/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pytest-django/7c99f33794113446ce865cbab825adb6ae7aee78/pytest_django/py.typed -------------------------------------------------------------------------------- /pytest_django/runner.py: -------------------------------------------------------------------------------- 1 | from argparse import ArgumentParser 2 | from collections.abc import Iterable 3 | from typing import Any 4 | 5 | 6 | class TestRunner: 7 | """A Django test runner which uses pytest to discover and run tests when using `manage.py test`.""" 8 | 9 | def __init__( 10 | self, 11 | *, 12 | verbosity: int = 1, 13 | failfast: bool = False, 14 | keepdb: bool = False, 15 | **kwargs: Any, 16 | ) -> None: 17 | self.verbosity = verbosity 18 | self.failfast = failfast 19 | self.keepdb = keepdb 20 | 21 | @classmethod 22 | def add_arguments(cls, parser: ArgumentParser) -> None: 23 | parser.add_argument( 24 | "--keepdb", action="store_true", help="Preserves the test DB between runs." 25 | ) 26 | 27 | def run_tests(self, test_labels: Iterable[str], **kwargs: Any) -> int: 28 | """Run pytest and return the exitcode. 29 | 30 | It translates some of Django's test command option to pytest's. 31 | """ 32 | import pytest 33 | 34 | argv = [] 35 | if self.verbosity == 0: 36 | argv.append("--quiet") 37 | elif self.verbosity >= 2: 38 | verbosity = "v" * (self.verbosity - 1) 39 | argv.append(f"-{verbosity}") 40 | if self.failfast: 41 | argv.append("--exitfirst") 42 | if self.keepdb: 43 | argv.append("--reuse-db") 44 | 45 | argv.extend(test_labels) 46 | return pytest.main(argv) 47 | -------------------------------------------------------------------------------- /pytest_django_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pytest-django/7c99f33794113446ce865cbab825adb6ae7aee78/pytest_django_test/__init__.py -------------------------------------------------------------------------------- /pytest_django_test/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pytest-django/7c99f33794113446ce865cbab825adb6ae7aee78/pytest_django_test/app/__init__.py -------------------------------------------------------------------------------- /pytest_django_test/app/fixtures/items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "app.item", 5 | "fields": { 6 | "name": "Fixture item" 7 | } 8 | } 9 | ] -------------------------------------------------------------------------------- /pytest_django_test/app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import ClassVar 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | initial = True 10 | 11 | dependencies: tuple[tuple[str, str], ...] = () 12 | 13 | operations: ClassVar = [ 14 | migrations.CreateModel( 15 | name="Item", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("name", models.CharField(max_length=100)), 27 | ], 28 | ), 29 | migrations.CreateModel( 30 | name="SecondItem", 31 | fields=[ 32 | ( 33 | "id", 34 | models.AutoField( 35 | auto_created=True, 36 | primary_key=True, 37 | serialize=False, 38 | verbose_name="ID", 39 | ), 40 | ), 41 | ("name", models.CharField(max_length=100)), 42 | ], 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /pytest_django_test/app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pytest-django/7c99f33794113446ce865cbab825adb6ae7aee78/pytest_django_test/app/migrations/__init__.py -------------------------------------------------------------------------------- /pytest_django_test/app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | # Routed to database "main". 5 | class Item(models.Model): 6 | name: str = models.CharField(max_length=100) 7 | 8 | 9 | # Routed to database "second". 10 | class SecondItem(models.Model): 11 | name: str = models.CharField(max_length=100) 12 | -------------------------------------------------------------------------------- /pytest_django_test/app/static/a_file.txt: -------------------------------------------------------------------------------- 1 | bla 2 | -------------------------------------------------------------------------------- /pytest_django_test/app/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpRequest, HttpResponse 2 | from django.template import Template 3 | from django.template.context import Context 4 | 5 | from .models import Item 6 | 7 | 8 | def admin_required_view(request: HttpRequest) -> HttpResponse: 9 | assert request.user.is_staff 10 | return HttpResponse(Template("You are an admin").render(Context())) 11 | 12 | 13 | def item_count(request: HttpRequest) -> HttpResponse: 14 | return HttpResponse(f"Item count: {Item.objects.count()}") 15 | -------------------------------------------------------------------------------- /pytest_django_test/db_helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import sqlite3 5 | import subprocess 6 | from collections.abc import Mapping 7 | 8 | import pytest 9 | from django.conf import settings 10 | from django.utils.encoding import force_str 11 | 12 | 13 | # Construct names for the "inner" database used in runpytest tests 14 | _settings = settings.DATABASES["default"] 15 | 16 | DB_NAME: str = _settings["NAME"] 17 | TEST_DB_NAME: str = _settings["TEST"]["NAME"] 18 | 19 | if _settings["ENGINE"] == "django.db.backends.sqlite3" and TEST_DB_NAME is None: 20 | TEST_DB_NAME = ":memory:" 21 | SECOND_DB_NAME = ":memory:" 22 | SECOND_TEST_DB_NAME = ":memory:" 23 | else: 24 | DB_NAME += "_inner" 25 | 26 | if TEST_DB_NAME is None: 27 | # No explicit test db name was given, construct a default one 28 | TEST_DB_NAME = f"test_{DB_NAME}_inner" 29 | else: 30 | # An explicit test db name was given, is that as the base name 31 | TEST_DB_NAME = f"{TEST_DB_NAME}_inner" 32 | 33 | SECOND_DB_NAME = DB_NAME + "_second" if DB_NAME is not None else None 34 | SECOND_TEST_DB_NAME = TEST_DB_NAME + "_second" if DB_NAME is not None else None 35 | 36 | 37 | def get_db_engine() -> str: 38 | db_engine: str = _settings["ENGINE"].split(".")[-1] 39 | return db_engine 40 | 41 | 42 | class CmdResult: 43 | def __init__(self, status_code: int, std_out: bytes, std_err: bytes) -> None: 44 | self.status_code = status_code 45 | self.std_out = std_out 46 | self.std_err = std_err 47 | 48 | 49 | def run_cmd(*args: str, env: Mapping[str, str] | None = None) -> CmdResult: 50 | r = subprocess.Popen( 51 | args, 52 | stdout=subprocess.PIPE, 53 | stderr=subprocess.PIPE, 54 | env={**os.environ, **(env or {})}, 55 | ) 56 | stdoutdata, stderrdata = r.communicate() 57 | ret = r.wait() 58 | return CmdResult(ret, stdoutdata, stderrdata) 59 | 60 | 61 | def run_psql(*args: str) -> CmdResult: 62 | env = {} 63 | user = _settings.get("USER") 64 | if user: # pragma: no branch 65 | args = ("-U", user, *args) 66 | password = _settings.get("PASSWORD") 67 | if password: # pragma: no branch 68 | env["PGPASSWORD"] = password 69 | host = _settings.get("HOST") 70 | if host: # pragma: no branch 71 | args = ("-h", host, *args) 72 | return run_cmd("psql", *args, env=env) 73 | 74 | 75 | def run_mysql(*args: str) -> CmdResult: 76 | user = _settings.get("USER") 77 | if user: # pragma: no branch 78 | args = ("-u", user, *args) 79 | password = _settings.get("PASSWORD") 80 | if password: # pragma: no branch 81 | # Note: "-ppassword" must be a single argument. 82 | args = ("-p" + password, *args) 83 | host = _settings.get("HOST") 84 | if host: # pragma: no branch 85 | args = ("-h", host, *args) 86 | return run_cmd("mysql", *args) 87 | 88 | 89 | def skip_if_sqlite_in_memory() -> None: 90 | if _settings["ENGINE"] == "django.db.backends.sqlite3" and _settings["TEST"]["NAME"] is None: 91 | pytest.skip("Do not test db reuse since database does not support it") 92 | 93 | 94 | def _get_db_name(db_suffix: str | None = None) -> str: 95 | name = TEST_DB_NAME 96 | if db_suffix: 97 | name = f"{name}_{db_suffix}" 98 | return name 99 | 100 | 101 | def drop_database(db_suffix: str | None = None) -> None: 102 | name = _get_db_name(db_suffix) 103 | db_engine = get_db_engine() 104 | 105 | if db_engine == "postgresql": 106 | r = run_psql("postgres", "-c", f"DROP DATABASE {name}") 107 | assert "DROP DATABASE" in force_str(r.std_out) or "does not exist" in force_str(r.std_err) 108 | return 109 | 110 | if db_engine == "mysql": 111 | r = run_mysql("-e", f"DROP DATABASE {name}") 112 | assert "database doesn't exist" in force_str(r.std_err) or r.status_code == 0 113 | return 114 | 115 | assert db_engine == "sqlite3", f"{db_engine} cannot be tested properly!" 116 | assert name != ":memory:", "sqlite in-memory database cannot be dropped!" 117 | if os.path.exists(name): # pragma: no branch 118 | os.unlink(name) 119 | 120 | 121 | def db_exists(db_suffix: str | None = None) -> bool: 122 | name = _get_db_name(db_suffix) 123 | db_engine = get_db_engine() 124 | 125 | if db_engine == "postgresql": 126 | r = run_psql(name, "-c", "SELECT 1") 127 | return r.status_code == 0 128 | 129 | if db_engine == "mysql": 130 | r = run_mysql(name, "-e", "SELECT 1") 131 | return r.status_code == 0 132 | 133 | assert db_engine == "sqlite3", f"{db_engine} cannot be tested properly!" 134 | assert TEST_DB_NAME != ":memory:", "sqlite in-memory database cannot be checked for existence!" 135 | return os.path.exists(name) 136 | 137 | 138 | def mark_database() -> None: 139 | db_engine = get_db_engine() 140 | 141 | if db_engine == "postgresql": 142 | r = run_psql(TEST_DB_NAME, "-c", "CREATE TABLE mark_table();") 143 | assert r.status_code == 0 144 | return 145 | 146 | if db_engine == "mysql": 147 | r = run_mysql(TEST_DB_NAME, "-e", "CREATE TABLE mark_table(kaka int);") 148 | assert r.status_code == 0 149 | return 150 | 151 | assert db_engine == "sqlite3", f"{db_engine} cannot be tested properly!" 152 | assert TEST_DB_NAME != ":memory:", "sqlite in-memory database cannot be marked!" 153 | 154 | conn = sqlite3.connect(TEST_DB_NAME) 155 | try: 156 | with conn: 157 | conn.execute("CREATE TABLE mark_table(kaka int);") 158 | finally: # Close the DB even if an error is raised 159 | conn.close() 160 | 161 | 162 | def mark_exists() -> bool: 163 | db_engine = get_db_engine() 164 | 165 | if db_engine == "postgresql": 166 | r = run_psql(TEST_DB_NAME, "-c", "SELECT 1 FROM mark_table") 167 | 168 | return r.status_code == 0 169 | 170 | if db_engine == "mysql": 171 | r = run_mysql(TEST_DB_NAME, "-e", "SELECT 1 FROM mark_table") 172 | 173 | return r.status_code == 0 174 | 175 | assert db_engine == "sqlite3", f"{db_engine} cannot be tested properly!" 176 | assert TEST_DB_NAME != ":memory:", "sqlite in-memory database cannot be checked for mark!" 177 | 178 | conn = sqlite3.connect(TEST_DB_NAME) 179 | try: 180 | with conn: 181 | conn.execute("SELECT 1 FROM mark_table") 182 | return True 183 | except sqlite3.OperationalError: 184 | return False 185 | finally: # Close the DB even if an error is raised 186 | conn.close() 187 | -------------------------------------------------------------------------------- /pytest_django_test/db_router.py: -------------------------------------------------------------------------------- 1 | class DbRouter: 2 | def db_for_read(self, model, **hints): 3 | if model._meta.app_label == "app" and model._meta.model_name == "seconditem": 4 | return "second" 5 | return None 6 | 7 | def db_for_write(self, model, **hints): 8 | if model._meta.app_label == "app" and model._meta.model_name == "seconditem": 9 | return "second" 10 | return None 11 | 12 | def allow_migrate(self, db, app_label, model_name=None, **hints): 13 | if app_label == "app" and model_name == "seconditem": 14 | return db == "second" 15 | -------------------------------------------------------------------------------- /pytest_django_test/settings_base.py: -------------------------------------------------------------------------------- 1 | ROOT_URLCONF = "pytest_django_test.urls" 2 | INSTALLED_APPS = [ 3 | "django.contrib.auth", 4 | "django.contrib.contenttypes", 5 | "django.contrib.sessions", 6 | "django.contrib.sites", 7 | "pytest_django_test.app", 8 | ] 9 | 10 | STATIC_URL = "/static/" 11 | SECRET_KEY = "foobar" 12 | 13 | MIDDLEWARE = [ 14 | "django.contrib.sessions.middleware.SessionMiddleware", 15 | "django.middleware.common.CommonMiddleware", 16 | "django.middleware.csrf.CsrfViewMiddleware", 17 | "django.contrib.auth.middleware.AuthenticationMiddleware", 18 | "django.contrib.messages.middleware.MessageMiddleware", 19 | ] 20 | 21 | 22 | TEMPLATES = [ 23 | { 24 | "BACKEND": "django.template.backends.django.DjangoTemplates", 25 | "DIRS": [], 26 | "APP_DIRS": True, 27 | "OPTIONS": {}, 28 | } 29 | ] 30 | 31 | DATABASE_ROUTERS = ["pytest_django_test.db_router.DbRouter"] 32 | 33 | USE_TZ = True 34 | -------------------------------------------------------------------------------- /pytest_django_test/settings_mysql.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | from .settings_base import * # noqa: F403 4 | 5 | 6 | DATABASES = { 7 | "default": { 8 | "ENGINE": "django.db.backends.mysql", 9 | "NAME": "pytest_django_tests_default", 10 | "USER": environ.get("TEST_DB_USER", "root"), 11 | "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), 12 | "HOST": environ.get("TEST_DB_HOST", "localhost"), 13 | "OPTIONS": { 14 | "charset": "utf8mb4", 15 | }, 16 | "TEST": { 17 | "CHARSET": "utf8mb4", 18 | "COLLATION": "utf8mb4_unicode_ci", 19 | }, 20 | }, 21 | "replica": { 22 | "ENGINE": "django.db.backends.mysql", 23 | "NAME": "pytest_django_tests_replica", 24 | "USER": environ.get("TEST_DB_USER", "root"), 25 | "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), 26 | "HOST": environ.get("TEST_DB_HOST", "localhost"), 27 | "OPTIONS": { 28 | "charset": "utf8mb4", 29 | }, 30 | "TEST": { 31 | "MIRROR": "default", 32 | "CHARSET": "utf8mb4", 33 | "COLLATION": "utf8mb4_unicode_ci", 34 | }, 35 | }, 36 | "second": { 37 | "ENGINE": "django.db.backends.mysql", 38 | "NAME": "pytest_django_tests_second", 39 | "USER": environ.get("TEST_DB_USER", "root"), 40 | "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), 41 | "HOST": environ.get("TEST_DB_HOST", "localhost"), 42 | "OPTIONS": { 43 | "charset": "utf8mb4", 44 | }, 45 | "TEST": { 46 | "CHARSET": "utf8mb4", 47 | "COLLATION": "utf8mb4_unicode_ci", 48 | }, 49 | }, 50 | } 51 | -------------------------------------------------------------------------------- /pytest_django_test/settings_postgres.py: -------------------------------------------------------------------------------- 1 | from os import environ 2 | 3 | from .settings_base import * # noqa: F403 4 | 5 | 6 | DATABASES = { 7 | "default": { 8 | "ENGINE": "django.db.backends.postgresql", 9 | "NAME": "pytest_django_tests_default", 10 | "USER": environ.get("TEST_DB_USER", ""), 11 | "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), 12 | "HOST": environ.get("TEST_DB_HOST", ""), 13 | }, 14 | "replica": { 15 | "ENGINE": "django.db.backends.postgresql", 16 | "NAME": "pytest_django_tests_replica", 17 | "USER": environ.get("TEST_DB_USER", ""), 18 | "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), 19 | "HOST": environ.get("TEST_DB_HOST", ""), 20 | "TEST": { 21 | "MIRROR": "default", 22 | }, 23 | }, 24 | "second": { 25 | "ENGINE": "django.db.backends.postgresql", 26 | "NAME": "pytest_django_tests_second", 27 | "USER": environ.get("TEST_DB_USER", ""), 28 | "PASSWORD": environ.get("TEST_DB_PASSWORD", ""), 29 | "HOST": environ.get("TEST_DB_HOST", ""), 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /pytest_django_test/settings_sqlite.py: -------------------------------------------------------------------------------- 1 | from .settings_base import * # noqa: F403 2 | 3 | 4 | DATABASES = { 5 | "default": { 6 | "ENGINE": "django.db.backends.sqlite3", 7 | "NAME": ":memory:", 8 | }, 9 | "replica": { 10 | "ENGINE": "django.db.backends.sqlite3", 11 | "NAME": ":memory:", 12 | "TEST": { 13 | "MIRROR": "default", 14 | }, 15 | }, 16 | "second": { 17 | "ENGINE": "django.db.backends.sqlite3", 18 | "NAME": ":memory:", 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /pytest_django_test/settings_sqlite_file.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | 3 | from .settings_base import * # noqa: F403 4 | 5 | 6 | # This is a SQLite configuration, which uses a file based database for 7 | # tests (via setting TEST_NAME / TEST['NAME']). 8 | 9 | # The name as expected / used by Django/pytest_django (tests/db_helpers.py). 10 | _fd, _filename_default = tempfile.mkstemp(prefix="test_") 11 | _fd, _filename_replica = tempfile.mkstemp(prefix="test_") 12 | _fd, _filename_second = tempfile.mkstemp(prefix="test_") 13 | 14 | DATABASES = { 15 | "default": { 16 | "ENGINE": "django.db.backends.sqlite3", 17 | "NAME": "/pytest_django_tests_default", 18 | "TEST": { 19 | "NAME": _filename_default, 20 | }, 21 | }, 22 | "replica": { 23 | "ENGINE": "django.db.backends.sqlite3", 24 | "NAME": "/pytest_django_tests_replica", 25 | "TEST": { 26 | "MIRROR": "default", 27 | "NAME": _filename_replica, 28 | }, 29 | }, 30 | "second": { 31 | "ENGINE": "django.db.backends.sqlite3", 32 | "NAME": "/pytest_django_tests_second", 33 | "TEST": { 34 | "NAME": _filename_second, 35 | }, 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /pytest_django_test/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .app import views 4 | 5 | 6 | urlpatterns = [ 7 | path("item_count/", views.item_count), 8 | path("admin-required/", views.admin_required_view), 9 | ] 10 | -------------------------------------------------------------------------------- /pytest_django_test/urls_overridden.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from django.urls import path 3 | 4 | 5 | urlpatterns = [ 6 | path("overridden_url/", lambda r: HttpResponse("Overridden urlconf works!")), 7 | ] 8 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytest-dev/pytest-django/7c99f33794113446ce865cbab825adb6ae7aee78/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import copy 4 | import os 5 | import pathlib 6 | import shutil 7 | from pathlib import Path 8 | from textwrap import dedent 9 | from typing import cast 10 | 11 | import pytest 12 | from django.conf import settings 13 | 14 | from .helpers import DjangoPytester 15 | 16 | 17 | pytest_plugins = "pytester" 18 | 19 | REPOSITORY_ROOT = pathlib.Path(__file__).parent.parent 20 | 21 | 22 | def pytest_configure(config: pytest.Config) -> None: 23 | config.addinivalue_line( 24 | "markers", 25 | "django_project: options for the django_pytester fixture", 26 | ) 27 | 28 | 29 | def _marker_apifun( 30 | extra_settings: str = "", 31 | create_manage_py: bool = False, 32 | project_root: str | None = None, 33 | create_settings: bool = True, 34 | ): 35 | return { 36 | "extra_settings": extra_settings, 37 | "create_manage_py": create_manage_py, 38 | "project_root": project_root, 39 | "create_settings": create_settings, 40 | } 41 | 42 | 43 | @pytest.fixture 44 | def pytester(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> pytest.Pytester: 45 | monkeypatch.delenv("PYTEST_ADDOPTS", raising=False) 46 | return pytester 47 | 48 | 49 | @pytest.fixture() 50 | def django_pytester( 51 | request: pytest.FixtureRequest, 52 | pytester: pytest.Pytester, 53 | monkeypatch: pytest.MonkeyPatch, 54 | ) -> DjangoPytester: 55 | from pytest_django_test.db_helpers import ( 56 | DB_NAME, 57 | SECOND_DB_NAME, 58 | SECOND_TEST_DB_NAME, 59 | TEST_DB_NAME, 60 | ) 61 | 62 | marker = request.node.get_closest_marker("django_project") 63 | 64 | options = _marker_apifun(**(marker.kwargs if marker else {})) 65 | 66 | if hasattr(request.node.cls, "db_settings"): 67 | db_settings = request.node.cls.db_settings 68 | else: 69 | db_settings = copy.deepcopy(settings.DATABASES) 70 | db_settings["default"]["NAME"] = DB_NAME 71 | db_settings["default"]["TEST"]["NAME"] = TEST_DB_NAME 72 | db_settings["second"]["NAME"] = SECOND_DB_NAME 73 | db_settings["second"].setdefault("TEST", {})["NAME"] = SECOND_TEST_DB_NAME 74 | 75 | test_settings = dedent( 76 | """ 77 | import django 78 | 79 | DATABASES = %(db_settings)s 80 | DATABASE_ROUTERS = ['pytest_django_test.db_router.DbRouter'] 81 | 82 | INSTALLED_APPS = [ 83 | 'django.contrib.auth', 84 | 'django.contrib.contenttypes', 85 | 'tpkg.app', 86 | ] 87 | SECRET_KEY = 'foobar' 88 | 89 | MIDDLEWARE = [ 90 | 'django.contrib.sessions.middleware.SessionMiddleware', 91 | 'django.middleware.common.CommonMiddleware', 92 | 'django.middleware.csrf.CsrfViewMiddleware', 93 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 94 | 'django.contrib.messages.middleware.MessageMiddleware', 95 | ] 96 | 97 | TEMPLATES = [ 98 | { 99 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 100 | 'DIRS': [], 101 | 'APP_DIRS': True, 102 | 'OPTIONS': {}, 103 | }, 104 | ] 105 | 106 | %(extra_settings)s 107 | """ 108 | ) % { 109 | "db_settings": repr(db_settings), 110 | "extra_settings": dedent(options["extra_settings"]), 111 | } 112 | 113 | if options["project_root"]: 114 | project_root = pytester.mkdir(options["project_root"]) 115 | else: 116 | project_root = pytester.path 117 | 118 | tpkg_path = project_root / "tpkg" 119 | tpkg_path.mkdir() 120 | 121 | if options["create_manage_py"]: 122 | project_root.joinpath("manage.py").write_text( 123 | dedent( 124 | """ 125 | #!/usr/bin/env python 126 | import sys 127 | from django.core.management import execute_from_command_line 128 | execute_from_command_line(sys.argv) 129 | """ 130 | ) 131 | ) 132 | 133 | tpkg_path.joinpath("__init__.py").touch() 134 | 135 | app_source = REPOSITORY_ROOT / "pytest_django_test/app" 136 | test_app_path = tpkg_path / "app" 137 | 138 | # Copy the test app to make it available in the new test run 139 | shutil.copytree(str(app_source), str(test_app_path)) 140 | if options["create_settings"]: 141 | tpkg_path.joinpath("the_settings.py").write_text(test_settings) 142 | 143 | # For suprocess tests, pytest's `pythonpath` setting doesn't currently 144 | # work, only the envvar does. 145 | pythonpath = os.pathsep.join(filter(None, [str(REPOSITORY_ROOT), os.getenv("PYTHONPATH", "")])) 146 | monkeypatch.setenv("PYTHONPATH", pythonpath) 147 | 148 | if options["create_settings"]: 149 | monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.the_settings") 150 | else: 151 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE", raising=False) 152 | 153 | def create_test_module(test_code: str, filename: str = "test_the_test.py") -> Path: 154 | r = tpkg_path.joinpath(filename) 155 | r.parent.mkdir(parents=True, exist_ok=True) 156 | r.write_text(dedent(test_code)) 157 | return r 158 | 159 | def create_app_file(code: str, filename: str) -> Path: 160 | r = test_app_path.joinpath(filename) 161 | r.parent.mkdir(parents=True, exist_ok=True) 162 | r.write_text(dedent(code)) 163 | return r 164 | 165 | pytester.makeini( 166 | """ 167 | [pytest] 168 | addopts = --strict-markers 169 | console_output_style=classic 170 | """ 171 | ) 172 | 173 | django_pytester_ = cast(DjangoPytester, pytester) 174 | django_pytester_.create_test_module = create_test_module # type: ignore[method-assign] 175 | django_pytester_.create_app_file = create_app_file # type: ignore[method-assign] 176 | django_pytester_.project_root = project_root 177 | 178 | return django_pytester_ 179 | 180 | 181 | @pytest.fixture 182 | def django_pytester_initial(django_pytester: DjangoPytester) -> pytest.Pytester: 183 | """A django_pytester fixture which provides initial_data.""" 184 | shutil.rmtree(django_pytester.project_root.joinpath("tpkg/app/migrations")) 185 | django_pytester.makefile( 186 | ".json", 187 | initial_data=""" 188 | [{ 189 | "pk": 1, 190 | "model": "app.item", 191 | "fields": { "name": "mark_initial_data" } 192 | }]""", 193 | ) 194 | 195 | return django_pytester 196 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | 6 | class DjangoPytester(pytest.Pytester): # type: ignore[misc] 7 | project_root: Path 8 | 9 | def create_test_module( # type: ignore[empty-body] 10 | self, 11 | test_code: str, 12 | filename: str = ..., 13 | ) -> Path: ... 14 | 15 | def create_app_file(self, code: str, filename: str) -> Path: # type: ignore[empty-body] 16 | ... 17 | -------------------------------------------------------------------------------- /tests/test_asserts.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests the dynamic loading of all Django assertion cases. 3 | """ 4 | 5 | from __future__ import annotations 6 | 7 | import inspect 8 | 9 | import pytest 10 | 11 | import pytest_django 12 | from pytest_django.asserts import __all__ as asserts_all 13 | 14 | 15 | def _get_actual_assertions_names() -> list[str]: 16 | """ 17 | Returns list with names of all assertion helpers in Django. 18 | """ 19 | from unittest import TestCase as DefaultTestCase 20 | 21 | from django import VERSION 22 | from django.test import TestCase as DjangoTestCase 23 | 24 | if VERSION >= (5, 0): 25 | from django.contrib.messages.test import MessagesTestMixin 26 | 27 | class MessagesTestCase(MessagesTestMixin, DjangoTestCase): 28 | pass 29 | 30 | obj = MessagesTestCase("run") 31 | else: 32 | obj = DjangoTestCase("run") 33 | 34 | def is_assert(func) -> bool: 35 | return func.startswith("assert") and "_" not in func 36 | 37 | base_methods = [ 38 | name for name, member in inspect.getmembers(DefaultTestCase) if is_assert(name) 39 | ] 40 | 41 | return [ 42 | name 43 | for name, member in inspect.getmembers(obj) 44 | if is_assert(name) and name not in base_methods 45 | ] 46 | 47 | 48 | def test_django_asserts_available() -> None: 49 | django_assertions = _get_actual_assertions_names() 50 | expected_assertions = asserts_all 51 | assert set(django_assertions) == set(expected_assertions) 52 | 53 | for name in expected_assertions: 54 | assert hasattr(pytest_django.asserts, name) 55 | 56 | 57 | @pytest.mark.django_db 58 | def test_sanity() -> None: 59 | from django.http import HttpResponse 60 | 61 | from pytest_django.asserts import assertContains, assertNumQueries 62 | 63 | response = HttpResponse("My response") 64 | 65 | assertContains(response, "My response") 66 | with pytest.raises(AssertionError): 67 | assertContains(response, "Not my response") 68 | 69 | assertNumQueries(0, lambda: 1 + 1) 70 | with assertNumQueries(0): 71 | pass 72 | 73 | assert assertContains.__doc__ 74 | -------------------------------------------------------------------------------- /tests/test_database.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Generator 4 | 5 | import pytest 6 | from django.db import connection, transaction 7 | 8 | from .helpers import DjangoPytester 9 | 10 | from pytest_django_test.app.models import Item, SecondItem 11 | 12 | 13 | def db_supports_reset_sequences() -> bool: 14 | """Return if the current db engine supports `reset_sequences`.""" 15 | ret: bool = ( 16 | connection.features.supports_transactions and connection.features.supports_sequence_reset 17 | ) 18 | return ret 19 | 20 | 21 | def test_noaccess() -> None: 22 | with pytest.raises(RuntimeError): 23 | Item.objects.create(name="spam") 24 | with pytest.raises(RuntimeError): 25 | Item.objects.count() 26 | 27 | 28 | @pytest.fixture 29 | def noaccess() -> None: 30 | with pytest.raises(RuntimeError): 31 | Item.objects.create(name="spam") 32 | with pytest.raises(RuntimeError): 33 | Item.objects.count() 34 | 35 | 36 | def test_noaccess_fixture(noaccess: None) -> None: 37 | # Setup will fail if this test needs to fail 38 | pass 39 | 40 | 41 | @pytest.fixture 42 | def non_zero_sequences_counter(db: None) -> None: 43 | """Ensure that the db's internal sequence counter is > 1. 44 | 45 | This is used to test the `reset_sequences` feature. 46 | """ 47 | item_1 = Item.objects.create(name="item_1") 48 | item_2 = Item.objects.create(name="item_2") 49 | item_1.delete() 50 | item_2.delete() 51 | 52 | 53 | class TestDatabaseFixtures: 54 | """Tests for the different database fixtures.""" 55 | 56 | @pytest.fixture( 57 | params=[ 58 | "db", 59 | "transactional_db", 60 | "django_db_reset_sequences", 61 | "django_db_serialized_rollback", 62 | ] 63 | ) 64 | def all_dbs(self, request: pytest.FixtureRequest) -> None: 65 | if request.param == "django_db_reset_sequences": 66 | request.getfixturevalue("django_db_reset_sequences") 67 | elif request.param == "transactional_db": 68 | request.getfixturevalue("transactional_db") 69 | elif request.param == "db": 70 | request.getfixturevalue("db") 71 | elif request.param == "django_db_serialized_rollback": 72 | request.getfixturevalue("django_db_serialized_rollback") 73 | else: 74 | raise AssertionError() # pragma: no cover 75 | 76 | def test_access(self, all_dbs: None) -> None: 77 | Item.objects.create(name="spam") 78 | 79 | def test_clean_db(self, all_dbs: None) -> None: 80 | # Relies on the order: test_access created an object 81 | assert Item.objects.count() == 0 82 | 83 | def test_transactions_disabled(self, db: None) -> None: 84 | if not connection.features.supports_transactions: 85 | pytest.skip("transactions required for this test") 86 | 87 | assert connection.in_atomic_block 88 | 89 | def test_transactions_enabled(self, transactional_db: None) -> None: 90 | if not connection.features.supports_transactions: 91 | pytest.skip("transactions required for this test") 92 | 93 | assert not connection.in_atomic_block 94 | 95 | def test_transactions_enabled_via_reset_seq( 96 | self, 97 | django_db_reset_sequences: None, 98 | ) -> None: 99 | if not connection.features.supports_transactions: 100 | pytest.skip("transactions required for this test") 101 | 102 | assert not connection.in_atomic_block 103 | 104 | def test_django_db_reset_sequences_fixture( 105 | self, 106 | db: None, 107 | django_pytester: DjangoPytester, 108 | non_zero_sequences_counter: None, 109 | ) -> None: 110 | if not db_supports_reset_sequences(): 111 | pytest.skip( 112 | "transactions and reset_sequences must be supported " 113 | "by the database to run this test" 114 | ) 115 | 116 | # The test runs on a database that already contains objects, so its 117 | # id counter is > 1. We check for the ids of newly created objects. 118 | django_pytester.create_test_module( 119 | """ 120 | import pytest 121 | from .app.models import Item 122 | 123 | def test_django_db_reset_sequences_requested( 124 | django_db_reset_sequences): 125 | item = Item.objects.create(name='new_item') 126 | assert item.id == 1 127 | """ 128 | ) 129 | 130 | result = django_pytester.runpytest_subprocess("-v", "--reuse-db") 131 | result.stdout.fnmatch_lines(["*test_django_db_reset_sequences_requested PASSED*"]) 132 | 133 | def test_serialized_rollback(self, db: None, django_pytester: DjangoPytester) -> None: 134 | django_pytester.create_app_file( 135 | """ 136 | from django.db import migrations 137 | 138 | def load_data(apps, schema_editor): 139 | Item = apps.get_model("app", "Item") 140 | Item.objects.create(name="loaded-in-migration") 141 | 142 | class Migration(migrations.Migration): 143 | dependencies = [ 144 | ("app", "0001_initial"), 145 | ] 146 | 147 | operations = [ 148 | migrations.RunPython(load_data), 149 | ] 150 | """, 151 | "migrations/0002_data_migration.py", 152 | ) 153 | 154 | django_pytester.create_test_module( 155 | """ 156 | import pytest 157 | from .app.models import Item 158 | 159 | @pytest.mark.django_db(transaction=True, serialized_rollback=True) 160 | def test_serialized_rollback_1(): 161 | assert Item.objects.filter(name="loaded-in-migration").exists() 162 | 163 | @pytest.mark.django_db(transaction=True) 164 | def test_serialized_rollback_2(django_db_serialized_rollback): 165 | assert Item.objects.filter(name="loaded-in-migration").exists() 166 | Item.objects.create(name="test2") 167 | 168 | @pytest.mark.django_db(transaction=True, serialized_rollback=True) 169 | def test_serialized_rollback_3(): 170 | assert Item.objects.filter(name="loaded-in-migration").exists() 171 | assert not Item.objects.filter(name="test2").exists() 172 | """ 173 | ) 174 | 175 | result = django_pytester.runpytest_subprocess("-v") 176 | assert result.ret == 0 177 | 178 | @pytest.fixture 179 | def mydb(self, all_dbs: None) -> None: 180 | # This fixture must be able to access the database 181 | Item.objects.create(name="spam") 182 | 183 | def test_mydb(self, mydb: None) -> None: 184 | if not connection.features.supports_transactions: 185 | pytest.skip("transactions required for this test") 186 | 187 | # Check the fixture had access to the db 188 | item = Item.objects.get(name="spam") 189 | assert item 190 | 191 | def test_fixture_clean(self, all_dbs: None) -> None: 192 | # Relies on the order: test_mydb created an object 193 | # See https://github.com/pytest-dev/pytest-django/issues/17 194 | assert Item.objects.count() == 0 195 | 196 | @pytest.fixture 197 | def fin(self, request: pytest.FixtureRequest, all_dbs: None) -> Generator[None, None, None]: 198 | # This finalizer must be able to access the database 199 | yield 200 | Item.objects.create(name="spam") 201 | 202 | def test_fin(self, fin: None) -> None: 203 | # Check finalizer has db access (teardown will fail if not) 204 | pass 205 | 206 | def test_durable_transactions(self, all_dbs: None) -> None: 207 | with transaction.atomic(durable=True): 208 | item = Item.objects.create(name="foo") 209 | assert Item.objects.get() == item 210 | 211 | 212 | class TestDatabaseFixturesAllOrder: 213 | @pytest.fixture 214 | def fixture_with_db(self, db: None) -> None: 215 | Item.objects.create(name="spam") 216 | 217 | @pytest.fixture 218 | def fixture_with_transdb(self, transactional_db: None) -> None: 219 | Item.objects.create(name="spam") 220 | 221 | @pytest.fixture 222 | def fixture_with_reset_sequences(self, django_db_reset_sequences: None) -> None: 223 | Item.objects.create(name="spam") 224 | 225 | @pytest.fixture 226 | def fixture_with_serialized_rollback(self, django_db_serialized_rollback: None) -> None: 227 | Item.objects.create(name="ham") 228 | 229 | def test_trans(self, fixture_with_transdb: None) -> None: 230 | pass 231 | 232 | def test_db(self, fixture_with_db: None) -> None: 233 | pass 234 | 235 | def test_db_trans(self, fixture_with_db: None, fixture_with_transdb: None) -> None: 236 | pass 237 | 238 | def test_trans_db(self, fixture_with_transdb: None, fixture_with_db: None) -> None: 239 | pass 240 | 241 | def test_reset_sequences( 242 | self, 243 | fixture_with_reset_sequences: None, 244 | fixture_with_transdb: None, 245 | fixture_with_db: None, 246 | ) -> None: 247 | pass 248 | 249 | # The test works when transactions are not supported, but it interacts 250 | # badly with other tests. 251 | @pytest.mark.skipif("not connection.features.supports_transactions") 252 | def test_serialized_rollback( 253 | self, 254 | fixture_with_serialized_rollback: None, 255 | fixture_with_db: None, 256 | ) -> None: 257 | pass 258 | 259 | 260 | class TestDatabaseMarker: 261 | "Tests for the django_db marker." 262 | 263 | @pytest.mark.django_db 264 | def test_access(self) -> None: 265 | Item.objects.create(name="spam") 266 | 267 | @pytest.mark.django_db 268 | def test_clean_db(self) -> None: 269 | # Relies on the order: test_access created an object. 270 | assert Item.objects.count() == 0 271 | 272 | @pytest.mark.django_db 273 | def test_transactions_disabled(self) -> None: 274 | if not connection.features.supports_transactions: 275 | pytest.skip("transactions required for this test") 276 | 277 | assert connection.in_atomic_block 278 | 279 | @pytest.mark.django_db(transaction=False) 280 | def test_transactions_disabled_explicit(self) -> None: 281 | if not connection.features.supports_transactions: 282 | pytest.skip("transactions required for this test") 283 | 284 | assert connection.in_atomic_block 285 | 286 | @pytest.mark.django_db(transaction=True) 287 | def test_transactions_enabled(self) -> None: 288 | if not connection.features.supports_transactions: 289 | pytest.skip("transactions required for this test") 290 | 291 | assert not connection.in_atomic_block 292 | 293 | @pytest.mark.django_db 294 | def test_reset_sequences_disabled(self, request: pytest.FixtureRequest) -> None: 295 | marker = request.node.get_closest_marker("django_db") 296 | assert not marker.kwargs 297 | 298 | @pytest.mark.django_db(reset_sequences=True) 299 | def test_reset_sequences_enabled(self, request: pytest.FixtureRequest) -> None: 300 | marker = request.node.get_closest_marker("django_db") 301 | assert marker.kwargs["reset_sequences"] 302 | 303 | @pytest.mark.django_db(transaction=True, reset_sequences=True) 304 | def test_transaction_reset_sequences_enabled(self, request: pytest.FixtureRequest) -> None: 305 | marker = request.node.get_closest_marker("django_db") 306 | assert marker.kwargs["reset_sequences"] 307 | 308 | @pytest.mark.django_db(databases=["default", "replica", "second"]) 309 | def test_databases(self, request: pytest.FixtureRequest) -> None: 310 | marker = request.node.get_closest_marker("django_db") 311 | assert marker.kwargs["databases"] == ["default", "replica", "second"] 312 | 313 | @pytest.mark.django_db(databases=["second"]) 314 | def test_second_database(self, request: pytest.FixtureRequest) -> None: 315 | SecondItem.objects.create(name="spam") 316 | 317 | @pytest.mark.django_db(databases=["default"]) 318 | def test_not_allowed_database(self, request: pytest.FixtureRequest) -> None: 319 | with pytest.raises(AssertionError, match="not allowed"): 320 | SecondItem.objects.count() 321 | with pytest.raises(AssertionError, match="not allowed"): 322 | SecondItem.objects.create(name="spam") 323 | 324 | @pytest.mark.django_db(databases=["replica"]) 325 | def test_replica_database(self, request: pytest.FixtureRequest) -> None: 326 | Item.objects.using("replica").count() 327 | 328 | @pytest.mark.django_db(databases=["replica"]) 329 | def test_replica_database_not_allowed(self, request: pytest.FixtureRequest) -> None: 330 | with pytest.raises(AssertionError, match="not allowed"): 331 | Item.objects.count() 332 | 333 | @pytest.mark.django_db(transaction=True, databases=["default", "replica"]) 334 | def test_replica_mirrors_default_database(self, request: pytest.FixtureRequest) -> None: 335 | Item.objects.create(name="spam") 336 | Item.objects.using("replica").create(name="spam") 337 | 338 | assert Item.objects.count() == 2 339 | assert Item.objects.using("replica").count() == 2 340 | 341 | @pytest.mark.django_db(databases="__all__") 342 | def test_all_databases(self, request: pytest.FixtureRequest) -> None: 343 | Item.objects.count() 344 | Item.objects.create(name="spam") 345 | SecondItem.objects.count() 346 | SecondItem.objects.create(name="spam") 347 | 348 | @pytest.mark.django_db 349 | def test_serialized_rollback_disabled(self, request: pytest.FixtureRequest): 350 | marker = request.node.get_closest_marker("django_db") 351 | assert not marker.kwargs 352 | 353 | # The test works when transactions are not supported, but it interacts 354 | # badly with other tests. 355 | @pytest.mark.skipif("not connection.features.supports_transactions") 356 | @pytest.mark.django_db(serialized_rollback=True) 357 | def test_serialized_rollback_enabled(self, request: pytest.FixtureRequest): 358 | marker = request.node.get_closest_marker("django_db") 359 | assert marker.kwargs["serialized_rollback"] 360 | 361 | @pytest.mark.django_db 362 | def test_available_apps_disabled(self, request: pytest.FixtureRequest) -> None: 363 | marker = request.node.get_closest_marker("django_db") 364 | assert not marker.kwargs 365 | 366 | @pytest.mark.django_db(available_apps=["pytest_django_test.app"]) 367 | def test_available_apps_enabled(self, request: pytest.FixtureRequest) -> None: 368 | marker = request.node.get_closest_marker("django_db") 369 | assert marker.kwargs["available_apps"] == ["pytest_django_test.app"] 370 | 371 | @pytest.mark.django_db 372 | def test_available_apps_default(self, request: pytest.FixtureRequest) -> None: 373 | from django.apps import apps 374 | from django.conf import settings 375 | 376 | for app in settings.INSTALLED_APPS: 377 | assert apps.is_installed(app) 378 | 379 | @pytest.mark.django_db(available_apps=["pytest_django_test.app"]) 380 | def test_available_apps_limited(self, request: pytest.FixtureRequest) -> None: 381 | from django.apps import apps 382 | from django.conf import settings 383 | 384 | assert apps.is_installed("pytest_django_test.app") 385 | 386 | for app in settings.INSTALLED_APPS: 387 | if app != "pytest_django_test.app": 388 | assert not apps.is_installed(app) 389 | 390 | 391 | def test_unittest_interaction(django_pytester: DjangoPytester) -> None: 392 | "Test that (non-Django) unittests cannot access the DB." 393 | 394 | django_pytester.create_test_module( 395 | """ 396 | import pytest 397 | import unittest 398 | from .app.models import Item 399 | 400 | class TestCase_setupClass(unittest.TestCase): 401 | @classmethod 402 | def setUpClass(cls): 403 | Item.objects.create(name='foo') 404 | 405 | def test_db_access_1(self): 406 | Item.objects.count() == 1 407 | 408 | class TestCase_setUp(unittest.TestCase): 409 | @classmethod 410 | def setUp(cls): 411 | Item.objects.create(name='foo') 412 | 413 | def test_db_access_2(self): 414 | Item.objects.count() == 1 415 | 416 | class TestCase(unittest.TestCase): 417 | def test_db_access_3(self): 418 | Item.objects.count() == 1 419 | """ 420 | ) 421 | 422 | result = django_pytester.runpytest_subprocess("-v", "--reuse-db") 423 | result.stdout.fnmatch_lines( 424 | [ 425 | "*test_db_access_1 ERROR*", 426 | "*test_db_access_2 FAILED*", 427 | "*test_db_access_3 FAILED*", 428 | "*ERROR at setup of TestCase_setupClass.test_db_access_1*", 429 | '*RuntimeError: Database access not allowed, use the "django_db" mark, ' 430 | 'or the "db" or "transactional_db" fixtures to enable it.', 431 | ] 432 | ) 433 | 434 | 435 | def test_django_testcase_multi_db(django_pytester: DjangoPytester) -> None: 436 | """Test that Django TestCase multi-db support works.""" 437 | 438 | django_pytester.create_test_module( 439 | """ 440 | import pytest 441 | from django.test import TestCase 442 | from .app.models import Item, SecondItem 443 | 444 | class TestCase(TestCase): 445 | databases = ["default", "second"] 446 | 447 | def test_db_access(self): 448 | Item.objects.count() == 0 449 | SecondItem.objects.count() == 0 450 | """ 451 | ) 452 | 453 | result = django_pytester.runpytest_subprocess("-v", "--reuse-db") 454 | result.assert_outcomes(passed=1) 455 | 456 | 457 | class Test_database_blocking: 458 | def test_db_access_in_conftest(self, django_pytester: DjangoPytester) -> None: 459 | """Make sure database access in conftest module is prohibited.""" 460 | 461 | django_pytester.makeconftest( 462 | """ 463 | from tpkg.app.models import Item 464 | Item.objects.get() 465 | """ 466 | ) 467 | 468 | result = django_pytester.runpytest_subprocess("-v") 469 | result.stderr.fnmatch_lines( 470 | [ 471 | '*RuntimeError: Database access not allowed, use the "django_db" mark, ' 472 | 'or the "db" or "transactional_db" fixtures to enable it.*' 473 | ] 474 | ) 475 | 476 | def test_db_access_in_test_module(self, django_pytester: DjangoPytester) -> None: 477 | django_pytester.create_test_module( 478 | """ 479 | from tpkg.app.models import Item 480 | Item.objects.get() 481 | """ 482 | ) 483 | 484 | result = django_pytester.runpytest_subprocess("-v") 485 | result.stdout.fnmatch_lines( 486 | [ 487 | '*RuntimeError: Database access not allowed, use the "django_db" mark, ' 488 | 'or the "db" or "transactional_db" fixtures to enable it.' 489 | ] 490 | ) 491 | -------------------------------------------------------------------------------- /tests/test_db_access_in_repr.py: -------------------------------------------------------------------------------- 1 | from .helpers import DjangoPytester 2 | 3 | 4 | def test_db_access_with_repr_in_report(django_pytester: DjangoPytester) -> None: 5 | django_pytester.create_test_module( 6 | """ 7 | import pytest 8 | 9 | from .app.models import Item 10 | 11 | def test_via_db_blocker(django_db_setup, django_db_blocker): 12 | with django_db_blocker.unblock(): 13 | Item.objects.get(name='This one is not there') 14 | 15 | def test_via_db_fixture(db): 16 | Item.objects.get(name='This one is not there') 17 | """ 18 | ) 19 | 20 | result = django_pytester.runpytest_subprocess("--tb=auto") 21 | result.stdout.fnmatch_lines( 22 | [ 23 | "tpkg/test_the_test.py FF", 24 | "E *DoesNotExist: Item matching query does not exist.", 25 | "tpkg/test_the_test.py:8: ", 26 | "self = *RuntimeError*Database access not allowed*", 27 | "E *DoesNotExist: Item matching query does not exist.", 28 | "* 2 failed*", 29 | ] 30 | ) 31 | assert "INTERNALERROR" not in str(result.stdout) + str(result.stderr) 32 | assert result.ret == 1 33 | -------------------------------------------------------------------------------- /tests/test_django_configurations.py: -------------------------------------------------------------------------------- 1 | """Tests which check the various ways you can set DJANGO_SETTINGS_MODULE 2 | 3 | If these tests fail you probably forgot to install django-configurations. 4 | """ 5 | 6 | import pytest 7 | 8 | 9 | pytest.importorskip("configurations") 10 | 11 | 12 | BARE_SETTINGS = """ 13 | from configurations import Configuration 14 | 15 | class MySettings(Configuration): 16 | # At least one database must be configured 17 | DATABASES = { 18 | 'default': { 19 | 'ENGINE': 'django.db.backends.sqlite3', 20 | 'NAME': ':memory:' 21 | }, 22 | } 23 | 24 | SECRET_KEY = 'foobar' 25 | """ 26 | 27 | 28 | def test_dc_env(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: 29 | monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.settings_env") 30 | monkeypatch.setenv("DJANGO_CONFIGURATION", "MySettings") 31 | 32 | pkg = pytester.mkpydir("tpkg") 33 | settings = pkg.joinpath("settings_env.py") 34 | settings.write_text(BARE_SETTINGS) 35 | pytester.makepyfile( 36 | """ 37 | import os 38 | 39 | def test_settings(): 40 | assert os.environ['DJANGO_SETTINGS_MODULE'] == 'tpkg.settings_env' 41 | assert os.environ['DJANGO_CONFIGURATION'] == 'MySettings' 42 | """ 43 | ) 44 | result = pytester.runpytest_subprocess() 45 | result.stdout.fnmatch_lines( 46 | [ 47 | "django: version: *, settings: tpkg.settings_env (from env), " 48 | "configuration: MySettings (from env)", 49 | "* 1 passed*", 50 | ] 51 | ) 52 | assert result.ret == 0 53 | 54 | 55 | def test_dc_env_overrides_ini(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: 56 | monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.settings_env") 57 | monkeypatch.setenv("DJANGO_CONFIGURATION", "MySettings") 58 | 59 | pytester.makeini( 60 | """ 61 | [pytest] 62 | DJANGO_SETTINGS_MODULE = DO_NOT_USE_ini 63 | DJANGO_CONFIGURATION = DO_NOT_USE_ini 64 | """ 65 | ) 66 | pkg = pytester.mkpydir("tpkg") 67 | settings = pkg.joinpath("settings_env.py") 68 | settings.write_text(BARE_SETTINGS) 69 | pytester.makepyfile( 70 | """ 71 | import os 72 | 73 | def test_ds(): 74 | assert os.environ['DJANGO_SETTINGS_MODULE'] == 'tpkg.settings_env' 75 | assert os.environ['DJANGO_CONFIGURATION'] == 'MySettings' 76 | """ 77 | ) 78 | result = pytester.runpytest_subprocess() 79 | result.stdout.fnmatch_lines( 80 | [ 81 | "django: version: *, settings: tpkg.settings_env (from env), " 82 | "configuration: MySettings (from env)", 83 | "* 1 passed*", 84 | ] 85 | ) 86 | assert result.ret == 0 87 | 88 | 89 | def test_dc_ini(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: 90 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 91 | 92 | pytester.makeini( 93 | """ 94 | [pytest] 95 | DJANGO_SETTINGS_MODULE = tpkg.settings_ini 96 | DJANGO_CONFIGURATION = MySettings 97 | """ 98 | ) 99 | pkg = pytester.mkpydir("tpkg") 100 | settings = pkg.joinpath("settings_ini.py") 101 | settings.write_text(BARE_SETTINGS) 102 | pytester.makepyfile( 103 | """ 104 | import os 105 | 106 | def test_ds(): 107 | assert os.environ['DJANGO_SETTINGS_MODULE'] == 'tpkg.settings_ini' 108 | assert os.environ['DJANGO_CONFIGURATION'] == 'MySettings' 109 | """ 110 | ) 111 | result = pytester.runpytest_subprocess() 112 | result.stdout.fnmatch_lines( 113 | [ 114 | "django: version: *, settings: tpkg.settings_ini (from ini), " 115 | "configuration: MySettings (from ini)", 116 | "* 1 passed*", 117 | ] 118 | ) 119 | assert result.ret == 0 120 | 121 | 122 | def test_dc_option(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: 123 | monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "DO_NOT_USE_env") 124 | monkeypatch.setenv("DJANGO_CONFIGURATION", "DO_NOT_USE_env") 125 | 126 | pytester.makeini( 127 | """ 128 | [pytest] 129 | DJANGO_SETTINGS_MODULE = DO_NOT_USE_ini 130 | DJANGO_CONFIGURATION = DO_NOT_USE_ini 131 | """ 132 | ) 133 | pkg = pytester.mkpydir("tpkg") 134 | settings = pkg.joinpath("settings_opt.py") 135 | settings.write_text(BARE_SETTINGS) 136 | pytester.makepyfile( 137 | """ 138 | import os 139 | 140 | def test_ds(): 141 | assert os.environ['DJANGO_SETTINGS_MODULE'] == 'tpkg.settings_opt' 142 | assert os.environ['DJANGO_CONFIGURATION'] == 'MySettings' 143 | """ 144 | ) 145 | result = pytester.runpytest_subprocess("--ds=tpkg.settings_opt", "--dc=MySettings") 146 | result.stdout.fnmatch_lines( 147 | [ 148 | "django: version: *, settings: tpkg.settings_opt (from option)," 149 | " configuration: MySettings (from option)", 150 | "* 1 passed*", 151 | ] 152 | ) 153 | assert result.ret == 0 154 | -------------------------------------------------------------------------------- /tests/test_django_settings_module.py: -------------------------------------------------------------------------------- 1 | """Tests which check the various ways you can set DJANGO_SETTINGS_MODULE 2 | 3 | If these tests fail you probably forgot to run "pip install -e .". 4 | """ 5 | 6 | import pytest 7 | 8 | 9 | BARE_SETTINGS = """ 10 | # At least one database must be configured 11 | DATABASES = { 12 | 'default': { 13 | 'ENGINE': 'django.db.backends.sqlite3', 14 | 'NAME': ':memory:' 15 | }, 16 | } 17 | SECRET_KEY = 'foobar' 18 | """ 19 | 20 | 21 | def test_ds_ini(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: 22 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 23 | pytester.makeini( 24 | """ 25 | [pytest] 26 | DJANGO_SETTINGS_MODULE = tpkg.settings_ini 27 | """ 28 | ) 29 | pkg = pytester.mkpydir("tpkg") 30 | pkg.joinpath("settings_ini.py").write_text(BARE_SETTINGS) 31 | pytester.makepyfile( 32 | """ 33 | import os 34 | 35 | def test_ds(): 36 | assert os.environ['DJANGO_SETTINGS_MODULE'] == 'tpkg.settings_ini' 37 | """ 38 | ) 39 | result = pytester.runpytest_subprocess() 40 | result.stdout.fnmatch_lines( 41 | [ 42 | "django: version: *, settings: tpkg.settings_ini (from ini)", 43 | "*= 1 passed*", 44 | ] 45 | ) 46 | assert result.ret == 0 47 | 48 | 49 | def test_ds_env(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: 50 | monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.settings_env") 51 | pkg = pytester.mkpydir("tpkg") 52 | settings = pkg.joinpath("settings_env.py") 53 | settings.write_text(BARE_SETTINGS) 54 | pytester.makepyfile( 55 | """ 56 | import os 57 | 58 | def test_settings(): 59 | assert os.environ['DJANGO_SETTINGS_MODULE'] == 'tpkg.settings_env' 60 | """ 61 | ) 62 | result = pytester.runpytest_subprocess() 63 | result.stdout.fnmatch_lines( 64 | [ 65 | "django: version: *, settings: tpkg.settings_env (from env)", 66 | "*= 1 passed*", 67 | ] 68 | ) 69 | 70 | 71 | def test_ds_option(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: 72 | monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "DO_NOT_USE_env") 73 | pytester.makeini( 74 | """ 75 | [pytest] 76 | DJANGO_SETTINGS_MODULE = DO_NOT_USE_ini 77 | """ 78 | ) 79 | pkg = pytester.mkpydir("tpkg") 80 | settings = pkg.joinpath("settings_opt.py") 81 | settings.write_text(BARE_SETTINGS) 82 | pytester.makepyfile( 83 | """ 84 | import os 85 | 86 | def test_ds(): 87 | assert os.environ['DJANGO_SETTINGS_MODULE'] == 'tpkg.settings_opt' 88 | """ 89 | ) 90 | result = pytester.runpytest_subprocess("--ds=tpkg.settings_opt") 91 | result.stdout.fnmatch_lines( 92 | [ 93 | "django: version: *, settings: tpkg.settings_opt (from option)", 94 | "*= 1 passed*", 95 | ] 96 | ) 97 | 98 | 99 | def test_ds_env_override_ini(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: 100 | "DSM env should override ini." 101 | monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "tpkg.settings_env") 102 | pytester.makeini( 103 | """\ 104 | [pytest] 105 | DJANGO_SETTINGS_MODULE = DO_NOT_USE_ini 106 | """ 107 | ) 108 | pkg = pytester.mkpydir("tpkg") 109 | settings = pkg.joinpath("settings_env.py") 110 | settings.write_text(BARE_SETTINGS) 111 | pytester.makepyfile( 112 | """ 113 | import os 114 | 115 | def test_ds(): 116 | assert os.environ['DJANGO_SETTINGS_MODULE'] == 'tpkg.settings_env' 117 | """ 118 | ) 119 | result = pytester.runpytest_subprocess() 120 | assert result.parseoutcomes()["passed"] == 1 121 | assert result.ret == 0 122 | 123 | 124 | def test_ds_non_existent(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: 125 | """ 126 | Make sure we do not fail with INTERNALERROR if an incorrect 127 | DJANGO_SETTINGS_MODULE is given. 128 | """ 129 | monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "DOES_NOT_EXIST") 130 | pytester.makepyfile("def test_ds(): pass") 131 | result = pytester.runpytest_subprocess() 132 | result.stderr.fnmatch_lines(["*ImportError:*DOES_NOT_EXIST*"]) 133 | assert result.ret != 0 134 | 135 | 136 | def test_ds_after_user_conftest( 137 | pytester: pytest.Pytester, 138 | monkeypatch: pytest.MonkeyPatch, 139 | ) -> None: 140 | """ 141 | Test that the settings module can be imported, after pytest has adjusted 142 | the sys.path. 143 | """ 144 | monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "settings_after_conftest") 145 | pytester.makepyfile("def test_ds(): pass") 146 | pytester.makepyfile(settings_after_conftest="SECRET_KEY='secret'") 147 | # pytester.makeconftest("import sys; print(sys.path)") 148 | result = pytester.runpytest_subprocess("-v") 149 | result.stdout.fnmatch_lines(["* 1 passed*"]) 150 | assert result.ret == 0 151 | 152 | 153 | def test_ds_in_pytest_configure( 154 | pytester: pytest.Pytester, 155 | monkeypatch: pytest.MonkeyPatch, 156 | ) -> None: 157 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 158 | pkg = pytester.mkpydir("tpkg") 159 | settings = pkg.joinpath("settings_ds.py") 160 | settings.write_text(BARE_SETTINGS) 161 | pytester.makeconftest( 162 | """ 163 | import os 164 | 165 | from django.conf import settings 166 | 167 | def pytest_configure(): 168 | if not settings.configured: 169 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 170 | 'tpkg.settings_ds') 171 | """ 172 | ) 173 | 174 | pytester.makepyfile( 175 | """ 176 | def test_anything(): 177 | pass 178 | """ 179 | ) 180 | 181 | r = pytester.runpytest_subprocess() 182 | assert r.parseoutcomes()["passed"] == 1 183 | assert r.ret == 0 184 | 185 | 186 | def test_django_settings_configure( 187 | pytester: pytest.Pytester, 188 | monkeypatch: pytest.MonkeyPatch, 189 | ) -> None: 190 | """ 191 | Make sure Django can be configured without setting 192 | DJANGO_SETTINGS_MODULE altogether, relying on calling 193 | django.conf.settings.configure() and then invoking pytest. 194 | """ 195 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 196 | 197 | p = pytester.makepyfile( 198 | run=""" 199 | from django.conf import settings 200 | settings.configure(SECRET_KEY='set from settings.configure()', 201 | DATABASES={'default': { 202 | 'ENGINE': 'django.db.backends.sqlite3', 203 | 'NAME': ':memory:' 204 | }}, 205 | INSTALLED_APPS=['django.contrib.auth', 206 | 'django.contrib.contenttypes',]) 207 | 208 | import pytest 209 | 210 | pytest.main() 211 | """ 212 | ) 213 | 214 | pytester.makepyfile( 215 | """ 216 | import pytest 217 | 218 | from django.conf import settings 219 | from django.test import RequestFactory, TestCase 220 | from django.contrib.auth.models import User 221 | 222 | def test_access_to_setting(): 223 | assert settings.SECRET_KEY == 'set from settings.configure()' 224 | 225 | # This test requires Django to be properly configured to be run 226 | def test_rf(rf): 227 | assert isinstance(rf, RequestFactory) 228 | 229 | # This tests that pytest-django actually configures the database 230 | # according to the settings above 231 | class ATestCase(TestCase): 232 | def test_user_count(self): 233 | assert User.objects.count() == 0 234 | 235 | @pytest.mark.django_db 236 | def test_user_count(): 237 | assert User.objects.count() == 0 238 | 239 | """ 240 | ) 241 | result = pytester.runpython(p) 242 | result.stdout.fnmatch_lines(["* 4 passed*"]) 243 | 244 | 245 | def test_settings_in_hook(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: 246 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 247 | pytester.makeconftest( 248 | """ 249 | from django.conf import settings 250 | 251 | def pytest_configure(): 252 | settings.configure(SECRET_KEY='set from pytest_configure', 253 | DATABASES={'default': { 254 | 'ENGINE': 'django.db.backends.sqlite3', 255 | 'NAME': ':memory:'}}, 256 | INSTALLED_APPS=['django.contrib.auth', 257 | 'django.contrib.contenttypes',]) 258 | """ 259 | ) 260 | pytester.makepyfile( 261 | """ 262 | import pytest 263 | from django.conf import settings 264 | from django.contrib.auth.models import User 265 | 266 | def test_access_to_setting(): 267 | assert settings.SECRET_KEY == 'set from pytest_configure' 268 | 269 | @pytest.mark.django_db 270 | def test_user_count(): 271 | assert User.objects.count() == 0 272 | """ 273 | ) 274 | r = pytester.runpytest_subprocess() 275 | assert r.ret == 0 276 | 277 | 278 | def test_django_not_loaded_without_settings( 279 | pytester: pytest.Pytester, 280 | monkeypatch: pytest.MonkeyPatch, 281 | ) -> None: 282 | """ 283 | Make sure Django is not imported at all if no Django settings is specified. 284 | """ 285 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 286 | pytester.makepyfile( 287 | """ 288 | import sys 289 | def test_settings(): 290 | assert 'django' not in sys.modules 291 | """ 292 | ) 293 | result = pytester.runpytest_subprocess() 294 | result.stdout.fnmatch_lines(["* 1 passed*"]) 295 | assert result.ret == 0 296 | 297 | 298 | def test_debug_false_by_default( 299 | pytester: pytest.Pytester, 300 | monkeypatch: pytest.MonkeyPatch, 301 | ) -> None: 302 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 303 | pytester.makeconftest( 304 | """ 305 | from django.conf import settings 306 | 307 | def pytest_configure(): 308 | settings.configure(SECRET_KEY='set from pytest_configure', 309 | DEBUG=True, 310 | DATABASES={'default': { 311 | 'ENGINE': 'django.db.backends.sqlite3', 312 | 'NAME': ':memory:'}}, 313 | INSTALLED_APPS=['django.contrib.auth', 314 | 'django.contrib.contenttypes',]) 315 | """ 316 | ) 317 | 318 | pytester.makepyfile( 319 | """ 320 | from django.conf import settings 321 | def test_debug_is_false(): 322 | assert settings.DEBUG is False 323 | """ 324 | ) 325 | 326 | r = pytester.runpytest_subprocess() 327 | assert r.ret == 0 328 | 329 | 330 | @pytest.mark.parametrize("django_debug_mode", [False, True]) 331 | def test_django_debug_mode_true_false( 332 | pytester: pytest.Pytester, 333 | monkeypatch: pytest.MonkeyPatch, 334 | django_debug_mode: bool, 335 | ) -> None: 336 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 337 | pytester.makeini( 338 | f""" 339 | [pytest] 340 | django_debug_mode = {django_debug_mode} 341 | """ 342 | ) 343 | pytester.makeconftest( 344 | """ 345 | from django.conf import settings 346 | 347 | def pytest_configure(): 348 | settings.configure(SECRET_KEY='set from pytest_configure', 349 | DEBUG=%s, 350 | DATABASES={'default': { 351 | 'ENGINE': 'django.db.backends.sqlite3', 352 | 'NAME': ':memory:'}}, 353 | INSTALLED_APPS=['django.contrib.auth', 354 | 'django.contrib.contenttypes',]) 355 | """ 356 | % (not django_debug_mode) 357 | ) 358 | 359 | pytester.makepyfile( 360 | f""" 361 | from django.conf import settings 362 | def test_debug_is_false(): 363 | assert settings.DEBUG is {django_debug_mode} 364 | """ 365 | ) 366 | 367 | r = pytester.runpytest_subprocess() 368 | assert r.ret == 0 369 | 370 | 371 | @pytest.mark.parametrize("settings_debug", [False, True]) 372 | def test_django_debug_mode_keep( 373 | pytester: pytest.Pytester, 374 | monkeypatch: pytest.MonkeyPatch, 375 | settings_debug: bool, 376 | ) -> None: 377 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 378 | pytester.makeini( 379 | """ 380 | [pytest] 381 | django_debug_mode = keep 382 | """ 383 | ) 384 | pytester.makeconftest( 385 | f""" 386 | from django.conf import settings 387 | 388 | def pytest_configure(): 389 | settings.configure( 390 | SECRET_KEY='set from pytest_configure', 391 | DEBUG={settings_debug}, 392 | DATABASES={{ 393 | 'default': {{ 394 | 'ENGINE': 'django.db.backends.sqlite3', 395 | 'NAME': ':memory:', 396 | }}, 397 | }}, 398 | INSTALLED_APPS=[ 399 | 'django.contrib.auth', 400 | 'django.contrib.contenttypes', 401 | ], 402 | ) 403 | """ 404 | ) 405 | 406 | pytester.makepyfile( 407 | f""" 408 | from django.conf import settings 409 | def test_debug_is_false(): 410 | assert settings.DEBUG is {settings_debug} 411 | """ 412 | ) 413 | 414 | r = pytester.runpytest_subprocess() 415 | assert r.ret == 0 416 | 417 | 418 | @pytest.mark.django_project( 419 | extra_settings=""" 420 | INSTALLED_APPS = [ 421 | 'tpkg.app.apps.TestApp', 422 | ] 423 | """ 424 | ) 425 | def test_django_setup_sequence(django_pytester) -> None: 426 | django_pytester.create_app_file( 427 | """ 428 | from django.apps import apps, AppConfig 429 | 430 | 431 | class TestApp(AppConfig): 432 | name = 'tpkg.app' 433 | 434 | def ready(self): 435 | populating = apps.loading 436 | print('READY(): populating=%r' % populating) 437 | """, 438 | "apps.py", 439 | ) 440 | 441 | django_pytester.create_app_file( 442 | """ 443 | from django.apps import apps 444 | 445 | populating = apps.loading 446 | 447 | print('IMPORT: populating=%r,ready=%r' % (populating, apps.ready)) 448 | SOME_THING = 1234 449 | """, 450 | "models.py", 451 | ) 452 | 453 | django_pytester.create_app_file("", "__init__.py") 454 | django_pytester.makepyfile( 455 | """ 456 | from django.apps import apps 457 | from tpkg.app.models import SOME_THING 458 | 459 | def test_anything(): 460 | populating = apps.loading 461 | 462 | print('TEST: populating=%r,ready=%r' % (populating, apps.ready)) 463 | """ 464 | ) 465 | 466 | result = django_pytester.runpytest_subprocess("-s", "--tb=line") 467 | result.stdout.fnmatch_lines(["*IMPORT: populating=True,ready=False*"]) 468 | result.stdout.fnmatch_lines(["*READY(): populating=True*"]) 469 | result.stdout.fnmatch_lines(["*TEST: populating=True,ready=True*"]) 470 | assert result.ret == 0 471 | 472 | 473 | def test_no_ds_but_django_imported( 474 | pytester: pytest.Pytester, 475 | monkeypatch: pytest.MonkeyPatch, 476 | ) -> None: 477 | """pytest-django should not bail out, if "django" has been imported 478 | somewhere, e.g. via pytest-splinter.""" 479 | 480 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 481 | 482 | pytester.makepyfile( 483 | """ 484 | import os 485 | import django 486 | 487 | from pytest_django.lazy_django import django_settings_is_configured 488 | 489 | def test_django_settings_is_configured(): 490 | assert django_settings_is_configured() is False 491 | 492 | def test_env(): 493 | assert 'DJANGO_SETTINGS_MODULE' not in os.environ 494 | 495 | def test_cfg(pytestconfig): 496 | assert pytestconfig.option.ds is None 497 | """ 498 | ) 499 | r = pytester.runpytest_subprocess("-s") 500 | assert r.ret == 0 501 | 502 | 503 | def test_no_ds_but_django_conf_imported( 504 | pytester: pytest.Pytester, 505 | monkeypatch: pytest.MonkeyPatch, 506 | ) -> None: 507 | """pytest-django should not bail out, if "django.conf" has been imported 508 | somewhere, e.g. via hypothesis (#599).""" 509 | 510 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 511 | 512 | pytester.makepyfile( 513 | """ 514 | import os 515 | import sys 516 | 517 | # line copied from hypothesis/extras/django.py 518 | from django.conf import settings as django_settings 519 | 520 | # Don't let pytest poke into this object, generating a 521 | # django.core.exceptions.ImproperlyConfigured 522 | del django_settings 523 | 524 | from pytest_django.lazy_django import django_settings_is_configured 525 | 526 | def test_django_settings_is_configured(): 527 | assert django_settings_is_configured() is False 528 | 529 | def test_django_conf_is_imported(): 530 | assert 'django.conf' in sys.modules 531 | 532 | def test_env(): 533 | assert 'DJANGO_SETTINGS_MODULE' not in os.environ 534 | 535 | def test_cfg(pytestconfig): 536 | assert pytestconfig.option.ds is None 537 | """ 538 | ) 539 | r = pytester.runpytest_subprocess("-s") 540 | assert r.ret == 0 541 | 542 | 543 | def test_no_django_settings_but_django_imported( 544 | pytester: pytest.Pytester, 545 | monkeypatch: pytest.MonkeyPatch, 546 | ) -> None: 547 | """Make sure we do not crash when Django happens to be imported, but 548 | settings is not properly configured""" 549 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 550 | pytester.makeconftest("import django") 551 | r = pytester.runpytest_subprocess("--help") 552 | assert r.ret == 0 553 | -------------------------------------------------------------------------------- /tests/test_doctest.txt: -------------------------------------------------------------------------------- 1 | This doctest should run without problems with pytest. 2 | 3 | >>> print('works') 4 | works 5 | -------------------------------------------------------------------------------- /tests/test_environment.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import pytest 5 | from django.contrib.sites import models as site_models 6 | from django.contrib.sites.models import Site 7 | from django.core import mail 8 | from django.db import connection 9 | from django.test import TestCase 10 | 11 | from .helpers import DjangoPytester 12 | 13 | from pytest_django_test.app.models import Item 14 | 15 | 16 | # It doesn't matter which order all the _again methods are run, we just need 17 | # to check the environment remains constant. 18 | # This is possible with some of the pytester magic, but this is the lazy way 19 | # to do it. 20 | 21 | 22 | @pytest.mark.parametrize("subject", ["subject1", "subject2"]) 23 | def test_autoclear_mailbox(subject: str) -> None: 24 | assert len(mail.outbox) == 0 25 | mail.send_mail(subject, "body", "from@example.com", ["to@example.com"]) 26 | assert len(mail.outbox) == 1 27 | 28 | m = mail.outbox[0] 29 | assert m.subject == subject 30 | assert m.body == "body" 31 | assert m.from_email == "from@example.com" 32 | assert m.to == ["to@example.com"] 33 | 34 | 35 | class TestDirectAccessWorksForDjangoTestCase(TestCase): 36 | def _do_test(self) -> None: 37 | assert len(mail.outbox) == 0 38 | mail.send_mail("subject", "body", "from@example.com", ["to@example.com"]) 39 | assert len(mail.outbox) == 1 40 | 41 | def test_one(self) -> None: 42 | self._do_test() 43 | 44 | def test_two(self) -> None: 45 | self._do_test() 46 | 47 | 48 | @pytest.mark.django_project( 49 | extra_settings=""" 50 | TEMPLATE_LOADERS = ( 51 | 'django.template.loaders.filesystem.Loader', 52 | 'django.template.loaders.app_directories.Loader', 53 | ) 54 | ROOT_URLCONF = 'tpkg.app.urls' 55 | """ 56 | ) 57 | def test_invalid_template_variable(django_pytester: DjangoPytester) -> None: 58 | django_pytester.create_app_file( 59 | """ 60 | from django.urls import path 61 | 62 | from tpkg.app import views 63 | 64 | urlpatterns = [path('invalid_template/', views.invalid_template)] 65 | """, 66 | "urls.py", 67 | ) 68 | django_pytester.create_app_file( 69 | """ 70 | from django.shortcuts import render 71 | 72 | 73 | def invalid_template(request): 74 | return render(request, 'invalid_template.html', {}) 75 | """, 76 | "views.py", 77 | ) 78 | django_pytester.create_app_file( 79 | "
{{ invalid_var }}
", "templates/invalid_template_base.html" 80 | ) 81 | django_pytester.create_app_file( 82 | "{% include 'invalid_template_base.html' %}", "templates/invalid_template.html" 83 | ) 84 | django_pytester.create_test_module( 85 | """ 86 | import pytest 87 | 88 | def test_for_invalid_template(client): 89 | client.get('/invalid_template/') 90 | 91 | @pytest.mark.ignore_template_errors 92 | def test_ignore(client): 93 | client.get('/invalid_template/') 94 | """ 95 | ) 96 | result = django_pytester.runpytest_subprocess("-s", "--fail-on-template-vars") 97 | 98 | origin = "'*/tpkg/app/templates/invalid_template_base.html'" 99 | result.stdout.fnmatch_lines_random( 100 | [ 101 | "tpkg/test_the_test.py F.*", 102 | f"E * Failed: Undefined template variable 'invalid_var' in {origin}", 103 | ] 104 | ) 105 | 106 | 107 | @pytest.mark.django_project( 108 | extra_settings=""" 109 | TEMPLATE_LOADERS = ( 110 | 'django.template.loaders.filesystem.Loader', 111 | 'django.template.loaders.app_directories.Loader', 112 | ) 113 | """ 114 | ) 115 | def test_invalid_template_variable_marker_cleanup(django_pytester: DjangoPytester) -> None: 116 | django_pytester.create_app_file( 117 | "
{{ invalid_var }}
", "templates/invalid_template_base.html" 118 | ) 119 | django_pytester.create_app_file( 120 | "{% include 'invalid_template_base.html' %}", "templates/invalid_template.html" 121 | ) 122 | django_pytester.create_test_module( 123 | """ 124 | from django.template.loader import render_to_string 125 | 126 | import pytest 127 | 128 | @pytest.mark.ignore_template_errors 129 | def test_ignore(client): 130 | render_to_string('invalid_template.html') 131 | 132 | def test_for_invalid_template(client): 133 | render_to_string('invalid_template.html') 134 | 135 | """ 136 | ) 137 | result = django_pytester.runpytest_subprocess("-s", "--fail-on-template-vars") 138 | 139 | origin = "'*/tpkg/app/templates/invalid_template_base.html'" 140 | result.stdout.fnmatch_lines_random( 141 | [ 142 | "tpkg/test_the_test.py .F*", 143 | f"E * Failed: Undefined template variable 'invalid_var' in {origin}", 144 | ] 145 | ) 146 | 147 | 148 | @pytest.mark.django_project( 149 | extra_settings=""" 150 | TEMPLATE_LOADERS = ( 151 | 'django.template.loaders.filesystem.Loader', 152 | 'django.template.loaders.app_directories.Loader', 153 | ) 154 | TEMPLATES[0]["OPTIONS"]["string_if_invalid"] = "Something clever" 155 | """ 156 | ) 157 | def test_invalid_template_variable_behaves_normally_when_ignored( 158 | django_pytester: DjangoPytester, 159 | ) -> None: 160 | django_pytester.create_app_file( 161 | "
{{ invalid_var }}
", "templates/invalid_template_base.html" 162 | ) 163 | django_pytester.create_app_file( 164 | "{% include 'invalid_template_base.html' %}", "templates/invalid_template.html" 165 | ) 166 | django_pytester.create_test_module( 167 | """ 168 | from django.template.loader import render_to_string 169 | 170 | import pytest 171 | 172 | @pytest.mark.ignore_template_errors 173 | def test_ignore(client): 174 | assert render_to_string('invalid_template.html') == "
Something clever
" 175 | 176 | def test_for_invalid_template(client): 177 | render_to_string('invalid_template.html') 178 | 179 | """ 180 | ) 181 | result = django_pytester.runpytest_subprocess("-s", "--fail-on-template-vars") 182 | 183 | origin = "'*/tpkg/app/templates/invalid_template_base.html'" 184 | result.stdout.fnmatch_lines_random( 185 | [ 186 | "tpkg/test_the_test.py .F*", 187 | f"E * Failed: Undefined template variable 'invalid_var' in {origin}", 188 | ] 189 | ) 190 | 191 | 192 | @pytest.mark.django_project( 193 | extra_settings=""" 194 | TEMPLATE_LOADERS = ( 195 | 'django.template.loaders.filesystem.Loader', 196 | 'django.template.loaders.app_directories.Loader', 197 | ) 198 | ROOT_URLCONF = 'tpkg.app.urls' 199 | """ 200 | ) 201 | def test_invalid_template_with_default_if_none(django_pytester: DjangoPytester) -> None: 202 | django_pytester.create_app_file( 203 | """ 204 |
{{ data.empty|default:'d' }}
205 |
{{ data.none|default:'d' }}
206 |
{{ data.empty|default_if_none:'d' }}
207 |
{{ data.none|default_if_none:'d' }}
208 |
{{ data.missing|default_if_none:'d' }}
209 | """, 210 | "templates/the_template.html", 211 | ) 212 | django_pytester.create_test_module( 213 | """ 214 | def test_for_invalid_template(): 215 | from django.shortcuts import render 216 | 217 | 218 | render( 219 | request=None, 220 | template_name='the_template.html', 221 | context={'data': {'empty': '', 'none': None}}, 222 | ) 223 | """ 224 | ) 225 | result = django_pytester.runpytest_subprocess("--fail-on-template-vars") 226 | result.stdout.fnmatch_lines( 227 | [ 228 | "tpkg/test_the_test.py F", 229 | "E * Failed: Undefined template variable 'data.missing' in *the_template.html'", 230 | ] 231 | ) 232 | 233 | 234 | @pytest.mark.django_project( 235 | extra_settings=""" 236 | TEMPLATE_LOADERS = ( 237 | 'django.template.loaders.filesystem.Loader', 238 | 'django.template.loaders.app_directories.Loader', 239 | ) 240 | ROOT_URLCONF = 'tpkg.app.urls' 241 | """ 242 | ) 243 | def test_invalid_template_variable_opt_in(django_pytester: DjangoPytester) -> None: 244 | django_pytester.create_app_file( 245 | """ 246 | from django.urls import path 247 | 248 | from tpkg.app import views 249 | 250 | urlpatterns = [path('invalid_template', views.invalid_template)] 251 | """, 252 | "urls.py", 253 | ) 254 | django_pytester.create_app_file( 255 | """ 256 | from django.shortcuts import render 257 | 258 | 259 | def invalid_template(request): 260 | return render(request, 'invalid_template.html', {}) 261 | """, 262 | "views.py", 263 | ) 264 | django_pytester.create_app_file( 265 | "
{{ invalid_var }}
", "templates/invalid_template.html" 266 | ) 267 | django_pytester.create_test_module( 268 | """ 269 | import pytest 270 | 271 | def test_for_invalid_template(client): 272 | client.get('/invalid_template/') 273 | 274 | @pytest.mark.ignore_template_errors 275 | def test_ignore(client): 276 | client.get('/invalid_template/') 277 | """ 278 | ) 279 | result = django_pytester.runpytest_subprocess("-s") 280 | result.stdout.fnmatch_lines_random(["tpkg/test_the_test.py ..*"]) 281 | 282 | 283 | @pytest.mark.django_db 284 | def test_database_rollback() -> None: 285 | assert Item.objects.count() == 0 286 | Item.objects.create(name="blah") 287 | assert Item.objects.count() == 1 288 | 289 | 290 | @pytest.mark.django_db 291 | def test_database_rollback_again() -> None: 292 | test_database_rollback() 293 | 294 | 295 | @pytest.mark.django_db 296 | def test_database_name() -> None: 297 | dirname, name = os.path.split(connection.settings_dict["NAME"]) 298 | assert "file:memorydb" in name or name == ":memory:" or name.startswith("test_") 299 | 300 | 301 | def test_database_noaccess() -> None: 302 | with pytest.raises(RuntimeError): 303 | Item.objects.count() 304 | 305 | 306 | class TestrunnerVerbosity: 307 | """Test that Django's code to setup and teardown the databases uses 308 | pytest's verbosity level.""" 309 | 310 | @pytest.fixture 311 | def pytester(self, django_pytester: DjangoPytester) -> pytest.Pytester: 312 | django_pytester.create_test_module( 313 | """ 314 | import pytest 315 | 316 | @pytest.mark.django_db 317 | def test_inner_testrunner(): 318 | pass 319 | """ 320 | ) 321 | return django_pytester 322 | 323 | def test_default(self, pytester: pytest.Pytester) -> None: 324 | """Not verbose by default.""" 325 | result = pytester.runpytest_subprocess("-s") 326 | result.stdout.fnmatch_lines(["tpkg/test_the_test.py .*"]) 327 | 328 | def test_vq_verbosity_0(self, pytester: pytest.Pytester) -> None: 329 | """-v and -q results in verbosity 0.""" 330 | result = pytester.runpytest_subprocess("-s", "-v", "-q") 331 | result.stdout.fnmatch_lines(["tpkg/test_the_test.py .*"]) 332 | 333 | def test_verbose_with_v(self, pytester: pytest.Pytester) -> None: 334 | """Verbose output with '-v'.""" 335 | result = pytester.runpytest_subprocess("-s", "-v") 336 | result.stdout.fnmatch_lines_random(["tpkg/test_the_test.py:*", "*PASSED*"]) 337 | result.stderr.fnmatch_lines(["*Destroying test database for alias 'default'*"]) 338 | 339 | def test_more_verbose_with_vv(self, pytester: pytest.Pytester) -> None: 340 | """More verbose output with '-v -v'.""" 341 | result = pytester.runpytest_subprocess("-s", "-v", "-v") 342 | result.stdout.fnmatch_lines_random( 343 | [ 344 | "tpkg/test_the_test.py:*", 345 | "*Operations to perform:*", 346 | "*Apply all migrations:*", 347 | "*PASSED*", 348 | ] 349 | ) 350 | result.stderr.fnmatch_lines( 351 | [ 352 | "*Creating test database for alias*", 353 | "*Destroying test database for alias 'default'*", 354 | ] 355 | ) 356 | 357 | def test_more_verbose_with_vv_and_reusedb(self, pytester: pytest.Pytester) -> None: 358 | """More verbose output with '-v -v', and --create-db.""" 359 | result = pytester.runpytest_subprocess("-s", "-v", "-v", "--create-db") 360 | result.stdout.fnmatch_lines(["tpkg/test_the_test.py:*", "*PASSED*"]) 361 | result.stderr.fnmatch_lines(["*Creating test database for alias*"]) 362 | assert "*Destroying test database for alias 'default' ('*')...*" not in result.stderr.str() 363 | 364 | 365 | @pytest.mark.django_db 366 | @pytest.mark.parametrize("site_name", ["site1", "site2"]) 367 | def test_clear_site_cache(site_name: str, rf, monkeypatch: pytest.MonkeyPatch) -> None: 368 | request = rf.get("/") 369 | monkeypatch.setattr(request, "get_host", lambda: "foo.com") 370 | Site.objects.create(domain="foo.com", name=site_name) 371 | assert Site.objects.get_current(request=request).name == site_name 372 | 373 | 374 | @pytest.mark.django_db 375 | @pytest.mark.parametrize("site_name", ["site1", "site2"]) 376 | def test_clear_site_cache_check_site_cache_size(site_name: str, settings) -> None: 377 | assert len(site_models.SITE_CACHE) == 0 378 | site = Site.objects.create(domain="foo.com", name=site_name) 379 | settings.SITE_ID = site.id 380 | assert Site.objects.get_current() == site 381 | assert len(site_models.SITE_CACHE) == 1 382 | 383 | 384 | @pytest.mark.django_project( 385 | project_root="django_project_root", 386 | create_manage_py=True, 387 | extra_settings=""" 388 | TEST_RUNNER = 'pytest_django.runner.TestRunner' 389 | """, 390 | ) 391 | def test_manage_test_runner(django_pytester: DjangoPytester) -> None: 392 | django_pytester.create_test_module( 393 | """ 394 | import pytest 395 | 396 | @pytest.mark.django_db 397 | def test_inner_testrunner(): 398 | pass 399 | """ 400 | ) 401 | result = django_pytester.run(*[sys.executable, "django_project_root/manage.py", "test"]) 402 | assert "1 passed" in "\n".join(result.outlines) 403 | 404 | 405 | @pytest.mark.django_project( 406 | project_root="django_project_root", 407 | create_manage_py=True, 408 | ) 409 | def test_manage_test_runner_without(django_pytester: DjangoPytester) -> None: 410 | django_pytester.create_test_module( 411 | """ 412 | import pytest 413 | 414 | @pytest.mark.django_db 415 | def test_inner_testrunner(): 416 | pass 417 | """ 418 | ) 419 | result = django_pytester.run(*[sys.executable, "django_project_root/manage.py", "test"]) 420 | assert "Found 0 test(s)." in "\n".join(result.outlines) 421 | -------------------------------------------------------------------------------- /tests/test_initialization.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | import pytest 4 | 5 | from .helpers import DjangoPytester 6 | 7 | 8 | def test_django_setup_order_and_uniqueness( 9 | django_pytester: DjangoPytester, 10 | monkeypatch: pytest.MonkeyPatch, 11 | ) -> None: 12 | """ 13 | The django.setup() function shall not be called multiple times by 14 | pytest-django, since it resets logging conf each time. 15 | """ 16 | django_pytester.makeconftest( 17 | """ 18 | import django.apps 19 | assert django.apps.apps.ready 20 | from tpkg.app.models import Item 21 | 22 | print("conftest") 23 | def pytest_configure(): 24 | import django 25 | print("pytest_configure: conftest") 26 | django.setup = lambda: SHOULD_NOT_GET_CALLED 27 | """ 28 | ) 29 | 30 | django_pytester.project_root.joinpath("tpkg", "plugin.py").write_text( 31 | dedent( 32 | """ 33 | import pytest 34 | import django.apps 35 | assert not django.apps.apps.ready 36 | 37 | print("plugin") 38 | def pytest_configure(): 39 | assert django.apps.apps.ready 40 | from tpkg.app.models import Item 41 | print("pytest_configure: plugin") 42 | 43 | @pytest.hookimpl(tryfirst=True) 44 | def pytest_load_initial_conftests(early_config, parser, args): 45 | print("pytest_load_initial_conftests") 46 | assert not django.apps.apps.ready 47 | """ 48 | ) 49 | ) 50 | django_pytester.makepyfile( 51 | """ 52 | def test_ds(): 53 | pass 54 | """ 55 | ) 56 | result = django_pytester.runpytest_subprocess("-s", "-p", "tpkg.plugin") 57 | result.stdout.fnmatch_lines( 58 | [ 59 | "plugin", 60 | "pytest_load_initial_conftests", 61 | "conftest", 62 | "pytest_configure: conftest", 63 | "pytest_configure: plugin", 64 | "* 1 passed*", 65 | ] 66 | ) 67 | assert result.ret == 0 68 | -------------------------------------------------------------------------------- /tests/test_manage_py_scan.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from .helpers import DjangoPytester 4 | 5 | 6 | @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) 7 | def test_django_project_found(django_pytester: DjangoPytester) -> None: 8 | # XXX: Important: Do not chdir() to django_project_root since runpytest_subprocess 9 | # will call "python /path/to/pytest.py", which will implicitly add cwd to 10 | # the path. By instead calling "python /path/to/pytest.py 11 | # django_project_root", we avoid implicitly adding the project to sys.path 12 | # This matches the behaviour when pytest is called directly as an 13 | # executable (cwd is not added to the Python path) 14 | 15 | django_pytester.create_test_module( 16 | """ 17 | def test_foobar(): 18 | assert 1 + 1 == 2 19 | """ 20 | ) 21 | 22 | result = django_pytester.runpytest_subprocess("django_project_root") 23 | assert result.ret == 0 24 | 25 | outcomes = result.parseoutcomes() 26 | assert outcomes["passed"] == 1 27 | 28 | 29 | @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) 30 | def test_django_project_found_with_k( 31 | django_pytester: DjangoPytester, 32 | monkeypatch: pytest.MonkeyPatch, 33 | ) -> None: 34 | """Test that cwd is checked as fallback with non-args via '-k foo'.""" 35 | testfile = django_pytester.create_test_module( 36 | """ 37 | def test_foobar(): 38 | assert True 39 | """, 40 | "sub/test_in_sub.py", 41 | ) 42 | 43 | monkeypatch.chdir(testfile.parent) 44 | result = django_pytester.runpytest_subprocess("-k", "test_foobar") 45 | assert result.ret == 0 46 | 47 | outcomes = result.parseoutcomes() 48 | assert outcomes["passed"] == 1 49 | 50 | 51 | @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) 52 | def test_django_project_found_with_k_and_cwd( 53 | django_pytester: DjangoPytester, 54 | monkeypatch: pytest.MonkeyPatch, 55 | ) -> None: 56 | """Cover cwd not used as fallback if present already in args.""" 57 | testfile = django_pytester.create_test_module( 58 | """ 59 | def test_foobar(): 60 | assert True 61 | """, 62 | "sub/test_in_sub.py", 63 | ) 64 | 65 | monkeypatch.chdir(testfile.parent) 66 | result = django_pytester.runpytest_subprocess(testfile.parent, "-k", "test_foobar") 67 | assert result.ret == 0 68 | 69 | outcomes = result.parseoutcomes() 70 | assert outcomes["passed"] == 1 71 | 72 | 73 | @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) 74 | def test_django_project_found_absolute( 75 | django_pytester: DjangoPytester, 76 | monkeypatch: pytest.MonkeyPatch, 77 | ) -> None: 78 | """This only tests that "." is added as an absolute path (#637).""" 79 | django_pytester.create_test_module( 80 | """ 81 | def test_dot_not_in_syspath(): 82 | import sys 83 | assert '.' not in sys.path[:5] 84 | """ 85 | ) 86 | monkeypatch.chdir("django_project_root") 87 | # NOTE: the "." here is important to test for an absolute path being used. 88 | result = django_pytester.runpytest_subprocess("-s", ".") 89 | assert result.ret == 0 90 | 91 | outcomes = result.parseoutcomes() 92 | assert outcomes["passed"] == 1 93 | 94 | 95 | @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) 96 | def test_django_project_found_invalid_settings( 97 | django_pytester: DjangoPytester, 98 | monkeypatch: pytest.MonkeyPatch, 99 | ) -> None: 100 | monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "DOES_NOT_EXIST") 101 | 102 | result = django_pytester.runpytest_subprocess("django_project_root") 103 | assert result.ret != 0 104 | 105 | result.stderr.fnmatch_lines(["*ImportError:*DOES_NOT_EXIST*"]) 106 | result.stderr.fnmatch_lines(["*pytest-django found a Django project*"]) 107 | 108 | 109 | def test_django_project_scan_disabled_invalid_settings( 110 | django_pytester: DjangoPytester, 111 | monkeypatch: pytest.MonkeyPatch, 112 | ) -> None: 113 | monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "DOES_NOT_EXIST") 114 | 115 | django_pytester.makeini( 116 | """ 117 | [pytest] 118 | django_find_project = false 119 | """ 120 | ) 121 | 122 | result = django_pytester.runpytest_subprocess("django_project_root") 123 | assert result.ret != 0 124 | 125 | result.stderr.fnmatch_lines(["*ImportError*DOES_NOT_EXIST*"]) 126 | result.stderr.fnmatch_lines(["*pytest-django did not search for Django projects*"]) 127 | 128 | 129 | @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) 130 | def test_django_project_found_invalid_settings_version( 131 | django_pytester: DjangoPytester, 132 | monkeypatch: pytest.MonkeyPatch, 133 | ) -> None: 134 | """Invalid DSM should not cause an error with --help or --version.""" 135 | monkeypatch.setenv("DJANGO_SETTINGS_MODULE", "DOES_NOT_EXIST") 136 | 137 | result = django_pytester.runpytest_subprocess("django_project_root", "--version", "--version") 138 | assert result.ret == 0 139 | 140 | result.stdout.fnmatch_lines(["*This is pytest version*"]) 141 | 142 | result = django_pytester.runpytest_subprocess("django_project_root", "--help") 143 | assert result.ret == 0 144 | result.stdout.fnmatch_lines(["*usage:*"]) 145 | 146 | 147 | @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) 148 | def test_django_project_late_settings_version( 149 | django_pytester: DjangoPytester, 150 | monkeypatch: pytest.MonkeyPatch, 151 | ) -> None: 152 | """Late configuration should not cause an error with --help or --version.""" 153 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 154 | django_pytester.makepyfile( 155 | t="WAT = 1", 156 | ) 157 | django_pytester.makeconftest( 158 | """ 159 | import os 160 | 161 | def pytest_configure(): 162 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 't') 163 | from django.conf import settings 164 | settings.WAT 165 | """ 166 | ) 167 | 168 | result = django_pytester.runpytest_subprocess("django_project_root", "--version", "--version") 169 | assert result.ret == 0 170 | 171 | result.stdout.fnmatch_lines(["*This is pytest version*"]) 172 | 173 | result = django_pytester.runpytest_subprocess("django_project_root", "--help") 174 | assert result.ret == 0 175 | result.stdout.fnmatch_lines(["*usage:*"]) 176 | 177 | 178 | @pytest.mark.django_project(project_root="django_project_root", create_manage_py=True) 179 | def test_runs_without_error_on_long_args(django_pytester: DjangoPytester) -> None: 180 | django_pytester.create_test_module( 181 | """ 182 | def test_this_is_a_long_message_which_caused_a_bug_when_scanning_for_manage_py_12346712341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234112341234112451234123412341234123412341234123412341234123412341234123412341234123412341234123412341234(): 183 | assert 1 + 1 == 2 184 | """ 185 | ) 186 | 187 | result = django_pytester.runpytest_subprocess( 188 | "-k", 189 | "this_is_a_long_message_which_caused_a_bug_when_scanning_for_manage_py_12346712341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234123412341234112341234112451234123412341234123412341234123412341234123412341234123412341234123412341234123412341234", 190 | "django_project_root", 191 | ) 192 | assert result.ret == 0 193 | -------------------------------------------------------------------------------- /tests/test_runner.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, call 2 | 3 | import pytest 4 | 5 | from pytest_django.runner import TestRunner 6 | 7 | 8 | @pytest.mark.parametrize( 9 | ("kwargs", "expected"), 10 | [ 11 | ({}, call(["tests"])), 12 | ({"verbosity": 0}, call(["--quiet", "tests"])), 13 | ({"verbosity": 1}, call(["tests"])), 14 | ({"verbosity": 2}, call(["-v", "tests"])), 15 | ({"verbosity": 3}, call(["-vv", "tests"])), 16 | ({"verbosity": 4}, call(["-vvv", "tests"])), 17 | ({"failfast": True}, call(["--exitfirst", "tests"])), 18 | ({"keepdb": True}, call(["--reuse-db", "tests"])), 19 | ], 20 | ) 21 | def test_runner_run_tests(monkeypatch, kwargs, expected): 22 | pytest_mock = Mock() 23 | monkeypatch.setattr("pytest.main", pytest_mock) 24 | runner = TestRunner(**kwargs) 25 | runner.run_tests(["tests"]) 26 | assert pytest_mock.call_args == expected 27 | -------------------------------------------------------------------------------- /tests/test_unittest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.test import SimpleTestCase, TestCase, tag 3 | 4 | from .helpers import DjangoPytester 5 | 6 | from pytest_django_test.app.models import Item 7 | 8 | 9 | class TestFixtures(TestCase): 10 | fixtures = ("items",) 11 | 12 | def test_fixtures(self) -> None: 13 | assert Item.objects.count() == 1 14 | assert Item.objects.get().name == "Fixture item" 15 | 16 | def test_fixtures_again(self) -> None: 17 | """Ensure fixtures are only loaded once.""" 18 | self.test_fixtures() 19 | 20 | 21 | class TestSetup(TestCase): 22 | def setUp(self) -> None: 23 | """setUp should be called after starting a transaction""" 24 | assert Item.objects.count() == 0 25 | Item.objects.create(name="Some item") 26 | Item.objects.create(name="Some item again") 27 | 28 | def test_count(self) -> None: 29 | self.assertEqual(Item.objects.count(), 2) # noqa: PT009 30 | assert Item.objects.count() == 2 31 | Item.objects.create(name="Foo") 32 | self.assertEqual(Item.objects.count(), 3) # noqa: PT009 33 | 34 | def test_count_again(self) -> None: 35 | self.test_count() 36 | 37 | def tearDown(self) -> None: 38 | """tearDown should be called before rolling back the database""" 39 | assert Item.objects.count() == 3 40 | 41 | 42 | class TestFixturesWithSetup(TestCase): 43 | fixtures = ("items",) 44 | 45 | def setUp(self) -> None: 46 | assert Item.objects.count() == 1 47 | Item.objects.create(name="Some item") 48 | 49 | def test_count(self) -> None: 50 | assert Item.objects.count() == 2 51 | Item.objects.create(name="Some item again") 52 | 53 | def test_count_again(self) -> None: 54 | self.test_count() 55 | 56 | def tearDown(self) -> None: 57 | assert Item.objects.count() == 3 58 | 59 | 60 | @tag("tag1", "tag2") 61 | class TestDjangoTagsToPytestMarkers(SimpleTestCase): 62 | """Django test tags are converted to Pytest markers, at the class & method 63 | levels.""" 64 | 65 | @pytest.fixture(autouse=True) 66 | def gimme_my_markers(self, request: pytest.FixtureRequest) -> None: 67 | self.markers = {m.name for m in request.node.iter_markers()} 68 | 69 | @tag("tag3", "tag4") # type: ignore[misc] 70 | def test_1(self) -> None: 71 | assert self.markers == {"tag1", "tag2", "tag3", "tag4"} 72 | 73 | def test_2(self) -> None: 74 | assert self.markers == {"tag1", "tag2"} 75 | 76 | @tag("tag5") # type: ignore[misc] 77 | def test_3(self) -> None: 78 | assert self.markers == {"tag1", "tag2", "tag5"} 79 | 80 | 81 | @tag("tag1") 82 | class TestNonDjangoClassWithTags: 83 | """Django test tags are only converted to Pytest markers if actually 84 | Django tests. Use pytest markers directly for pytest tests.""" 85 | 86 | @pytest.fixture(autouse=True) 87 | def gimme_my_markers(self, request: pytest.FixtureRequest) -> None: 88 | self.markers = {m.name for m in request.node.iter_markers()} 89 | 90 | @tag("tag2") # type: ignore[misc] 91 | def test_1(self) -> None: 92 | assert not self.markers 93 | 94 | 95 | def test_sole_test(django_pytester: DjangoPytester) -> None: 96 | """ 97 | Make sure the database is configured when only Django TestCase classes 98 | are collected, without the django_db marker. 99 | 100 | Also ensures that the DB is available after a failure (#824). 101 | """ 102 | django_pytester.create_test_module( 103 | """ 104 | import os 105 | 106 | from django.test import TestCase 107 | from django.conf import settings 108 | 109 | from .app.models import Item 110 | 111 | class TestFoo(TestCase): 112 | def test_foo(self): 113 | # Make sure we are actually using the test database 114 | _, db_name = os.path.split(settings.DATABASES['default']['NAME']) 115 | assert db_name.startswith('test_') or db_name == ':memory:' \\ 116 | or 'file:memorydb' in db_name 117 | 118 | # Make sure it is usable 119 | assert Item.objects.count() == 0 120 | 121 | assert 0, "trigger_error" 122 | 123 | class TestBar(TestCase): 124 | def test_bar(self): 125 | assert Item.objects.count() == 0 126 | """ 127 | ) 128 | 129 | result = django_pytester.runpytest_subprocess("-v") 130 | result.stdout.fnmatch_lines( 131 | [ 132 | "*::test_foo FAILED", 133 | "*::test_bar PASSED", 134 | '> assert 0, "trigger_error"', 135 | "E AssertionError: trigger_error", 136 | "E assert 0", 137 | "*= 1 failed, 1 passed*", 138 | ] 139 | ) 140 | assert result.ret == 1 141 | 142 | 143 | class TestUnittestMethods: 144 | "Test that setup/teardown methods of unittests are being called." 145 | 146 | def test_django(self, django_pytester: DjangoPytester) -> None: 147 | django_pytester.create_test_module( 148 | """ 149 | from django.test import TestCase 150 | 151 | class TestFoo(TestCase): 152 | @classmethod 153 | def setUpClass(self): 154 | print('\\nCALLED: setUpClass') 155 | 156 | def setUp(self): 157 | print('\\nCALLED: setUp') 158 | 159 | def tearDown(self): 160 | print('\\nCALLED: tearDown') 161 | 162 | @classmethod 163 | def tearDownClass(self): 164 | print('\\nCALLED: tearDownClass') 165 | 166 | def test_pass(self): 167 | pass 168 | """ 169 | ) 170 | 171 | result = django_pytester.runpytest_subprocess("-v", "-s") 172 | result.stdout.fnmatch_lines( 173 | [ 174 | "CALLED: setUpClass", 175 | "CALLED: setUp", 176 | "CALLED: tearDown", 177 | "PASSED*", 178 | "CALLED: tearDownClass", 179 | ] 180 | ) 181 | assert result.ret == 0 182 | 183 | def test_setUpClass_not_being_a_classmethod(self, django_pytester: DjangoPytester) -> None: 184 | django_pytester.create_test_module( 185 | """ 186 | from django.test import TestCase 187 | 188 | class TestFoo(TestCase): 189 | def setUpClass(self): 190 | pass 191 | 192 | def test_pass(self): 193 | pass 194 | """ 195 | ) 196 | 197 | result = django_pytester.runpytest_subprocess("-v", "-s") 198 | expected_lines = [ 199 | "* ERROR at setup of TestFoo.test_pass *", 200 | "E * TypeError: *", 201 | ] 202 | result.stdout.fnmatch_lines(expected_lines) 203 | assert result.ret == 1 204 | 205 | def test_setUpClass_multiple_subclasses(self, django_pytester: DjangoPytester) -> None: 206 | django_pytester.create_test_module( 207 | """ 208 | from django.test import TestCase 209 | 210 | 211 | class TestFoo(TestCase): 212 | @classmethod 213 | def setUpClass(cls): 214 | super(TestFoo, cls).setUpClass() 215 | 216 | def test_shared(self): 217 | pass 218 | 219 | 220 | class TestBar(TestFoo): 221 | def test_bar1(self): 222 | pass 223 | 224 | 225 | class TestBar2(TestFoo): 226 | def test_bar21(self): 227 | pass 228 | """ 229 | ) 230 | 231 | result = django_pytester.runpytest_subprocess("-v") 232 | result.stdout.fnmatch_lines( 233 | [ 234 | "*TestFoo::test_shared PASSED*", 235 | "*TestBar::test_bar1 PASSED*", 236 | "*TestBar::test_shared PASSED*", 237 | "*TestBar2::test_bar21 PASSED*", 238 | "*TestBar2::test_shared PASSED*", 239 | ] 240 | ) 241 | assert result.ret == 0 242 | 243 | def test_setUpClass_mixin(self, django_pytester: DjangoPytester) -> None: 244 | django_pytester.create_test_module( 245 | """ 246 | from django.test import TestCase 247 | 248 | class TheMixin: 249 | @classmethod 250 | def setUpClass(cls): 251 | super(TheMixin, cls).setUpClass() 252 | 253 | 254 | class TestFoo(TheMixin, TestCase): 255 | def test_foo(self): 256 | pass 257 | 258 | 259 | class TestBar(TheMixin, TestCase): 260 | def test_bar(self): 261 | pass 262 | """ 263 | ) 264 | 265 | result = django_pytester.runpytest_subprocess("-v") 266 | result.stdout.fnmatch_lines(["*TestFoo::test_foo PASSED*", "*TestBar::test_bar PASSED*"]) 267 | assert result.ret == 0 268 | 269 | def test_setUpClass_skip(self, django_pytester: DjangoPytester) -> None: 270 | django_pytester.create_test_module( 271 | """ 272 | from django.test import TestCase 273 | import pytest 274 | 275 | 276 | class TestFoo(TestCase): 277 | @classmethod 278 | def setUpClass(cls): 279 | if cls is TestFoo: 280 | raise pytest.skip("Skip base class") 281 | super(TestFoo, cls).setUpClass() 282 | 283 | def test_shared(self): 284 | pass 285 | 286 | 287 | class TestBar(TestFoo): 288 | def test_bar1(self): 289 | pass 290 | 291 | 292 | class TestBar2(TestFoo): 293 | def test_bar21(self): 294 | pass 295 | """ 296 | ) 297 | 298 | result = django_pytester.runpytest_subprocess("-v") 299 | result.stdout.fnmatch_lines( 300 | [ 301 | "*TestFoo::test_shared SKIPPED*", 302 | "*TestBar::test_bar1 PASSED*", 303 | "*TestBar::test_shared PASSED*", 304 | "*TestBar2::test_bar21 PASSED*", 305 | "*TestBar2::test_shared PASSED*", 306 | ] 307 | ) 308 | assert result.ret == 0 309 | 310 | def test_multi_inheritance_setUpClass(self, django_pytester: DjangoPytester) -> None: 311 | django_pytester.create_test_module( 312 | """ 313 | from django.test import TestCase 314 | 315 | # Using a mixin is a regression test, see #280 for more details: 316 | # https://github.com/pytest-dev/pytest-django/issues/280 317 | 318 | class SomeMixin: 319 | pass 320 | 321 | class TestA(SomeMixin, TestCase): 322 | expected_state = ['A'] 323 | state = [] 324 | 325 | @classmethod 326 | def setUpClass(cls): 327 | super(TestA, cls).setUpClass() 328 | cls.state.append('A') 329 | 330 | @classmethod 331 | def tearDownClass(cls): 332 | assert cls.state.pop() == 'A' 333 | super(TestA, cls).tearDownClass() 334 | 335 | def test_a(self): 336 | assert self.state == self.expected_state 337 | 338 | class TestB(TestA): 339 | expected_state = ['A', 'B'] 340 | 341 | @classmethod 342 | def setUpClass(cls): 343 | super(TestB, cls).setUpClass() 344 | cls.state.append('B') 345 | 346 | @classmethod 347 | def tearDownClass(cls): 348 | assert cls.state.pop() == 'B' 349 | super(TestB, cls).tearDownClass() 350 | 351 | def test_b(self): 352 | assert self.state == self.expected_state 353 | 354 | class TestC(TestB): 355 | expected_state = ['A', 'B', 'C'] 356 | 357 | @classmethod 358 | def setUpClass(cls): 359 | super(TestC, cls).setUpClass() 360 | cls.state.append('C') 361 | 362 | @classmethod 363 | def tearDownClass(cls): 364 | assert cls.state.pop() == 'C' 365 | super(TestC, cls).tearDownClass() 366 | 367 | def test_c(self): 368 | assert self.state == self.expected_state 369 | """ 370 | ) 371 | 372 | result = django_pytester.runpytest_subprocess("-vvvv", "-s") 373 | assert result.parseoutcomes()["passed"] == 6 374 | assert result.ret == 0 375 | 376 | def test_unittest(self, django_pytester: DjangoPytester) -> None: 377 | django_pytester.create_test_module( 378 | """ 379 | from unittest import TestCase 380 | 381 | class TestFoo(TestCase): 382 | @classmethod 383 | def setUpClass(self): 384 | print('\\nCALLED: setUpClass') 385 | 386 | def setUp(self): 387 | print('\\nCALLED: setUp') 388 | 389 | def tearDown(self): 390 | print('\\nCALLED: tearDown') 391 | 392 | @classmethod 393 | def tearDownClass(self): 394 | print('\\nCALLED: tearDownClass') 395 | 396 | def test_pass(self): 397 | pass 398 | """ 399 | ) 400 | 401 | result = django_pytester.runpytest_subprocess("-v", "-s") 402 | result.stdout.fnmatch_lines( 403 | [ 404 | "CALLED: setUpClass", 405 | "CALLED: setUp", 406 | "CALLED: tearDown", 407 | "PASSED*", 408 | "CALLED: tearDownClass", 409 | ] 410 | ) 411 | assert result.ret == 0 412 | 413 | def test_setUpClass_leaf_but_not_in_dunder_dict(self, django_pytester: DjangoPytester) -> None: 414 | django_pytester.create_test_module( 415 | """ 416 | from django.test import testcases 417 | 418 | class CMSTestCase(testcases.TestCase): 419 | pass 420 | 421 | class FooBarTestCase(testcases.TestCase): 422 | 423 | @classmethod 424 | def setUpClass(cls): 425 | print('FooBarTestCase.setUpClass') 426 | super(FooBarTestCase, cls).setUpClass() 427 | 428 | class TestContact(CMSTestCase, FooBarTestCase): 429 | 430 | def test_noop(self): 431 | print('test_noop') 432 | """ 433 | ) 434 | 435 | result = django_pytester.runpytest_subprocess("-q", "-s") 436 | result.stdout.fnmatch_lines(["*FooBarTestCase.setUpClass*", "*test_noop*", "1 passed*"]) 437 | assert result.ret == 0 438 | 439 | 440 | class TestCaseWithDbFixture(TestCase): 441 | pytestmark = pytest.mark.usefixtures("db") 442 | 443 | def test_simple(self) -> None: 444 | # We only want to check setup/teardown does not conflict 445 | assert 1 446 | 447 | 448 | class TestCaseWithTrDbFixture(TestCase): 449 | pytestmark = pytest.mark.usefixtures("transactional_db") 450 | 451 | def test_simple(self) -> None: 452 | # We only want to check setup/teardown does not conflict 453 | assert 1 454 | 455 | 456 | def test_pdb_enabled(django_pytester: DjangoPytester) -> None: 457 | """ 458 | Make sure the database is flushed and tests are isolated when 459 | using the --pdb option. 460 | 461 | See issue #405 for details: 462 | https://github.com/pytest-dev/pytest-django/issues/405 463 | """ 464 | 465 | django_pytester.create_test_module( 466 | ''' 467 | import os 468 | 469 | from django.test import TestCase 470 | from django.conf import settings 471 | 472 | from .app.models import Item 473 | 474 | class TestPDBIsolation(TestCase): 475 | def setUp(self): 476 | """setUp should be called after starting a transaction""" 477 | assert Item.objects.count() == 0 478 | Item.objects.create(name='Some item') 479 | Item.objects.create(name='Some item again') 480 | 481 | def test_count(self): 482 | self.assertEqual(Item.objects.count(), 2) 483 | assert Item.objects.count() == 2 484 | Item.objects.create(name='Foo') 485 | self.assertEqual(Item.objects.count(), 3) 486 | 487 | def test_count_again(self): 488 | self.test_count() 489 | 490 | def tearDown(self): 491 | """tearDown should be called before rolling back the database""" 492 | assert Item.objects.count() == 3 493 | 494 | ''' 495 | ) 496 | 497 | result = django_pytester.runpytest_subprocess("-v", "--pdb") 498 | assert result.ret == 0 499 | 500 | 501 | def test_debug_not_used(django_pytester: DjangoPytester) -> None: 502 | django_pytester.create_test_module( 503 | """ 504 | from django.test import TestCase 505 | 506 | pre_setup_count = 0 507 | 508 | 509 | class TestClass1(TestCase): 510 | 511 | def debug(self): 512 | assert 0, "should not be called" 513 | 514 | def test_method(self): 515 | pass 516 | """ 517 | ) 518 | 519 | result = django_pytester.runpytest_subprocess("--pdb") 520 | result.stdout.fnmatch_lines(["*= 1 passed*"]) 521 | assert result.ret == 0 522 | -------------------------------------------------------------------------------- /tests/test_urls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.conf import settings 3 | from django.urls import is_valid_path 4 | from django.utils.encoding import force_str 5 | 6 | from .helpers import DjangoPytester 7 | 8 | 9 | @pytest.mark.urls("pytest_django_test.urls_overridden") 10 | def test_urls() -> None: 11 | assert settings.ROOT_URLCONF == "pytest_django_test.urls_overridden" 12 | assert is_valid_path("/overridden_url/") 13 | 14 | 15 | @pytest.mark.urls("pytest_django_test.urls_overridden") 16 | def test_urls_client(client) -> None: 17 | response = client.get("/overridden_url/") 18 | assert force_str(response.content) == "Overridden urlconf works!" 19 | 20 | 21 | @pytest.mark.django_project( 22 | extra_settings=""" 23 | ROOT_URLCONF = "empty" 24 | """, 25 | ) 26 | def test_urls_cache_is_cleared(django_pytester: DjangoPytester) -> None: 27 | django_pytester.makepyfile( 28 | empty=""" 29 | urlpatterns = [] 30 | """, 31 | myurls=""" 32 | from django.urls import path 33 | 34 | def fake_view(request): 35 | pass 36 | 37 | urlpatterns = [path('first', fake_view, name='first')] 38 | """, 39 | ) 40 | 41 | django_pytester.create_test_module( 42 | """ 43 | from django.urls import reverse, NoReverseMatch 44 | import pytest 45 | 46 | @pytest.mark.urls('myurls') 47 | def test_something(): 48 | reverse('first') 49 | 50 | def test_something_else(): 51 | with pytest.raises(NoReverseMatch): 52 | reverse('first') 53 | """, 54 | ) 55 | 56 | result = django_pytester.runpytest_subprocess() 57 | assert result.ret == 0 58 | 59 | 60 | @pytest.mark.django_project( 61 | extra_settings=""" 62 | ROOT_URLCONF = "empty" 63 | """, 64 | ) 65 | def test_urls_cache_is_cleared_and_new_urls_can_be_assigned( 66 | django_pytester: DjangoPytester, 67 | ) -> None: 68 | django_pytester.makepyfile( 69 | empty=""" 70 | urlpatterns = [] 71 | """, 72 | myurls=""" 73 | from django.urls import path 74 | 75 | def fake_view(request): 76 | pass 77 | 78 | urlpatterns = [path('first', fake_view, name='first')] 79 | """, 80 | myurls2=""" 81 | from django.urls import path 82 | 83 | def fake_view(request): 84 | pass 85 | 86 | urlpatterns = [path('second', fake_view, name='second')] 87 | """, 88 | ) 89 | 90 | django_pytester.create_test_module( 91 | """ 92 | from django.urls import reverse, NoReverseMatch 93 | import pytest 94 | 95 | @pytest.mark.urls('myurls') 96 | def test_something(): 97 | reverse('first') 98 | 99 | @pytest.mark.urls('myurls2') 100 | def test_something_else(): 101 | with pytest.raises(NoReverseMatch): 102 | reverse('first') 103 | 104 | reverse('second') 105 | """, 106 | ) 107 | 108 | result = django_pytester.runpytest_subprocess() 109 | assert result.ret == 0 110 | -------------------------------------------------------------------------------- /tests/test_without_django_loaded.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def no_ds(monkeypatch) -> None: 6 | """Ensure DJANGO_SETTINGS_MODULE is unset""" 7 | monkeypatch.delenv("DJANGO_SETTINGS_MODULE") 8 | 9 | 10 | pytestmark = pytest.mark.usefixtures("no_ds") 11 | 12 | 13 | def test_no_ds(pytester: pytest.Pytester) -> None: 14 | pytester.makepyfile( 15 | """ 16 | import os 17 | 18 | def test_env(): 19 | assert 'DJANGO_SETTINGS_MODULE' not in os.environ 20 | 21 | def test_cfg(pytestconfig): 22 | assert pytestconfig.option.ds is None 23 | """ 24 | ) 25 | r = pytester.runpytest_subprocess() 26 | assert r.ret == 0 27 | 28 | 29 | def test_database(pytester: pytest.Pytester) -> None: 30 | pytester.makepyfile( 31 | """ 32 | import pytest 33 | 34 | @pytest.mark.django_db 35 | def test_mark(): 36 | assert 0 37 | 38 | @pytest.mark.django_db(transaction=True) 39 | def test_mark_trans(): 40 | assert 0 41 | 42 | def test_db(db): 43 | assert 0 44 | 45 | def test_transactional_db(transactional_db): 46 | assert 0 47 | """ 48 | ) 49 | r = pytester.runpytest_subprocess() 50 | assert r.ret == 0 51 | r.stdout.fnmatch_lines(["*4 skipped*"]) 52 | 53 | 54 | def test_client(pytester: pytest.Pytester) -> None: 55 | pytester.makepyfile( 56 | """ 57 | def test_client(client): 58 | assert 0 59 | 60 | def test_admin_client(admin_client): 61 | assert 0 62 | """ 63 | ) 64 | r = pytester.runpytest_subprocess() 65 | assert r.ret == 0 66 | r.stdout.fnmatch_lines(["*2 skipped*"]) 67 | 68 | 69 | def test_rf(pytester: pytest.Pytester) -> None: 70 | pytester.makepyfile( 71 | """ 72 | def test_rf(rf): 73 | assert 0 74 | """ 75 | ) 76 | r = pytester.runpytest_subprocess() 77 | assert r.ret == 0 78 | r.stdout.fnmatch_lines(["*1 skipped*"]) 79 | 80 | 81 | def test_settings(pytester: pytest.Pytester) -> None: 82 | pytester.makepyfile( 83 | """ 84 | def test_settings(settings): 85 | assert 0 86 | """ 87 | ) 88 | r = pytester.runpytest_subprocess() 89 | assert r.ret == 0 90 | r.stdout.fnmatch_lines(["*1 skipped*"]) 91 | 92 | 93 | def test_live_server(pytester: pytest.Pytester) -> None: 94 | pytester.makepyfile( 95 | """ 96 | def test_live_server(live_server): 97 | assert 0 98 | """ 99 | ) 100 | r = pytester.runpytest_subprocess() 101 | assert r.ret == 0 102 | r.stdout.fnmatch_lines(["*1 skipped*"]) 103 | 104 | 105 | def test_urls_mark(pytester: pytest.Pytester) -> None: 106 | pytester.makepyfile( 107 | """ 108 | import pytest 109 | 110 | @pytest.mark.urls('foo.bar') 111 | def test_urls(): 112 | assert 0 113 | """ 114 | ) 115 | r = pytester.runpytest_subprocess() 116 | assert r.ret == 0 117 | r.stdout.fnmatch_lines(["*1 skipped*"]) 118 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py313-dj{main,52,51}-postgres 4 | py312-dj{main,52,51,42}-postgres 5 | py311-dj{main,52,51,42}-postgres 6 | py310-dj{main,52,51,42}-postgres 7 | py39-dj42-postgres 8 | linting 9 | 10 | [testenv] 11 | extras = testing 12 | deps = 13 | djmain: https://github.com/django/django/archive/main.tar.gz 14 | dj52: Django>=5.2a1,<6.0 15 | dj51: Django>=5.1,<5.2 16 | dj50: Django>=5.0,<5.1 17 | dj42: Django>=4.2,<4.3 18 | 19 | mysql: mysqlclient==2.1.0 20 | 21 | postgres: psycopg[binary] 22 | coverage: coverage[toml] 23 | coverage: coverage-enable-subprocess 24 | 25 | pytestmin: pytest>=7.0,<7.1 26 | xdist: pytest-xdist>=1.15 27 | 28 | setenv = 29 | mysql: DJANGO_SETTINGS_MODULE=pytest_django_test.settings_mysql 30 | postgres: DJANGO_SETTINGS_MODULE=pytest_django_test.settings_postgres 31 | sqlite: DJANGO_SETTINGS_MODULE=pytest_django_test.settings_sqlite 32 | sqlite_file: DJANGO_SETTINGS_MODULE=pytest_django_test.settings_sqlite_file 33 | 34 | coverage: PYTESTDJANGO_TEST_RUNNER=coverage run -m pytest 35 | coverage: COVERAGE_PROCESS_START={toxinidir}/pyproject.toml 36 | coverage: COVERAGE_FILE={toxinidir}/.coverage 37 | coverage: PYTESTDJANGO_COVERAGE_SRC={toxinidir}/ 38 | 39 | passenv = PYTEST_ADDOPTS,TERM,TEST_DB_USER,TEST_DB_PASSWORD,TEST_DB_HOST 40 | usedevelop = True 41 | commands = 42 | coverage: coverage erase 43 | {env:PYTESTDJANGO_TEST_RUNNER:pytest} {posargs:tests} 44 | coverage: coverage combine 45 | coverage: coverage report 46 | coverage: coverage xml 47 | 48 | [testenv:linting] 49 | extras = 50 | deps = 51 | ruff==0.9.5 52 | mypy==1.15.0 53 | commands = 54 | ruff check --diff {posargs:pytest_django pytest_django_test tests} 55 | ruff format --quiet --diff {posargs:pytest_django pytest_django_test tests} 56 | mypy {posargs:pytest_django pytest_django_test tests} 57 | 58 | [testenv:doc8] 59 | extras = 60 | basepython = python3 61 | skip_install = true 62 | deps = 63 | sphinx 64 | doc8 65 | commands = 66 | doc8 docs/ 67 | 68 | [testenv:docs] 69 | deps = 70 | extras = docs 71 | commands = sphinx-build -n -W -b html -d docs/_build/doctrees docs docs/_build/html 72 | --------------------------------------------------------------------------------