├── .github └── workflows │ ├── check.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .readthedocs.yaml ├── AUTHORS.rst ├── HISTORY.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── behave_django ├── __init__.py ├── decorators.py ├── environment.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── behave.py ├── pageobject.py ├── runner.py └── testcase.py ├── docs ├── Makefile ├── conf.py ├── configuration.rst ├── contribute.rst ├── fixtures.rst ├── index.rst ├── installation.rst ├── isolation.rst ├── make.bat ├── pageobject.rst ├── setup.rst ├── testclient.rst ├── testcoverage.rst ├── usage.rst └── webbrowser.rst ├── pyproject.toml ├── tests ├── acceptance │ ├── environment.py │ ├── features │ │ ├── context-urlhelper.feature │ │ ├── database-transactions.feature │ │ ├── django-test-client.feature │ │ ├── django-unittest-asserts.feature │ │ ├── failing-feature.feature │ │ ├── fixture-decorator.feature │ │ ├── fixture-loading.feature │ │ ├── live-test-server.feature │ │ ├── running-tests.feature │ │ └── using-pageobject.feature │ └── steps │ │ ├── context-urlhelper.py │ │ ├── database_transactions.py │ │ ├── django-unittest-asserts.py │ │ ├── django_test_client.py │ │ ├── failing_feature.py │ │ ├── fixture-loading.py │ │ ├── live_test_server.py │ │ ├── pageobjects │ │ ├── __init__.py │ │ └── pages.py │ │ ├── running_tests.py │ │ └── using_pageobjects.py ├── manage.py ├── test_app │ ├── __init__.py │ ├── admin.py │ ├── features │ │ └── multiple-feature-directories.feature │ ├── fixtures │ │ ├── behave-fixtures.json │ │ ├── behave-second-fixture.json │ │ └── empty-fixture.json │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── templates │ │ ├── about.html │ │ └── index.html │ ├── tests.py │ └── views.py ├── test_project │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── unit │ ├── __init__.py │ ├── test_cli.py │ ├── test_exit_codes.py │ ├── test_passthru_args.py │ ├── test_simple_testcase.py │ ├── test_use_existing_db.py │ └── util.py └── tox.ini /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | env: 12 | PIP_DISABLE_PIP_VERSION_CHECK: '1' 13 | PY_COLORS: '1' 14 | 15 | jobs: 16 | check: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | env: 22 | - lint 23 | - format 24 | - package 25 | - docs 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-python@v5 29 | with: 30 | python-version: '3.10' 31 | - name: Install prerequisites 32 | run: python -m pip install tox 33 | - name: Run ${{ matrix.env }} 34 | run: tox -e ${{ matrix.env }} 35 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | env: 9 | PIP_DISABLE_PIP_VERSION_CHECK: '1' 10 | PY_COLORS: '1' 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-python@v5 18 | with: 19 | python-version: '3.10' 20 | - name: Install prerequisites 21 | run: python -m pip install build twine wheel 22 | - name: Build package 23 | run: python -m build 24 | - name: Upload to PyPI 25 | env: 26 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 27 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 28 | run: twine upload dist/* 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | env: 12 | PIP_DISABLE_PIP_VERSION_CHECK: '1' 13 | PY_COLORS: '1' 14 | 15 | jobs: 16 | python-django: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | python-version: 22 | - '3.12' 23 | - '3.11' 24 | - '3.10' 25 | django-version: 26 | - '5.1' 27 | - '5.0' 28 | - '4.2' 29 | include: 30 | - { python-version: '3.8', django-version: '4.2' } 31 | - { python-version: '3.9', django-version: '4.2' } 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/setup-python@v5 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | - name: Install prerequisites 38 | run: python -m pip install tox-gh-actions 39 | - name: Run tests (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }}) 40 | run: tox 41 | env: 42 | DJANGO: ${{ matrix.django-version }} 43 | behave-latest: 44 | runs-on: ubuntu-latest 45 | env: 46 | TOXENV: behave-latest 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: actions/setup-python@v5 50 | with: 51 | python-version: '3.10' 52 | - name: Install prerequisites 53 | run: python -m pip install tox 54 | - name: Run tests (${{ env.TOXENV }}) 55 | run: tox 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | venv/ 26 | .venv/ 27 | 28 | # No lockfiles, no version pinning (we develop a library) 29 | .python-version 30 | *.lock 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .cache 39 | *,cover 40 | .coverage 41 | .coverage.* 42 | coverage.xml 43 | nosetests.xml 44 | .pytest_cache/ 45 | .tox/ 46 | tests/TESTS-features.*.xml 47 | tests/*-report.xml 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff 54 | *.log 55 | *.sqlite3 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # IDE configs 61 | .idea/ 62 | .ropeproject/ 63 | .vscode/ 64 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # https://docs.readthedocs.io/en/stable/config-file/v2.html 3 | 4 | version: 2 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.11" 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | # We recommend specifying your dependencies to enable reproducible builds: 15 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 16 | # python: 17 | # install: 18 | # - requirements: docs/requirements.txt 19 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Maintainers 2 | ----------- 3 | 4 | * `Mitchel Cabuloy `_ (original author) 5 | * `Peter Bittner `_ 6 | * `Javier Buzzi `_ 7 | 8 | Contributors 9 | ------------ 10 | 11 | * `薛丞宏 `_ 12 | * `Alex Hutton `_ 13 | * `Dave Kwon `_ 14 | * `David Avsajanishvili `_ 15 | * `Dolan Antenucci `_ 16 | * `Ivan Rocha `_ 17 | * `Jens Engel `_ 18 | * `Jérôme Thiard `_ 19 | * `Karel Hovorka `_ 20 | * `Nate Hill `_ 21 | * `Nik Nyby `_ 22 | * `Paolo Melchiorre `_ 23 | * `Sebastian Manger `_ 24 | * `Tom Mortimer-Jones `_ 25 | * `Wojciech Banaś `_ 26 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | Release History 2 | --------------- 3 | 4 | 1.5.0 (unreleased) 5 | ++++++++++++++++++ 6 | 7 | **Features and Improvements** 8 | 9 | - Use ruff for linting and code style; reformat code base 10 | - Migrate packaging from ``setup.py`` to pure ``pyproject.toml``. 11 | - Add instructions to measure test coverage to the documentation 12 | - Cover Python 3.9 to 3.12 and Django 3.2, 4.2 and 5.0, drop Python 3.5, 3.6 and Django 2.2 and 3.0 support 13 | - Bump Behave requirement to 1.2.7.dev3/4/5 (allows TOML support and option to change the Behave TestRunner) 14 | - New option to change the Django TestRunner 15 | 16 | 1.4.0 (2020-06-15) 17 | ++++++++++++++++++ 18 | 19 | **Features and Improvements** 20 | 21 | - Add experimental `Page Object pattern`_ helpers 22 | - Cover Python 3.8, drop Python 3.4 and Django 1.11 to 2.1 support 23 | 24 | **Bugfixes** 25 | 26 | - Replace deprecated `multi_db`_ by suggested ``databases`` attribute 27 | - Remove obsolete Python 2 compatibility code 28 | 29 | .. _Page Object pattern: https://www.martinfowler.com/bliki/PageObject.html 30 | 31 | 1.3.0 (2019-04-16) 32 | ++++++++++++++++++ 33 | 34 | **Features and Improvements** 35 | 36 | - Add Bandit security linter to CI setup 37 | - Minor refactorings to please linters 38 | - Update and clarify documentation 39 | - Cover Django 2.2 with test matrix, remove Django 2.0 40 | 41 | **Bugfixes** 42 | 43 | - Fix fixtures decorator behavior (reset between scenarios) 44 | 45 | 1.2.0 (2018-03-12) 46 | ++++++++++++++++++ 47 | 48 | **Features and Improvements** 49 | 50 | - Added option to set `multi_db`_ on TestCase 51 | 52 | **Bugfixes** 53 | 54 | - Made fixtures decorator compatible with newly released behave 55 | 56 | .. _multi_db: https://docs.djangoproject.com/en/stable/topics/testing/tools/#testing-multi-db 57 | 58 | 1.1.0 (2017-01-29) 59 | ++++++++++++++++++ 60 | 61 | **Features and Improvements** 62 | 63 | - Added :code:`django_ready` hook for running setup code within the django environment 64 | 65 | 1.0.0 (2017-10-25) 66 | ++++++++++++++++++ 67 | 68 | **Features and Improvements** 69 | 70 | - Added decorator to load fixtures 71 | - Updated django integration logic 72 | 73 | 0.5.0 (2017-03-19) 74 | ++++++++++++++++++ 75 | 76 | **Features and Improvements** 77 | 78 | - Added :code:`--simple` command line option to run tests using the 79 | regular :code:`TestCase` class instead of :code:`StaticLiveServerTestCase` 80 | 81 | 0.4.1 (2017-01-16) 82 | ++++++++++++++++++ 83 | 84 | **Features and Improvements** 85 | 86 | - Behave's short form arguments are now accepted (e.g. :code:`-i` for :code:`--include`) 87 | - Added :code:`--keepdb` short form argument, `-k` 88 | - Prefix conflicting command line options with :code:`--behave` 89 | 90 | **Bugfixes** 91 | 92 | - Fixed specifying paths didn't work 93 | 94 | 0.4.0 (2016-08-23) 95 | ++++++++++++++++++ 96 | 97 | **Features and Improvements** 98 | 99 | - Replace `optparse` with `argparse` 100 | - Support Django 1.8 + 1.9 + 1.10 101 | 102 | 0.3.0 (2015-10-27) 103 | ++++++++++++++++++ 104 | 105 | **Features and Improvements** 106 | 107 | - Added the :code:`--keepdb` flag to reuse the existing test database 108 | instead of recreating it for every test run. (Django >= 1.8 only) 109 | - Overhaul tests to use Tox and pytest for a better testing experience. 110 | 111 | 0.2.3 (2015-08-21) 112 | ++++++++++++++++++ 113 | 114 | **Bugfixes** 115 | 116 | - Fixed bug where some behave commands do not work 117 | 118 | 0.2.2 (2015-07-13) 119 | ++++++++++++++++++ 120 | 121 | **Bugfixes** 122 | 123 | - Fixed bug where positional arguments don't get sent to behave. 124 | 125 | 0.2.1 (2015-06-30) 126 | ++++++++++++++++++ 127 | 128 | **Bugfixes** 129 | 130 | - Fixed bug where invalid arguments are passed onto behave, making the command fail to execute. 131 | 132 | 0.2.0 (2015-06-27) 133 | ++++++++++++++++++ 134 | 135 | **Features and Improvements** 136 | 137 | - Integration with :code:`behave` is now done via monkey patching. 138 | Including the :code:`environment.before_scenario()` and 139 | :code:`environment.after_scenario()` function calls in your 140 | :code:`environment.py` file is no longer needed. 141 | - A new CLI option, :code:`--use-existing-database`, has been added. 142 | See the `Configuration docs`_. 143 | 144 | **Bugfixes** 145 | 146 | - Calling :code:`python manage.py behave --dry-run` does not create a 147 | test database any longer. 148 | 149 | .. _Configuration docs: 150 | https://behave-django.readthedocs.io/en/latest/configuration.html 151 | 152 | 0.1.4 (2015-06-08) 153 | ++++++++++++++++++ 154 | 155 | **Features and Improvements** 156 | 157 | - :code:`context.get_url()`. URL helper attached to context with built-in 158 | reverse resolution as a handy shortcut. 159 | 160 | 0.1.3 (2015-05-13) 161 | ++++++++++++++++++ 162 | 163 | **Features and Improvements** 164 | 165 | - Fixture loading. You can now load your fixtures by setting :code:`context.fixtures`. 166 | - behave-django now supports all versions of Django 167 | 168 | **Bugfixes** 169 | 170 | - The behave command should now correctly return non-zero exit codes when a test fails. 171 | 172 | 0.1.2 (2015-04-06) 173 | ++++++++++++++++++ 174 | 175 | **Features and Improvements** 176 | 177 | - You can now have a :code:`.behaverc` in your project's root directory. 178 | You can specify where your feature directories are in this file, among 179 | other things. See the `behave docs on configuration files`_. 180 | - Removed :code:`BEHAVE\_FEATURES` setting in favor of using behave's configuration file 181 | 182 | .. _behave docs on configuration files: 183 | https://behave.readthedocs.io/en/latest/behave.html#configuration-files 184 | 185 | 0.1.1 (2015-04-04) 186 | ++++++++++++++++++ 187 | 188 | **Features and Improvements** 189 | 190 | - Behave management command now accepts behave command line arguments 191 | - :code:`BEHAVE\_FEATURES` settings added for multiple feature directories 192 | 193 | **Bugfixes** 194 | 195 | - Removed test apps and projects from the release package 196 | 197 | 0.1.0 (2015-04-02) 198 | ++++++++++++++++++ 199 | 200 | - Initial release 201 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mitchel Cabuloy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include behave_django *.py 4 | prune tests 5 | prune docs 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | behave-django |latest-version| 2 | ============================== 3 | 4 | |check-status| |test-status| |python-support| |license| |docs-status| |gitter| 5 | 6 | Behave BDD integration for Django 7 | 8 | .. features-marker 9 | 10 | Features 11 | -------- 12 | 13 | - Web browser automation ready 14 | - Database transactions per scenario 15 | - Use Django's test client 16 | - Use unittest + Django assert library 17 | - Use behave's command line arguments 18 | - Use behave's configuration file 19 | - Fixture loading 20 | - Page objects 21 | 22 | .. support-marker 23 | 24 | Version Support 25 | --------------- 26 | 27 | *behave-django* is `tested against`_ the officially supported combinations of 28 | Python and Django (Django 4.2, 5.0, 5.1 on Python 3.8 through 3.12). 29 | 30 | *behave-django* requires a few newer features of *behave* and hence installs 31 | a recent unreleased version of `behave`_ as a dependency. 32 | 33 | .. docs-marker 34 | 35 | Documentation 36 | ------------- 37 | 38 | - Documentation is available from `behave-django.readthedocs.io`_ 39 | - Read more about *behave* at `behave.readthedocs.io`_ 40 | 41 | .. contribute-marker 42 | 43 | How to Contribute 44 | ----------------- 45 | 46 | Please, read the `contributing guide`_ in the docs. 47 | 48 | .. references-marker 49 | 50 | 51 | .. _tested against: https://github.com/behave/behave-django/actions/workflows/test.yml 52 | .. _behave: https://pypi.org/project/behave/ 53 | .. _behave-django.readthedocs.io: https://behave-django.readthedocs.io/en/latest/ 54 | .. _behave.readthedocs.io: https://behave.readthedocs.io/en/latest/usecase_django/ 55 | .. _contributing guide: https://behave-django.readthedocs.io/en/latest/contribute.html 56 | .. |latest-version| image:: https://img.shields.io/pypi/v/behave-django.svg 57 | :target: https://pypi.org/project/behave-django/ 58 | :alt: Latest version 59 | .. |check-status| image:: https://github.com/behave/behave-django/actions/workflows/check.yml/badge.svg 60 | :target: https://github.com/behave/behave-django/actions/workflows/check.yml 61 | :alt: Code checks status 62 | .. |test-status| image:: https://github.com/behave/behave-django/actions/workflows/test.yml/badge.svg 63 | :target: https://github.com/behave/behave-django/actions/workflows/test.yml 64 | :alt: Test suite status 65 | .. |python-support| image:: https://img.shields.io/pypi/pyversions/behave-django.svg 66 | :target: https://pypi.org/project/behave-django/ 67 | :alt: Python versions 68 | .. |license| image:: https://img.shields.io/pypi/l/behave-django.svg 69 | :target: https://github.com/behave/behave-django/blob/main/LICENSE 70 | :alt: Software license 71 | .. |docs-status| image:: https://img.shields.io/readthedocs/behave-django/stable.svg 72 | :target: https://readthedocs.org/projects/behave-django/ 73 | :alt: Documentation Status 74 | .. |gitter| image:: https://img.shields.io/gitter/room/behave/behave-django.svg 75 | :alt: Gitter chat room 76 | :target: https://gitter.im/behave/behave-django 77 | -------------------------------------------------------------------------------- /behave_django/__init__.py: -------------------------------------------------------------------------------- 1 | """Behave BDD integration for Django""" 2 | 3 | __version__ = '1.5.0' 4 | -------------------------------------------------------------------------------- /behave_django/decorators.py: -------------------------------------------------------------------------------- 1 | def fixtures(*fixture_files): 2 | """ 3 | Provide fixtures for given step_impl. Fixtures will be loaded in 4 | environment.py#before_scenario. 5 | 6 | @fixtures('data.json') 7 | @when('a user clicks the button') 8 | def step_impl(context): 9 | pass 10 | 11 | :param fixture_files: list of fixture files 12 | """ 13 | 14 | def wrapper(step_impl): 15 | setattr(step_impl, 'registered_fixtures', fixture_files) 16 | return step_impl 17 | 18 | return wrapper 19 | -------------------------------------------------------------------------------- /behave_django/environment.py: -------------------------------------------------------------------------------- 1 | from copy import copy 2 | 3 | from behave import step_registry as module_step_registry 4 | from behave.runner import Context, ModelRunner 5 | from django.shortcuts import resolve_url 6 | 7 | 8 | class PatchedContext(Context): 9 | """Provides methods for Behave's ``context`` variable.""" 10 | 11 | @property 12 | def base_url(self): 13 | try: 14 | return self.test.live_server_url 15 | except AttributeError as err: 16 | msg = ( 17 | 'Web browser automation is not available. ' 18 | 'This scenario step can not be run with the --simple or -S flag.' 19 | ) 20 | raise RuntimeError(msg) from err 21 | 22 | def get_url(self, to=None, *args, **kwargs): 23 | return self.base_url + (resolve_url(to, *args, **kwargs) if to else '') 24 | 25 | 26 | def load_registered_fixtures(context): 27 | """ 28 | Apply fixtures that are registered with the @fixtures decorator. 29 | """ 30 | # -- SELECT STEP REGISTRY: 31 | # HINT: Newer behave versions use runner.step_registry 32 | # to be able to support multiple runners, each with its own step_registry. 33 | runner = context._runner 34 | step_registry = getattr(runner, 'step_registry', None) 35 | if not step_registry: 36 | # -- BACKWARD-COMPATIBLE: Use module_step_registry 37 | step_registry = module_step_registry.registry 38 | 39 | # -- SETUP SCENARIO FIXTURES: 40 | for step in context.scenario.all_steps: 41 | match = step_registry.find_match(step) 42 | if match and hasattr(match.func, 'registered_fixtures'): 43 | if not context.test.fixtures: 44 | context.test.fixtures = [] 45 | context.test.fixtures.extend(match.func.registered_fixtures) 46 | 47 | 48 | class BehaveHooksMixin: 49 | """ 50 | Provides methods that run during test execution. 51 | 52 | These methods are attached to behave via monkey patching. 53 | """ 54 | 55 | testcase_class = None 56 | 57 | def patch_context(self, context): 58 | """ 59 | Patches the context to add utility functions 60 | 61 | Sets up the base_url, and the get_url() utility function. 62 | """ 63 | context.__class__ = PatchedContext 64 | # Simply setting __class__ directly doesn't work 65 | # because behave.runner.Context.__setattr__ is implemented wrongly. 66 | object.__setattr__(context, '__class__', PatchedContext) 67 | 68 | def setup_testclass(self, context): 69 | """ 70 | Adds the test instance to context 71 | """ 72 | context.test = self.testcase_class() 73 | 74 | def setup_fixtures(self, context): 75 | """ 76 | Sets up fixtures 77 | """ 78 | if getattr(context, 'fixtures', None): 79 | context.test.fixtures = copy(context.fixtures) 80 | 81 | if getattr(context, 'reset_sequences', None): 82 | context.test.reset_sequences = context.reset_sequences 83 | 84 | if getattr(context, 'databases', None): 85 | context.test.__class__.databases = context.databases 86 | 87 | if hasattr(context, 'scenario'): 88 | load_registered_fixtures(context) 89 | 90 | def setup_test(self, context): 91 | """ 92 | Sets up the Django test 93 | 94 | This method runs the code necessary to create the test database, start 95 | the live server, etc. 96 | """ 97 | context.test._pre_setup(run=True) 98 | context.test.setUpClass() 99 | context.test() 100 | 101 | def teardown_test(self, context): 102 | """ 103 | Tears down the Django test 104 | """ 105 | context.test.tearDownClass() 106 | context.test._post_teardown(run=True) 107 | del context.test 108 | 109 | 110 | def monkey_patch_behave(django_test_runner): 111 | """ 112 | Integrate behave_django in behave via before/after scenario hooks 113 | """ 114 | behave_run_hook = ModelRunner.run_hook 115 | 116 | def run_hook(self, name, context, *args): 117 | if name == 'before_all': 118 | django_test_runner.patch_context(context) 119 | 120 | behave_run_hook(self, name, context, *args) 121 | 122 | if name == 'before_scenario': 123 | django_test_runner.setup_testclass(context) 124 | django_test_runner.setup_fixtures(context) 125 | django_test_runner.setup_test(context) 126 | behave_run_hook(self, 'django_ready', context) 127 | 128 | if name == 'after_scenario': 129 | django_test_runner.teardown_test(context) 130 | 131 | ModelRunner.run_hook = run_hook 132 | -------------------------------------------------------------------------------- /behave_django/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behave/behave-django/9574e23fd1cbf7fcbd7c160fa2852134232be26e/behave_django/management/__init__.py -------------------------------------------------------------------------------- /behave_django/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behave/behave-django/9574e23fd1cbf7fcbd7c160fa2852134232be26e/behave_django/management/commands/__init__.py -------------------------------------------------------------------------------- /behave_django/management/commands/behave.py: -------------------------------------------------------------------------------- 1 | """Main entry point, the ``behave`` management command for Django.""" 2 | 3 | import sys 4 | from argparse import ArgumentTypeError 5 | from importlib import import_module 6 | 7 | from behave.__main__ import main as behave_main 8 | from behave.configuration import OPTIONS as behave_options 9 | from django.core.management.base import BaseCommand 10 | 11 | from behave_django.environment import monkey_patch_behave 12 | from behave_django.runner import ( 13 | BehaviorDrivenTestRunner, 14 | ExistingDatabaseTestRunner, 15 | SimpleTestRunner, 16 | ) 17 | 18 | 19 | def valid_python_module(path): 20 | try: 21 | module_path, class_name = path.rsplit(':', 1) 22 | module = import_module(module_path) 23 | return getattr(module, class_name) 24 | except (ValueError, ImportError) as err: 25 | msg = f"Failed to import module '{module_path}'." 26 | raise ArgumentTypeError(msg) from err 27 | except AttributeError as err: 28 | msg = f"No class '{class_name}' found in '{module_path}'." 29 | raise ArgumentTypeError(msg) from err 30 | 31 | 32 | def add_command_arguments(parser): 33 | """Command line arguments for the behave management command.""" 34 | 35 | parser.add_argument( 36 | '--noinput', 37 | '--no-input', 38 | action='store_const', 39 | const=False, 40 | dest='interactive', 41 | help='Tells Django to NOT prompt the user for input of any kind.', 42 | ) 43 | parser.add_argument( 44 | '--failfast', 45 | action='store_const', 46 | const=True, 47 | dest='failfast', 48 | help='Tells Django to stop running the test suite after first failed test.', 49 | ) 50 | parser.add_argument( 51 | '-r', 52 | '--reverse', 53 | action='store_const', 54 | const=True, 55 | dest='reverse', 56 | help='Reverses test cases order.', 57 | ) 58 | parser.add_argument( 59 | '--use-existing-database', 60 | action='store_true', 61 | default=False, 62 | help="Don't create a test database. USE AT YOUR OWN RISK!", 63 | ) 64 | parser.add_argument( 65 | '-k', 66 | '--keepdb', 67 | action='store_const', 68 | const=True, 69 | help='Preserves the test DB between runs.', 70 | ) 71 | parser.add_argument( 72 | '-S', 73 | '--simple', 74 | action='store_true', 75 | default=False, 76 | help="Use simple test runner that supports Django's testing client only" 77 | ' (no web browser automation)', 78 | ) 79 | parser.add_argument( 80 | '--runner', 81 | action='store', 82 | type=valid_python_module, 83 | default='behave_django.runner:BehaviorDrivenTestRunner', 84 | help='Full Python dotted path to a package, module, Django TestRunner.' 85 | ' Defaults to "%(default)s".', 86 | ) 87 | 88 | 89 | def add_behave_arguments(parser): 90 | """Additional command line arguments extracted from upstream behave.""" 91 | 92 | # Option strings that conflict with Django 93 | conflicts = [ 94 | '-c', 95 | '-k', 96 | '-r', 97 | '-S', 98 | '-v', 99 | '--no-color', 100 | '--runner', 101 | '--simple', 102 | '--version', 103 | ] 104 | 105 | parser.add_argument( 106 | 'paths', 107 | action='store', 108 | nargs='*', 109 | help='Feature directory, file or file location (FILE:LINE).', 110 | ) 111 | 112 | for fixed, keywords in behave_options: 113 | keywords = keywords.copy() 114 | 115 | # Configfile only entries are ignored 116 | if not fixed: 117 | continue 118 | 119 | # Build option strings 120 | option_strings = [] 121 | for option in fixed: 122 | # Prefix conflicting option strings with `--behave` 123 | if option in conflicts: 124 | prefix = '--' if option.startswith('--') else '-' 125 | option = option.replace(prefix, '--behave-', 1) 126 | 127 | option_strings.append(option) 128 | 129 | # config_help isn't a valid keyword for add_argument 130 | if 'config_help' in keywords: 131 | keywords['help'] = keywords['config_help'] 132 | del keywords['config_help'] 133 | 134 | parser.add_argument(*option_strings, **keywords) 135 | 136 | 137 | class Command(BaseCommand): 138 | help = 'Runs behave tests' 139 | 140 | def add_arguments(self, parser): 141 | """Add behave's and our command line arguments to the command.""" 142 | 143 | parser.usage = '%(prog)s [options] [ [DIR|FILE|FILE:LINE] ]+' 144 | parser.description = """\ 145 | Run a number of feature tests with behave.""" 146 | 147 | add_command_arguments(parser) 148 | add_behave_arguments(parser) 149 | 150 | def handle(self, *args, **options): 151 | """Main entry point when django-behave executes.""" 152 | 153 | django_runner_class = options['runner'] 154 | 155 | # FIXME: This check should be unnecessary. BUG: if no `--runner` 156 | # option is specified the value of `--behave-runner`` is provided. 157 | if isinstance(django_runner_class, str): 158 | django_runner_class = BehaviorDrivenTestRunner 159 | 160 | is_default_runner = django_runner_class is BehaviorDrivenTestRunner 161 | 162 | if is_default_runner: 163 | if options['dry_run'] or options['use_existing_database']: 164 | django_runner_class = ExistingDatabaseTestRunner 165 | elif options['simple']: 166 | django_runner_class = SimpleTestRunner 167 | 168 | elif options['use_existing_database'] or options['simple']: 169 | self.stderr.write( 170 | self.style.WARNING( 171 | '--use-existing-database or --simple has no effect' 172 | ' together with --runner' 173 | ) 174 | ) 175 | 176 | if options['use_existing_database'] and options['simple']: 177 | self.stderr.write( 178 | self.style.WARNING( 179 | '--simple flag has no effect together with --use-existing-database' 180 | ) 181 | ) 182 | 183 | # Configure django environment 184 | passthru_args = ['failfast', 'interactive', 'keepdb', 'reverse'] 185 | runner_args = { 186 | k: v for k, v in options.items() if k in passthru_args and v is not None 187 | } 188 | 189 | django_test_runner = django_runner_class(**runner_args) 190 | django_test_runner.setup_test_environment() 191 | 192 | old_config = django_test_runner.setup_databases() 193 | 194 | # Run Behave tests 195 | monkey_patch_behave(django_test_runner) 196 | behave_args = self.get_behave_args() 197 | exit_status = behave_main(args=behave_args) 198 | 199 | # Teardown django environment 200 | django_test_runner.teardown_databases(old_config) 201 | django_test_runner.teardown_test_environment() 202 | 203 | if exit_status != 0: 204 | sys.exit(exit_status) 205 | 206 | def get_behave_args(self, argv=sys.argv): 207 | """ 208 | Get a list of those command line arguments specified with the 209 | management command that are meant as arguments for running behave. 210 | """ 211 | parser = BehaveArgsHelper().create_parser('manage.py', 'behave') 212 | args, unknown = parser.parse_known_args(argv[2:]) 213 | 214 | behave_args = [] 215 | for option in unknown: 216 | # Remove behave prefix 217 | if option.startswith('--behave-'): 218 | option = option.replace('--behave-', '', 1) 219 | prefix = '-' if len(option) == 1 else '--' 220 | option = prefix + option 221 | 222 | behave_args.append(option) 223 | 224 | return behave_args 225 | 226 | 227 | class BehaveArgsHelper(Command): 228 | """Command line parser for passing arguments down to behave.""" 229 | 230 | def add_arguments(self, parser): 231 | """ 232 | Override setup of command line arguments to make behave commands not 233 | be recognized. The unrecognized args will then be for behave! :) 234 | """ 235 | add_command_arguments(parser) 236 | -------------------------------------------------------------------------------- /behave_django/pageobject.py: -------------------------------------------------------------------------------- 1 | """ 2 | A headless Page Object pattern implementation. 3 | 4 | Background reading: https://www.martinfowler.com/bliki/PageObject.html 5 | """ 6 | 7 | import django.shortcuts 8 | from bs4 import BeautifulSoup 9 | 10 | 11 | class WrongElementError(RuntimeError): 12 | """ 13 | A different PageObject element was expected by the getter from the 14 | ```elements`` dictionary. 15 | """ 16 | 17 | def __init__(self, element, expected): 18 | message = f'Expected {expected}, found {element.__class__}' 19 | super().__init__(message) 20 | 21 | 22 | class PageObject: 23 | """ 24 | Headless page object pattern implementation. 25 | 26 | :page: 27 | view name, model or URL path for ``django.shortcuts.resolve_url`` 28 | to load the respective page using the Django test client 29 | :elements: 30 | Dictionary of elements accessible by helper methods 31 | """ 32 | 33 | page = None 34 | elements = {} 35 | 36 | def __init__(self, context): 37 | """ 38 | Load and parse the page specified by the ``page`` class attribute. 39 | 40 | :context: 41 | Behave's context patched with Django support by behave-django 42 | """ 43 | urlpath = django.shortcuts.resolve_url(self.__class__.page) 44 | self.response = context.test.client.get(urlpath) 45 | self.document = BeautifulSoup(self.response.content, 'html.parser') 46 | self.request = self.response.request 47 | self.context = context 48 | 49 | def __eq__(self, other): 50 | """ 51 | Override default implementation, ignoring changing details of 52 | request and response. 53 | 54 | Instead of page we compare the request URL path, which is the 55 | resolved value and should always match for equal pages. 56 | """ 57 | return ( 58 | isinstance(other, PageObject) 59 | and self.elements == other.elements 60 | and self.document.string == other.document.string 61 | and self.request == other.request 62 | and self.response.status_code == other.response.status_code 63 | ) 64 | 65 | def _get_element_ensure(self, name, ensure): 66 | """ 67 | Return a subdocument matching the CSS selector of ``elements[name]`` 68 | and enforcing a requested PageObject element class. 69 | 70 | :raises KeyError: 71 | if there is no element configured with the ``name`` key 72 | :raises WrongElementError: 73 | if the element found for ``name`` doesn't match the requested type 74 | """ 75 | element = self.__class__.elements[name] 76 | if isinstance(element, ensure): 77 | return element 78 | raise WrongElementError(element, expected=ensure) 79 | 80 | def get_element(self, name): 81 | """ 82 | Return a subdocument matching the CSS selector of elements[name]. 83 | """ 84 | return self.get_elements(name)[0] 85 | 86 | def get_elements(self, name): 87 | """ 88 | Return all subdocuments matching the CSS selector of elements[name]. 89 | """ 90 | element = self._get_element_ensure(name, Element) 91 | return self.document.select(element.selector) 92 | 93 | def get_link(self, name): 94 | """ 95 | Return a subdocument matching the CSS selector of elements[name], 96 | patched with methods for a link. 97 | """ 98 | return self.get_links(name)[0] 99 | 100 | def get_links(self, name): 101 | """ 102 | Return all subdocuments matching the CSS selector of elements[name], 103 | patched with methods for a link. 104 | """ 105 | current_context = self.context 106 | element = self._get_element_ensure(name, Link) 107 | links = self.document.select(element.selector) 108 | 109 | for link in links: 110 | 111 | def click(): 112 | """Visit a link, load related URL, return a PageObject""" 113 | href = link.get('href') 114 | 115 | class NewPageObject(PageObject): 116 | page = href 117 | 118 | return NewPageObject(current_context) 119 | 120 | link.click = click 121 | return links 122 | 123 | 124 | class Element: 125 | """An arbitrary HTML element""" 126 | 127 | def __init__(self, css): 128 | self.selector = css 129 | 130 | 131 | class Link(Element): 132 | """A HTML anchor element representing a hyperlink""" 133 | -------------------------------------------------------------------------------- /behave_django/runner.py: -------------------------------------------------------------------------------- 1 | from django.test.runner import DiscoverRunner 2 | 3 | from behave_django.environment import BehaveHooksMixin 4 | from behave_django.testcase import ( 5 | BehaviorDrivenTestCase, 6 | DjangoSimpleTestCase, 7 | ExistingDatabaseTestCase, 8 | ) 9 | 10 | 11 | class BehaviorDrivenTestRunner(DiscoverRunner, BehaveHooksMixin): 12 | """Test runner that uses the BehaviorDrivenTestCase.""" 13 | 14 | testcase_class = BehaviorDrivenTestCase 15 | 16 | 17 | class ExistingDatabaseTestRunner(DiscoverRunner, BehaveHooksMixin): 18 | """Test runner that uses the ExistingDatabaseTestCase. 19 | 20 | This test runner nullifies Django's test database setup methods. Using this 21 | test runner would make your tests run with the default configured database 22 | in settings.py. 23 | """ 24 | 25 | testcase_class = ExistingDatabaseTestCase 26 | 27 | def setup_databases(self, **kwargs): 28 | pass 29 | 30 | def teardown_databases(self, old_config, **kwargs): 31 | pass 32 | 33 | 34 | class SimpleTestRunner(DiscoverRunner, BehaveHooksMixin): 35 | """Test runner that uses DjangoSimpleTestCase with atomic 36 | transaction management and no support of web browser automation. 37 | """ 38 | 39 | testcase_class = DjangoSimpleTestCase 40 | -------------------------------------------------------------------------------- /behave_django/testcase.py: -------------------------------------------------------------------------------- 1 | from django.contrib.staticfiles.testing import StaticLiveServerTestCase 2 | from django.test.testcases import TestCase 3 | 4 | 5 | class BehaviorDrivenTestMixin: 6 | """Prevents the TestCase from executing its setup and teardown methods. 7 | 8 | This mixin overrides the test case's ``_pre_setup`` and ``_post_teardown`` 9 | methods in order to prevent them from executing when the test case is 10 | instantiated. We do this to have total control over the test execution. 11 | """ 12 | 13 | def _pre_setup(self, run=False): 14 | if run: 15 | super()._pre_setup() 16 | 17 | def _post_teardown(self, run=False): 18 | if run: 19 | super()._post_teardown() 20 | 21 | def runTest(self): 22 | pass 23 | 24 | 25 | class BehaviorDrivenTestCase(BehaviorDrivenTestMixin, StaticLiveServerTestCase): 26 | """Test case attached to the context during behave execution. 27 | 28 | This test case prevents the regular tests from running. 29 | """ 30 | 31 | 32 | class ExistingDatabaseTestCase(BehaviorDrivenTestCase): 33 | """Test case used for the --use-existing-database setup. 34 | 35 | This test case prevents fixtures from being loaded to the database in use. 36 | """ 37 | 38 | def _fixture_setup(self): 39 | pass 40 | 41 | def _fixture_teardown(self): 42 | pass 43 | 44 | 45 | class DjangoSimpleTestCase(BehaviorDrivenTestMixin, TestCase): 46 | """Test case attached to the context during behave execution. 47 | 48 | This test case uses `transaction.atomic()` to achieve test isolation 49 | instead of flushing the entire database. As a result, tests run much 50 | quicker and have no issues with altered DB state after all tests ran 51 | when `--keepdb` is used. 52 | 53 | As a side effect, this test case does not support web browser automation. 54 | Use Django's testing client instead to test requests and responses. 55 | 56 | Also, it prevents the regular tests from running. 57 | """ 58 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/behave-django.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/behave-django.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/behave-django" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/behave-django" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # behave-django documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Jun 9 15:52:13 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import os 16 | import shlex 17 | import sys 18 | 19 | sys.path.insert(0, os.path.abspath('..')) 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | #sys.path.insert(0, os.path.abspath('.')) 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | #needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | from behave_django import __version__ 35 | 36 | extensions = [ 37 | 'sphinx.ext.autodoc', 38 | 'sphinx.ext.viewcode', 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # The suffix(es) of source filenames. 45 | # You can specify multiple suffix as a list of string: 46 | # source_suffix = ['.rst', '.md'] 47 | source_suffix = '.rst' 48 | 49 | # The encoding of source files. 50 | #source_encoding = 'utf-8-sig' 51 | 52 | # The master toctree document. 53 | master_doc = 'index' 54 | 55 | # General information about the project. 56 | project = 'behave-django' 57 | copyright = '2020, Mitchel Cabuloy & Peter Bittner' 58 | author = 'Mitchel Cabuloy and contributors' 59 | 60 | # The version info for the project you're documenting, acts as replacement for 61 | # |version| and |release|, also used in various other places throughout the 62 | # built documents. 63 | # 64 | # The short X.Y version. 65 | version = __version__ 66 | # The full version, including alpha/beta/rc tags. 67 | release = __version__ 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | # 72 | # This is also used if you do content translation via gettext catalogs. 73 | # Usually you set "language" from the command line for these cases. 74 | language = 'en' 75 | 76 | # There are two options for replacing |today|: either, you set today to some 77 | # non-false value, then it is used: 78 | #today = '' 79 | # Else, today_fmt is used as the format for a strftime call. 80 | #today_fmt = '%B %d, %Y' 81 | 82 | # List of patterns, relative to source directory, that match files and 83 | # directories to ignore when looking for source files. 84 | exclude_patterns = ['_build'] 85 | 86 | # The reST default role (used for this markup: `text`) to use for all 87 | # documents. 88 | #default_role = None 89 | 90 | # If true, '()' will be appended to :func: etc. cross-reference text. 91 | #add_function_parentheses = True 92 | 93 | # If true, the current module name will be prepended to all description 94 | # unit titles (such as .. function::). 95 | #add_module_names = True 96 | 97 | # If true, sectionauthor and moduleauthor directives will be shown in the 98 | # output. They are ignored by default. 99 | #show_authors = False 100 | 101 | # The name of the Pygments (syntax highlighting) style to use. 102 | pygments_style = 'sphinx' 103 | 104 | # A list of ignored prefixes for module index sorting. 105 | #modindex_common_prefix = [] 106 | 107 | # If true, keep warnings as "system message" paragraphs in the built documents. 108 | #keep_warnings = False 109 | 110 | # If true, `todo` and `todoList` produce output, else they produce nothing. 111 | todo_include_todos = False 112 | 113 | 114 | # -- Options for HTML output ---------------------------------------------- 115 | 116 | # The theme to use for HTML and HTML Help pages. See the documentation for 117 | # a list of builtin themes. 118 | html_theme = 'alabaster' 119 | 120 | # Theme options are theme-specific and customize the look and feel of a theme 121 | # further. For a list of options available for each theme, see the 122 | # documentation. 123 | html_theme_options = { 124 | 'fixed_sidebar': True, 125 | 'github_user': 'behave', 126 | 'github_repo': 'behave-django', 127 | 'github_banner': True, 128 | 'show_related': True, 129 | } 130 | 131 | # Add any paths that contain custom themes here, relative to this directory. 132 | #html_theme_path = [] 133 | 134 | # The name for this set of Sphinx documents. If None, it defaults to 135 | # " v documentation". 136 | #html_title = None 137 | 138 | # A shorter title for the navigation bar. Default is the same as html_title. 139 | #html_short_title = None 140 | 141 | # The name of an image file (relative to this directory) to place at the top 142 | # of the sidebar. 143 | #html_logo = None 144 | 145 | # The name of an image file (within the static path) to use as favicon of the 146 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 147 | # pixels large. 148 | #html_favicon = None 149 | 150 | # Add any paths that contain custom static files (such as style sheets) here, 151 | # relative to this directory. They are copied after the builtin static files, 152 | # so a file named "default.css" will overwrite the builtin "default.css". 153 | # html_static_path = ['_static'] 154 | 155 | # Add any extra paths that contain custom files (such as robots.txt or 156 | # .htaccess) here, relative to this directory. These files are copied 157 | # directly to the root of the documentation. 158 | #html_extra_path = [] 159 | 160 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 161 | # using the given strftime format. 162 | #html_last_updated_fmt = '%b %d, %Y' 163 | 164 | # If true, SmartyPants will be used to convert quotes and dashes to 165 | # typographically correct entities. 166 | #html_use_smartypants = True 167 | 168 | # Custom sidebar templates, maps document names to template names. 169 | #html_sidebars = {} 170 | 171 | # Additional templates that should be rendered to pages, maps page names to 172 | # template names. 173 | #html_additional_pages = {} 174 | 175 | # If false, no module index is generated. 176 | #html_domain_indices = True 177 | 178 | # If false, no index is generated. 179 | #html_use_index = True 180 | 181 | # If true, the index is split into individual pages for each letter. 182 | #html_split_index = False 183 | 184 | # If true, links to the reST sources are added to the pages. 185 | #html_show_sourcelink = True 186 | 187 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 188 | #html_show_sphinx = True 189 | 190 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 191 | #html_show_copyright = True 192 | 193 | # If true, an OpenSearch description file will be output, and all pages will 194 | # contain a tag referring to it. The value of this option must be the 195 | # base URL from which the finished HTML is served. 196 | #html_use_opensearch = '' 197 | 198 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 199 | #html_file_suffix = None 200 | 201 | # Language to be used for generating the HTML full-text search index. 202 | # Sphinx supports the following languages: 203 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 204 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 205 | #html_search_language = 'en' 206 | 207 | # A dictionary with options for the search language support, empty by default. 208 | # Now only 'ja' uses this config value 209 | #html_search_options = {'type': 'default'} 210 | 211 | # The name of a javascript file (relative to the configuration directory) that 212 | # implements a search results scorer. If empty, the default will be used. 213 | #html_search_scorer = 'scorer.js' 214 | 215 | # Output file base name for HTML help builder. 216 | htmlhelp_basename = 'behave-djangodoc' 217 | 218 | # -- Options for LaTeX output --------------------------------------------- 219 | 220 | latex_elements = { 221 | # The paper size ('letterpaper' or 'a4paper'). 222 | #'papersize': 'letterpaper', 223 | 224 | # The font size ('10pt', '11pt' or '12pt'). 225 | #'pointsize': '10pt', 226 | 227 | # Additional stuff for the LaTeX preamble. 228 | #'preamble': '', 229 | 230 | # Latex figure (float) alignment 231 | #'figure_align': 'htbp', 232 | } 233 | 234 | # Grouping the document tree into LaTeX files. List of tuples 235 | # (source start file, target name, title, 236 | # author, documentclass [howto, manual, or own class]). 237 | latex_documents = [ 238 | (master_doc, 'behave-django.tex', 'behave-django Documentation', 239 | 'Mitchel Cabuloy', 'manual'), 240 | ] 241 | 242 | # The name of an image file (relative to this directory) to place at the top of 243 | # the title page. 244 | #latex_logo = None 245 | 246 | # For "manual" documents, if this is true, then toplevel headings are parts, 247 | # not chapters. 248 | #latex_use_parts = False 249 | 250 | # If true, show page references after internal links. 251 | #latex_show_pagerefs = False 252 | 253 | # If true, show URL addresses after external links. 254 | #latex_show_urls = False 255 | 256 | # Documents to append as an appendix to all manuals. 257 | #latex_appendices = [] 258 | 259 | # If false, no module index is generated. 260 | #latex_domain_indices = True 261 | 262 | 263 | # -- Options for manual page output --------------------------------------- 264 | 265 | # One entry per manual page. List of tuples 266 | # (source start file, name, description, authors, manual section). 267 | man_pages = [ 268 | (master_doc, 'behave-django', 'behave-django Documentation', 269 | [author], 1) 270 | ] 271 | 272 | # If true, show URL addresses after external links. 273 | #man_show_urls = False 274 | 275 | 276 | # -- Options for Texinfo output ------------------------------------------- 277 | 278 | # Grouping the document tree into Texinfo files. List of tuples 279 | # (source start file, target name, title, author, 280 | # dir menu entry, description, category) 281 | texinfo_documents = [ 282 | (master_doc, 'behave-django', 'behave-django Documentation', 283 | author, 'behave-django', 'Behave BDD integration for Django.', 284 | 'Testing'), 285 | ] 286 | 287 | # Documents to append as an appendix to all manuals. 288 | #texinfo_appendices = [] 289 | 290 | # If false, no module index is generated. 291 | #texinfo_domain_indices = True 292 | 293 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 294 | #texinfo_show_urls = 'footnote' 295 | 296 | # If true, do not generate a @detailmenu in the "Top" node's menu. 297 | #texinfo_no_detailmenu = False 298 | -------------------------------------------------------------------------------- /docs/configuration.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | Command line options 5 | -------------------- 6 | 7 | You can use regular *behave* command line options with the ``behave`` 8 | management command. 9 | 10 | .. code-block:: console 11 | 12 | $ python manage.py behave --tags @wip 13 | 14 | Additional command line options provided by *behave-django*: 15 | 16 | ``--keepdb`` 17 | ************ 18 | 19 | Starting with Django 1.8, the ``--keepdb`` flag was added to ``manage.py test`` 20 | to facilitate faster testing by using the existing database instead of 21 | recreating it each time you run the test. This flag enables 22 | ``manage.py behave --keepdb`` to take advantage of that feature. 23 | |keepdb docs|_. 24 | 25 | ``--runner`` 26 | ************ 27 | 28 | Full Python dotted path to the `Django test runner`_ module used for your 29 | BDD test suite. Default: |BehaviorDrivenTestRunner|_ 30 | 31 | You can use this option if you require custom behavior that deviates from 32 | `Django's default test runner`_ and is hard or impossible to configure. 33 | 34 | .. note:: 35 | 36 | Not to be confused with ``--behave-runner`` that handles the internal 37 | `test runner inside behave`_. You would use that to override *behave*'s 38 | feature/step file discovery and similar behavior. Read more about it 39 | in the |behave docs (runner opt)|_. 40 | 41 | ``--simple`` 42 | ************ 43 | 44 | Use Django's simple ``TestCase`` which rolls back the database transaction 45 | after each scenario instead of flushing the entire database. Tests run much 46 | quicker, however HTTP server is not started and therefore web browser 47 | automation is not available. 48 | 49 | ``--use-existing-database`` 50 | *************************** 51 | 52 | Don't create a test database, and use the database of your default runserver 53 | instead. USE AT YOUR OWN RISK! Only use this option for testing against a 54 | *copy* of your production database or other valuable data. Your tests may 55 | destroy your data irrecoverably. 56 | 57 | Behave configuration file 58 | ------------------------- 59 | 60 | You can use *behave*'s configuration file. Just add a ``[tool.behave]`` 61 | section to your ``pyproject.toml`` file or create a ``behave.ini``, 62 | ``.behaverc``, ``setup.cfg`` or ``tox.ini`` file in your project's root 63 | directory and behave will pick it up. You can read more about it in the 64 | |behave docs (config files)|_. 65 | 66 | For example, if you want to have your features directory somewhere else. 67 | In your ``.behaverc`` file, you can put 68 | 69 | .. code-block:: ini 70 | 71 | [behave] 72 | paths=my_project/apps/accounts/features/ 73 | my_project/apps/polls/features/ 74 | 75 | *Behave* should now look for your features in those folders. 76 | 77 | 78 | .. |keepdb docs| replace:: More information about ``--keepdb`` 79 | .. _keepdb docs: https://docs.djangoproject.com/en/stable/topics/testing/overview/#the-test-database 80 | .. _Django test runner: https://docs.djangoproject.com/en/stable/ref/settings/#test-runner 81 | .. _Django's default test runner: https://github.com/django/django/blob/stable/4.0.x/django/test/runner.py#L555-L582 82 | .. |BehaviorDrivenTestRunner| replace:: ``behave_django.runner:BehaviorDrivenTestRunner`` 83 | .. _BehaviorDrivenTestRunner: https://github.com/behave/behave-django/blob/1.4.0/behave_django/runner.py#L9-L13 84 | .. _test runner inside behave: https://github.com/behave/behave/blob/v1.2.7.dev2/behave/runner.py#L728-L736 85 | .. |behave docs (runner opt)| replace:: behave docs 86 | .. _behave docs (runner opt): https://behave.readthedocs.io/en/latest/behave.html#cmdoption-r 87 | .. |behave docs (config files)| replace:: behave docs 88 | .. _behave docs (config files): https://behave.readthedocs.io/en/latest/behave.html#configuration-files 89 | -------------------------------------------------------------------------------- /docs/contribute.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Want to help out with *behave-django*? Cool! Here's a quick guide to 5 | do just that. 6 | 7 | Preparation 8 | ----------- 9 | 10 | Fork the *behave-django* repository, then clone it: 11 | 12 | .. code:: console 13 | 14 | $ git clone git@github.com:your-username/behave-django.git 15 | 16 | Ensure Tox is installed. We use it to run linters, run the tests and 17 | generate the docs: 18 | 19 | .. code:: console 20 | 21 | $ pip install tox 22 | 23 | Essentials 24 | ---------- 25 | 26 | Make sure the tests pass. The ``@failing`` tag is used for tests that 27 | are supposed to fail. The ``@requires-live-http`` tag is used for 28 | tests that can't run with ``--simple`` flag. See the ``[testenv]`` 29 | section in ``tox.ini`` for details. 30 | 31 | .. code:: console 32 | 33 | $ tox list # show all Tox environments 34 | $ tox -e py312-django51 # run just a single environment 35 | $ tox # run all linting and tests 36 | 37 | Getting your hands dirty 38 | ------------------------ 39 | 40 | Start your topic branch: 41 | 42 | .. code:: console 43 | 44 | $ git checkout -b your-topic-branch 45 | 46 | Make your changes. Add tests for your change. Make the tests pass: 47 | 48 | .. code:: console 49 | 50 | $ tox -e behave-latest 51 | 52 | Finally, make sure your tests pass on all the configurations 53 | *behave-django* supports. This is defined in ``tox.ini``. The Python 54 | versions you test against need to be available in your PATH. 55 | 56 | .. code:: console 57 | 58 | $ tox 59 | 60 | You can choose not to run all tox tests and let the CI server take care 61 | about that. In this case make sure your tests pass when you push your 62 | changes and open the PR. 63 | 64 | Documentation changes 65 | --------------------- 66 | 67 | If you make changes to the documentation generate it locally and take a 68 | look at the results. Sphinx builds the output in ``docs/_build/``. 69 | 70 | .. code:: console 71 | 72 | $ tox -e docs 73 | $ python -m webbrowser -t docs/_build/html/index.html 74 | 75 | Finally 76 | ------- 77 | 78 | Push to your fork and `submit a pull request`_. 79 | 80 | To clean up behind you, you can run: 81 | 82 | .. code:: console 83 | 84 | $ tox -e clean 85 | 86 | Other things to note 87 | -------------------- 88 | 89 | - Write tests. 90 | - Your tests don't have to be *behave* tests. ``:-)`` 91 | - We use Ruff to govern our code style (``ruff check`` and ``ruff format`` 92 | will run over the code on the CI server). 93 | 94 | Thank you! 95 | 96 | 97 | .. _submit a pull request: https://github.com/behave/behave-django/compare/ 98 | -------------------------------------------------------------------------------- /docs/fixtures.rst: -------------------------------------------------------------------------------- 1 | Fixture Loading 2 | =============== 3 | 4 | behave-django can load your fixtures for you per feature/scenario. There are 5 | two approaches to this: 6 | 7 | * loading the fixtures in ``environment.py``, or 8 | * using a decorator on your step function 9 | 10 | 11 | Fixtures in environment.py 12 | -------------------------- 13 | 14 | In ``environment.py`` we can load our context with the fixtures array. 15 | 16 | .. code-block:: python 17 | 18 | def before_all(context): 19 | context.fixtures = ['user-data.json'] 20 | 21 | This fixture would then be loaded before every scenario. 22 | 23 | If you wanted different fixtures for different scenarios: 24 | 25 | .. code-block:: python 26 | 27 | def before_scenario(context, scenario): 28 | if scenario.name == 'User login with valid credentials': 29 | context.fixtures = ['user-data.json'] 30 | elif scenario.name == 'Check out cart': 31 | context.fixtures = ['user-data.json', 'store.json', 'cart.json'] 32 | else: 33 | # Resetting fixtures, otherwise previously set fixtures carry 34 | # over to subsequent scenarios. 35 | context.fixtures = [] 36 | 37 | You could also have fixtures per Feature too 38 | 39 | .. code-block:: python 40 | 41 | def before_feature(context, feature): 42 | if feature.name == 'Login': 43 | context.fixtures = ['user-data.json'] 44 | # This works because behave will use the same context for 45 | # everything below Feature. (Scenarios, Outlines, Backgrounds) 46 | else: 47 | # Resetting fixtures, otherwise previously set fixtures carry 48 | # over to subsequent features. 49 | context.fixtures = [] 50 | 51 | Of course, since ``context.fixtures`` is really just a list, you can mutate it 52 | however you want, it will only be processed upon leaving the 53 | ``before_scenario()`` function of your ``environment.py`` file. Just keep in 54 | mind that it does not reset between features or scenarios, unless explicitly 55 | done so (as shown in the examples above). 56 | 57 | .. note:: 58 | 59 | If you provide initial data via Python code `using the ORM`_ you need 60 | to place these calls in ``before_scenario()`` even if the data is 61 | meant to be used on the whole feature. This is because Django's 62 | ``LiveServerTestCase`` resets the test database after each scenario. 63 | 64 | Fixtures using a decorator 65 | -------------------------- 66 | 67 | You can define `Django fixtures`_ using a function decorator. The decorator will 68 | load the fixtures in the ``before_scenario``, as documented above. It is merely 69 | a convenient way to keep fixtures close to your steps. 70 | 71 | .. code-block:: python 72 | 73 | from behave_django.decorators import fixtures 74 | 75 | @fixtures('users.json') 76 | @when('someone does something') 77 | def step_impl(context): 78 | pass 79 | 80 | .. note:: 81 | 82 | Fixtures included with the decorator will apply to all other steps that 83 | they share a scenario with. This is because *behave-django* needs to 84 | provide them to the test environment before processing the particular 85 | scenario. 86 | 87 | Support for multiple databases 88 | ------------------------------ 89 | 90 | By default, Django only loads fixtures into the ``default`` database. 91 | 92 | Use ``before_scenario`` to load the fixtures in all of the databases you have 93 | configured if your tests rely on the fixtures being loaded in all of them. 94 | 95 | .. code-block:: python 96 | 97 | def before_scenario(context, scenario): 98 | context.databases = '__all__' 99 | 100 | You can read more about it in the `Multiple database docs`_. 101 | 102 | 103 | .. _using the ORM: https://docs.djangoproject.com/en/stable/topics/testing/tools/#fixture-loading 104 | .. _Django fixtures: https://docs.djangoproject.com/en/stable/howto/initial-data/#providing-data-with-fixtures 105 | .. _Multiple database docs: https://docs.djangoproject.com/en/stable/topics/testing/tools/#multi-database-support 106 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | :end-before: .. support-marker 3 | 4 | Contents 5 | -------- 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | installation 11 | usage 12 | webbrowser 13 | testclient 14 | testcoverage 15 | isolation 16 | fixtures 17 | pageobject 18 | setup 19 | configuration 20 | contribute 21 | 22 | .. include:: ../README.rst 23 | :start-after: .. support-marker 24 | :end-before: .. docs-marker 25 | 26 | .. include:: ../README.rst 27 | :start-after: .. references-marker 28 | 29 | Indices and tables 30 | ------------------ 31 | 32 | * :ref:`search` 33 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Install using pip 5 | 6 | .. code-block:: bash 7 | 8 | $ pip install behave-django 9 | 10 | Add ``behave_django`` to your ``INSTALLED_APPS`` 11 | 12 | .. code-block:: python 13 | 14 | INSTALLED_APPS += ['behave_django'] 15 | -------------------------------------------------------------------------------- /docs/isolation.rst: -------------------------------------------------------------------------------- 1 | Test Isolation 2 | ============== 3 | 4 | Database transactions per scenario 5 | ---------------------------------- 6 | 7 | Each scenario is run inside a database transaction, just like Django does 8 | it with the related TestCase implementation. So you can do something like: 9 | 10 | .. code-block:: python 11 | 12 | @given('user "{username}" exists') 13 | def create_user(context, username): 14 | # This won't be here for the next scenario 15 | User.objects.create_user(username=username, password='correcthorsebatterystaple') 16 | 17 | And you don’t have to clean the database yourself. 18 | 19 | Troubleshooting 20 | --------------- 21 | 22 | .. note:: 23 | 24 | Users have reported that test isolation and loading fixtures works 25 | differently with different test runners (e.g. when the ``--simple`` 26 | option is used). This is likely related to the Django TestCase class 27 | configured for the runner. 28 | 29 | Be sure you understand which Django TestCase class is used in your 30 | case, and how it's implemented in Django. See the `runner module`_ 31 | and Django's documentation on `Writing and running tests`_ for details. 32 | 33 | 34 | .. _runner module: https://github.com/behave/behave-django/blob/main/behave_django/runner.py 35 | .. _Writing and running tests: https://docs.djangoproject.com/en/stable/topics/testing/overview/ 36 | -------------------------------------------------------------------------------- /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. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\behave-django.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\behave-django.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/pageobject.rst: -------------------------------------------------------------------------------- 1 | Using Page Objects 2 | ================== 3 | 4 | .. warning:: 5 | 6 | This is an *alpha* feature. It may be removed or its underlying 7 | implementation changed without a deprecation process! Please follow the 8 | discussions in `related issues`_ or on `Gitter`_ if you plan to use it. 9 | 10 | With *behave-django* you can use the `Page Object pattern`_ and work on a 11 | natural abstraction layer for the content or behavior your web application 12 | produces. This is a popular approach to make your tests more stable and 13 | your code easier to read. 14 | 15 | .. code-block:: python 16 | 17 | # FILE: steps/pageobjects/pages.py 18 | from behave_django.pageobject import PageObject, Link 19 | 20 | class Welcome(PageObject): 21 | page = 'home' # view name, model or URL path 22 | elements = { 23 | 'about': Link(css='footer a[role=about]'), 24 | } 25 | 26 | class About(PageObject): 27 | page = 'about' 28 | 29 | .. code-block:: python 30 | 31 | # FILE: steps/welcome.py 32 | from pageobjects.pages import About, Welcome 33 | 34 | @given('I am on the Welcome page') 35 | def step_impl(context): 36 | context.welcome_page = Welcome(context) 37 | assert context.welcome_page.response.status_code == 200 38 | 39 | @when('I click on the "About" link') 40 | def step_impl(context): 41 | context.target_page = \ 42 | context.welcome_page.get_link('about').click() 43 | assert context.target_page.response.status_code == 200 44 | 45 | @then('The About page is loaded') 46 | def step_impl(context): 47 | assert About(context) == context.target_page 48 | 49 | A ``PageObject`` instance automatically loads and parses the page you 50 | specify by its ``page`` attribute. You then have access to the following 51 | attributes: 52 | 53 | ``request`` 54 | The HTTP request used by the Django test client to fetch the document. 55 | This is merely a convenient alias for ``response.request``. 56 | 57 | ``response`` 58 | The Django test client's HTTP response object. Use this to verify the 59 | actual HTTP response related to the retrieved document. 60 | 61 | ``document`` 62 | The parsed content of the response. This is, technically speaking, a 63 | `Beautiful Soup`_ object. You *can* use this to access and verify any 64 | part of the document content, though it's recommended that you only 65 | access the elements you specify with the ``elements`` attribute, using 66 | the appropriate helper methods. 67 | 68 | Helpers to access your page object's elements: 69 | 70 | ``get_link(name) -> Link`` 71 | A subdocument representing a HTML anchor link, retrieved from 72 | ``document`` using the CSS selector specified in ``elements[name]``. 73 | The returned ``Link`` object provides a ``click()`` method to trigger 74 | loading the link's URL, which again returns a ``PageObject``. 75 | 76 | .. note:: 77 | 78 | *behave-django*'s `PageObject`_ is a headless page object, meaning 79 | that it doesn't use Selenium to drive the user interface. 80 | 81 | If you need a page object that encapsulates Selenium you may take a look 82 | at alternative libraries, such as `page-object`_, `page-objects`_ or 83 | `selenium-page-factory`_. But keep in mind that this is a different 84 | kind of testing: 85 | 86 | - You'll be testing the Web browser, hence for Web browser compatibility. 87 | - Preparing an environment for test automation will be laborious. 88 | - Mocking objects in your tests will be difficult, if not impossible. 89 | - Your tests will be *significantly* slower and potentially brittle. 90 | 91 | Think twice if that is really what you need. In most cases you'll be 92 | better off testing your Django application code only. That's when you 93 | would use `Django's test client`_ and our headless page object. 94 | 95 | 96 | .. _related issues: https://github.com/behave/behave-django/issues 97 | .. _Gitter: https://gitter.im/behave/behave-django 98 | .. _Page Object pattern: https://www.martinfowler.com/bliki/PageObject.html 99 | .. _Beautiful Soup: https://www.crummy.com/software/BeautifulSoup/bs4/doc/ 100 | .. _PageObject: 101 | https://github.com/behave/behave-django/blob/main/behave_django/pageobject.py 102 | .. _page-object: https://pypi.org/project/page-object/ 103 | .. _page-objects: https://pypi.org/project/page-objects/ 104 | .. _selenium-page-factory: https://pypi.org/project/selenium-page-factory/ 105 | .. _Django's test client: 106 | https://docs.djangoproject.com/en/stable/topics/testing/tools/#the-test-client 107 | -------------------------------------------------------------------------------- /docs/setup.rst: -------------------------------------------------------------------------------- 1 | Environment Setup 2 | ================= 3 | 4 | django_ready hook 5 | ----------------- 6 | 7 | You can add a ``django_ready`` function in your ``environment.py`` file in case 8 | you want to make per-scenario changes inside a transaction. 9 | 10 | For example, if you have `factories`_ you want to instantiate on a per-scenario 11 | basis, you can initialize them in ``environment.py`` like this: 12 | 13 | .. code-block:: python 14 | 15 | from myapp.main.tests.factories import UserFactory, RandomContentFactory 16 | 17 | 18 | def django_ready(context): 19 | # This function is run inside the transaction 20 | UserFactory(username='user1') 21 | UserFactory(username='user2') 22 | RandomContentFactory() 23 | 24 | Or maybe you want to modify the ``test`` instance: 25 | 26 | .. code-block:: python 27 | 28 | from rest_framework.test import APIClient 29 | 30 | 31 | def django_ready(context): 32 | context.test.client = APIClient() 33 | 34 | 35 | .. _factories: https://factoryboy.readthedocs.io/en/latest/ 36 | -------------------------------------------------------------------------------- /docs/testclient.rst: -------------------------------------------------------------------------------- 1 | Django’s Test Client 2 | ==================== 3 | 4 | Internally, Django's TestCase is used to maintain the test environment. 5 | You can access the TestCase instance via ``context.test``. 6 | 7 | .. code-block:: python 8 | 9 | # Using Django's testing client 10 | @when('I visit "{url}"') 11 | def visit(context, url): 12 | # save response in context for next step 13 | context.response = context.test.client.get(url) 14 | 15 | Simple testing 16 | -------------- 17 | 18 | If you only use Django's test client then *behave* tests can run much 19 | quicker with the ``--simple`` command line option. In this case transaction 20 | rollback is used for test automation instead of flushing the database after 21 | each scenario, just like in Django's standard ``TestCase``. 22 | 23 | No HTTP server is started during the simple testing, so you can't use web 24 | browser automation. Accessing ``context.base_url`` or calling 25 | ``context.get_url()`` will raise an exception. 26 | 27 | unittest + Django assert library 28 | -------------------------------- 29 | 30 | Additionally, you can utilize unittest and Django’s assert library. 31 | 32 | .. code-block:: python 33 | 34 | @then('I should see "{text}"') 35 | def visit(context, text): 36 | # compare with response from ``when`` step 37 | response = context.response 38 | context.test.assertContains(response, text) 39 | -------------------------------------------------------------------------------- /docs/testcoverage.rst: -------------------------------------------------------------------------------- 1 | Test Coverage 2 | ============= 3 | 4 | You can integrate `Coverage.py`_ with behave-django to find out the test coverage 5 | of your code. 6 | 7 | There are two ways to do this. The simple (and obvious one) is via invocation 8 | through the ``coverage`` CLI. Alternatively, you can integrate Coverage in the 9 | ``environment.py`` file of your BDD test setup as shown below. 10 | 11 | Prerequisites 12 | ------------- 13 | 14 | Obviously, you need to install Coverage to measure code coverage, e.g. 15 | 16 | .. code-block:: bash 17 | 18 | $ pip install coverage[toml] 19 | 20 | Invoke via ``coverage`` 21 | ----------------------- 22 | 23 | Instead of using ``python manage.py``, you simply use 24 | ``coverage run manage.py`` to invoke your BDD tests, e.g. 25 | 26 | .. code-block:: bash 27 | 28 | $ coverage run manage.py behave 29 | 30 | Afterwards, you can display a coverage report in your terminal to understand 31 | which lines your tests are missing, e.g. 32 | 33 | .. code-block:: bash 34 | 35 | $ coverage report --show-missing 36 | 37 | .. tip:: 38 | 39 | A Django project setup with coverage configured in ``pyproject.toml`` and 40 | executed by Tox is show-cased in the `Painless CI/CD Copier template for 41 | Django`_. 42 | 43 | Integrate via ``environment.py`` 44 | -------------------------------- 45 | 46 | In ``environment.py``, add the code snippet below in the ``before_all`` function 47 | to start measuring test coverage: 48 | 49 | .. code-block:: python 50 | 51 | import coverage 52 | 53 | def before_all(context): 54 | cov = coverage.Coverage() 55 | cov.start() 56 | context.cov = cov 57 | 58 | Then write below code to end up measuring test coverage. 59 | You can save the coverage result on html format. 60 | 61 | .. code-block:: python 62 | 63 | def after_all(context): 64 | cov = context.cov 65 | cov.stop() 66 | cov.save() 67 | cov.html_report(directory="./cov") 68 | 69 | You can check the test coverage on the web with the following command. 70 | 71 | .. code-block:: bash 72 | 73 | $ python -m http.server --directory ./cov 74 | 75 | .. warning:: 76 | 77 | Internally, the time ``before_all`` is executed seems to be later than the 78 | time when django loads the modules set in each app. 79 | 80 | So sometimes it is necessary to reload django app's modules for accurate 81 | test coverage measurement. 82 | 83 | Like this: 84 | 85 | .. code-block:: python 86 | 87 | import inspect 88 | import importlib 89 | 90 | def reload_modules(): 91 | import your_app1 92 | import your_app2 93 | 94 | for app in [your_app1, your_app2]: 95 | members = inspect.getmembers(app) 96 | modules = map( 97 | lambda keyval: keyval[1], 98 | filter(lambda keyval: inspect.ismodule(keyval[1]), members), 99 | ) 100 | for module in modules: 101 | try: 102 | importlib.reload(module) 103 | except: 104 | continue 105 | 106 | .. code-block:: python 107 | 108 | def before_all(context): 109 | # cov 110 | cov = coverage.Coverage() 111 | cov.start() 112 | context.cov = cov 113 | 114 | # modules 115 | reload_modules() 116 | 117 | .. _Coverage.py: https://coverage.readthedocs.io/ 118 | .. _Painless CI/CD Copier template for Django: 119 | https://gitlab.com/painless-software/cicd/app/django 120 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | Create the features directory in your project’s root directory. (Next 5 | to ``manage.py``) 6 | 7 | :: 8 | 9 | features/ 10 | steps/ 11 | your_steps.py 12 | environment.py 13 | your-feature.feature 14 | 15 | Run ``python manage.py behave``:: 16 | 17 | $ python manage.py behave 18 | Creating test database for alias 'default'... 19 | Feature: Running tests # features/running-tests.feature:1 20 | In order to prove that behave-django works 21 | As the Maintainer 22 | I want to test running behave against this features directory 23 | Scenario: The Test # features/running-tests.feature:6 24 | Given this step exists # features/steps/running_tests.py:4 0.000s 25 | When I run "python manage.py behave" # features/steps/running_tests.py:9 0.000s 26 | Then I should see the behave tests run # features/steps/running_tests.py:14 0.000s 27 | 28 | 1 features passed, 0 failed, 0 skipped 29 | 1 scenarios passed, 0 failed, 0 skipped 30 | 3 steps passed, 0 failed, 0 skipped, 0 undefined 31 | Took.010s 32 | Destroying test database for alias 'default'... 33 | 34 | See the `environment.py`_, `running-tests.feature`_ and `steps/running_tests.py`_ 35 | files in the ``features`` folder of the project repository for implementation 36 | details of this very example. See the folder also for `more useful examples`_. 37 | 38 | Alternative folder structure 39 | ---------------------------- 40 | 41 | For larger projects, specifically those that also have other types of tests, 42 | it's recommended to use a more sophisticated folder structure, e.g. 43 | 44 | :: 45 | 46 | tests/ 47 | acceptance/ 48 | features/ 49 | example.feature 50 | steps/ 51 | given.py 52 | then.py 53 | when.py 54 | environment.py 55 | 56 | Your *behave* configuration should then look something like this: 57 | 58 | .. code-block:: ini 59 | 60 | [behave] 61 | paths = tests/acceptance 62 | junit_directory = tests/reports 63 | junit = yes 64 | 65 | This way you'll be able to cleanly accommodate unit tests, integration 66 | tests, field tests, penetration tests, etc. and test reports in a single 67 | tests folder. 68 | 69 | .. note:: 70 | 71 | The `behave docs`_ provide additional helpful information on using *behave* 72 | with Django and various test automation libraries. 73 | 74 | .. _environment.py: https://github.com/behave/behave-django/blob/main/tests/acceptance/environment.py 75 | .. _running-tests.feature: https://github.com/behave/behave-django/blob/main/tests/acceptance/features/running-tests.feature 76 | .. _more useful examples: https://github.com/behave/behave-django/tree/main/tests/acceptance/features 77 | .. _steps/running_tests.py: https://github.com/behave/behave-django/blob/main/tests/acceptance/steps/running_tests.py 78 | .. _behave docs: https://behave.readthedocs.io/en/latest/practical_tips.html 79 | -------------------------------------------------------------------------------- /docs/webbrowser.rst: -------------------------------------------------------------------------------- 1 | Web Browser Automation 2 | ====================== 3 | 4 | You can access the test HTTP server from your preferred web automation 5 | library via ``context.base_url``. Alternatively, you can use 6 | ``context.get_url()``, which is a helper function for absolute paths and 7 | reversing URLs in your Django project. It takes an absolute path, a view 8 | name, or a model as an argument, similar to `django.shortcuts.redirect`_. 9 | 10 | Examples: 11 | 12 | .. code-block:: python 13 | 14 | # Using Splinter 15 | @when('I visit "{page}"') 16 | def visit(context, page): 17 | context.browser.visit(context.get_url(page)) 18 | 19 | .. code-block:: python 20 | 21 | # Get context.base_url 22 | context.get_url() 23 | # Get context.base_url + '/absolute/url/here' 24 | context.get_url('/absolute/url/here') 25 | # Get context.base_url + reverse('view-name') 26 | context.get_url('view-name') 27 | # Get context.base_url + reverse('view-name', 'with args', and='kwargs') 28 | context.get_url('view-name', 'with args', and='kwargs') 29 | # Get context.base_url + model_instance.get_absolute_url() 30 | context.get_url(model_instance) 31 | 32 | 33 | .. _django.shortcuts.redirect: https://docs.djangoproject.com/en/stable/topics/http/shortcuts/#redirect 34 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = ["setuptools>=64", "setuptools_scm>=8"] 4 | 5 | [project] 6 | name = "behave-django" 7 | dynamic = ["version"] 8 | description = "Behave BDD integration for Django" 9 | readme = "README.rst" 10 | license = {file = "LICENSE"} 11 | authors = [ 12 | {name = "Mitchel Cabuloy", email = "mixxorz@gmail.com"}, 13 | {name = "Peter Bittner", email = "django@bittner.it"}, 14 | ] 15 | maintainers = [ 16 | {name = "Peter Bittner", email = "django@bittner.it"}, 17 | {name = "Javier Buzzi", email = "buzzi.javier@gmail.com"}, 18 | ] 19 | classifiers = [ 20 | "Development Status :: 5 - Production/Stable", 21 | "Environment :: Console", 22 | "Environment :: Plugins", 23 | "Environment :: Web Environment", 24 | "Framework :: Django", 25 | "Framework :: Django :: 4.2", 26 | "Framework :: Django :: 5.0", 27 | "Framework :: Django :: 5.1", 28 | "Intended Audience :: Developers", 29 | "License :: OSI Approved :: MIT License", 30 | "Operating System :: OS Independent", 31 | "Programming Language :: Python", 32 | "Programming Language :: Python :: 3", 33 | "Programming Language :: Python :: 3.8", 34 | "Programming Language :: Python :: 3.9", 35 | "Programming Language :: Python :: 3.10", 36 | "Programming Language :: Python :: 3.11", 37 | "Programming Language :: Python :: 3.12", 38 | "Topic :: Internet :: WWW/HTTP", 39 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 40 | "Topic :: Software Development :: Testing", 41 | ] 42 | keywords = [ 43 | "bdd", 44 | "behave", 45 | "django", 46 | "testing", 47 | ] 48 | requires-python = ">=3.8" 49 | dependencies = [ 50 | "behave[toml]>=1.2.7.dev6", 51 | "django>=4.2", 52 | "beautifulsoup4", 53 | ] 54 | 55 | [project.urls] 56 | Source = "https://github.com/behave/behave-django" 57 | Documentation = "https://behave-django.readthedocs.io/" 58 | 59 | [tool.behave] 60 | junit = true 61 | junit_directory = "tests" 62 | paths = [ 63 | "tests/acceptance", 64 | "tests/test_app", 65 | ] 66 | show_skipped = false 67 | 68 | [tool.coverage.run] 69 | source = ["behave_django"] 70 | 71 | [tool.coverage.report] 72 | show_missing = true 73 | 74 | [tool.pytest.ini_options] 75 | addopts = "--color=yes --junitxml=tests/unittests-report.xml --verbose" 76 | testpaths = [ 77 | "tests/unit", 78 | ] 79 | 80 | [tool.ruff] 81 | extend-exclude = ["docs/conf.py"] 82 | 83 | [tool.ruff.format] 84 | quote-style = "single" 85 | exclude = ["tests/test_app/migrations/*.py"] 86 | 87 | [tool.ruff.lint] 88 | extend-select = ["ALL"] 89 | extend-ignore = ["ANN", "COM812", "D", "FIX", "Q000", "RUF012", "TD"] 90 | 91 | [tool.ruff.lint.flake8-quotes] 92 | inline-quotes = "single" 93 | 94 | [tool.ruff.lint.per-file-ignores] 95 | "behave_django/decorators.py" = ["B010"] 96 | "behave_django/environment.py" = ["SLF001"] 97 | "behave_django/management/commands/behave.py" = ["ARG002", "N811", "PLW2901"] 98 | "behave_django/pageobject.py" = ["B023"] 99 | "behave_django/testcase.py" = ["FBT002", "N802"] 100 | "tests/acceptance/steps/live_test_server.py" = ["S310"] 101 | "tests/test_app/*.py" = ["ERA001"] 102 | "tests/test_app/models.py" = ["DJ008"] 103 | "tests/test_project/settings.py" = ["S105"] 104 | "tests/unit/util.py" = ["S603"] 105 | "tests/**/*.py" = ["ARG001", "ARG002", "INP001", "PT009", "S101"] 106 | 107 | [tool.setuptools.packages.find] 108 | where = ["."] 109 | 110 | [tool.setuptools_scm] 111 | local_scheme = "no-local-version" 112 | -------------------------------------------------------------------------------- /tests/acceptance/environment.py: -------------------------------------------------------------------------------- 1 | """ 2 | behave environment module for testing behave-django 3 | """ 4 | 5 | 6 | def before_feature(context, feature): 7 | if feature.name == 'Fixture loading': 8 | context.fixtures = ['behave-fixtures.json'] 9 | 10 | elif feature.name == 'Fixture loading with decorator': 11 | # Including empty fixture to test that #92 is fixed 12 | context.fixtures = ['empty-fixture.json'] 13 | 14 | 15 | def before_scenario(context, scenario): 16 | if scenario.name == 'Load fixtures for this scenario and feature': 17 | context.fixtures.append('behave-second-fixture.json') 18 | 19 | if scenario.name == 'Load fixtures then reset sequences': 20 | context.fixtures.append('behave-second-fixture.json') 21 | context.reset_sequences = True 22 | 23 | if scenario.name == 'Load fixtures with databases option': 24 | context.databases = '__all__' 25 | 26 | 27 | def django_ready(context): 28 | context.django = True 29 | -------------------------------------------------------------------------------- /tests/acceptance/features/context-urlhelper.feature: -------------------------------------------------------------------------------- 1 | @requires-live-http 2 | Feature: URL helpers are available in behave's context 3 | In order to ensure that url helpers are available as documented 4 | As the Maintainer 5 | I want to test all suggested uses 6 | 7 | Scenario: get_url() is an alias for the base_url property 8 | When I call get_url() without arguments 9 | Then it returns the value of base_url 10 | 11 | Scenario: The first argument in get_url() is appended to base_url if it is a path 12 | When I call get_url("/path/to/page/") with an absolute path 13 | Then the result is the base_url with "/path/to/page/" appended 14 | 15 | Scenario: The reversed view path is appended to base_url if the first argument in get_url() is a view name 16 | When I call get_url("admin:password_change") with a view name 17 | Then this returns the same result as get_url(reverse("admin:password_change")) 18 | And the result is the base_url with reverse("admin:password_change") appended 19 | 20 | Scenario: The model's absolute_url is appended to base_url if the first argument in get_url() is a model 21 | When I call get_url(model) with a model instance 22 | Then this returns the same result as get_url(model.get_absolute_url()) 23 | And the result is the base_url with model.get_absolute_url() appended 24 | -------------------------------------------------------------------------------- /tests/acceptance/features/database-transactions.feature: -------------------------------------------------------------------------------- 1 | Feature: Database Transactions 2 | In order to test if transactions are done per scenario 3 | As the Maintainer 4 | I want to save two database items 5 | 6 | Scenario: Save the first item 7 | When I save the object 8 | Then I should only have one object 9 | 10 | Scenario: Save the second item 11 | When I save the object 12 | Then I should only have one object 13 | -------------------------------------------------------------------------------- /tests/acceptance/features/django-test-client.feature: -------------------------------------------------------------------------------- 1 | Feature: Django's test client 2 | In order to ensure that the django's test client works in behave steps 3 | As the Maintainer 4 | I want to test if the client works 5 | 6 | Scenario: Django's test client 7 | When I use django's test client to visit "/" 8 | Then it should return a successful response 9 | -------------------------------------------------------------------------------- /tests/acceptance/features/django-unittest-asserts.feature: -------------------------------------------------------------------------------- 1 | Feature: Django + unittest asserts 2 | In order to have cleaner assert calls 3 | As the Maintainer 4 | I want to use Django + unittest's built in assert library 5 | 6 | Scenario: Testing the unittest assert library 7 | When I use the unittest assert library 8 | Then it should work properly 9 | 10 | Scenario: Testing the django assert library 11 | When I use the django assert library 12 | Then it should work properly 13 | -------------------------------------------------------------------------------- /tests/acceptance/features/failing-feature.feature: -------------------------------------------------------------------------------- 1 | @failing 2 | Feature: Test failing feature 3 | In order for the system to know that there is a failing test 4 | As the Maintainer 5 | I want behave-django to return a non-zero return code for failing tests 6 | 7 | Scenario: Failing test 8 | Given this step exists 9 | Then this step should fail 10 | -------------------------------------------------------------------------------- /tests/acceptance/features/fixture-decorator.feature: -------------------------------------------------------------------------------- 1 | @requires-live-http 2 | Feature: Fixture loading with decorator 3 | In order to have sample data during my behave tests 4 | As a developer 5 | I want to load fixtures for specific steps with a decorator 6 | 7 | Scenario: Load fixtures with the decorator 8 | Given a step with a fixture decorator 9 | Then the fixture should be loaded 10 | 11 | Scenario: A Subsequent scenario should only load its fixtures 12 | Given a step with a second fixture decorator 13 | Then I should only have one object 14 | 15 | Scenario: Load multiple fixtures and callables 16 | Given a step with multiple fixtures 17 | Then the fixture for the second scenario should be loaded 18 | -------------------------------------------------------------------------------- /tests/acceptance/features/fixture-loading.feature: -------------------------------------------------------------------------------- 1 | @requires-live-http 2 | Feature: Fixture loading 3 | In order to have sample data during my behave tests 4 | As the Maintainer 5 | I want to load fixtures 6 | 7 | Scenario: Load fixtures 8 | Then the fixture should be loaded 9 | 10 | Scenario: Load fixtures for this scenario and feature 11 | Then the fixture for the second scenario should be loaded 12 | 13 | @failing 14 | Scenario: Load fixtures then reset sequences 15 | Then the sequences should be reset 16 | 17 | Scenario: Load fixtures with databases option 18 | Then databases should be set to all database in the Django settings 19 | -------------------------------------------------------------------------------- /tests/acceptance/features/live-test-server.feature: -------------------------------------------------------------------------------- 1 | @requires-live-http 2 | Feature: Live server 3 | In order to prove that the live server works 4 | As the Maintainer 5 | I want to send an HTTP request 6 | 7 | Scenario: HTTP GET 8 | When I visit "/" 9 | Then I should see "Behave Django works" 10 | -------------------------------------------------------------------------------- /tests/acceptance/features/running-tests.feature: -------------------------------------------------------------------------------- 1 | Feature: Running tests 2 | In order to prove that behave-django works 3 | As the Maintainer 4 | I want to test running behave against this features directory 5 | 6 | Scenario: The Test 7 | Given this step exists 8 | When I run "python manage.py behave" 9 | Then I should see the behave tests run 10 | 11 | Scenario: Test django_ready 12 | When I run "python manage.py behave" 13 | Then django_ready should be called 14 | -------------------------------------------------------------------------------- /tests/acceptance/features/using-pageobject.feature: -------------------------------------------------------------------------------- 1 | @requires-live-http 2 | Feature: Using page objects works as documented 3 | In order to ensure that page objects can be used as documented 4 | As the Maintainer 5 | I want to test all suggested uses 6 | 7 | Scenario: Welcome page object returns a valid (Beautiful Soup) document 8 | When I instantiate the Welcome page object 9 | Then it provides a valid Beautiful Soup document 10 | And get_link() returns the link subdocument 11 | When I call click() on the link 12 | Then it loads a new PageObject 13 | -------------------------------------------------------------------------------- /tests/acceptance/steps/context-urlhelper.py: -------------------------------------------------------------------------------- 1 | from behave import then, when 2 | from django.urls import reverse 3 | from test_app.models import BehaveTestModel 4 | 5 | 6 | @when('I call get_url() without arguments') 7 | def without_args(context): 8 | context.result = context.get_url() 9 | 10 | 11 | @when('I call get_url("{url_path}") with an absolute path') 12 | def path_arg(context, url_path): 13 | context.result = context.get_url(url_path) 14 | 15 | 16 | @when('I call get_url("{view_name}") with a view name') 17 | def view_arg(context, view_name): 18 | context.result = context.get_url(view_name) 19 | 20 | 21 | @when('I call get_url(model) with a model instance') 22 | def model_arg(context): 23 | context.model = BehaveTestModel(name='Foo', number=3) 24 | context.result = context.get_url(context.model) 25 | 26 | 27 | @then('it returns the value of base_url') 28 | def is_baseurl_value(context): 29 | context.test.assertEqual(context.result, context.base_url) 30 | 31 | 32 | @then('the result is the base_url with "{url_path}" appended') 33 | def baseurl_plus_path(context, url_path): 34 | context.test.assertEqual(context.result, context.base_url + url_path) 35 | 36 | 37 | @then('the result is the base_url with reverse("{view_name}") appended') 38 | def baseurl_plus_reverse(context, view_name): 39 | path = reverse(view_name) 40 | assert len(path) > 0, 'Non-empty path expected' 41 | context.test.assertEqual(context.result, context.base_url + path) 42 | 43 | 44 | @then('the result is the base_url with model.get_absolute_url() appended') 45 | def baseurl_plus_absolute_url(context): 46 | path = context.model.get_absolute_url() 47 | assert len(path) > 0, 'Non-empty path expected' 48 | context.test.assertEqual(context.result, context.base_url + path) 49 | 50 | 51 | @then('this returns the same result as get_url(reverse("{view_name}"))') 52 | def explicit_reverse(context, view_name): 53 | path = reverse(view_name) 54 | context.test.assertEqual(context.result, context.get_url(path)) 55 | 56 | 57 | @then('this returns the same result as get_url(model.get_absolute_url())') 58 | def get_model_url(context): 59 | path = context.model.get_absolute_url() 60 | context.test.assertEqual(context.result, context.get_url(path)) 61 | -------------------------------------------------------------------------------- /tests/acceptance/steps/database_transactions.py: -------------------------------------------------------------------------------- 1 | from behave import then, when 2 | from test_app.models import BehaveTestModel 3 | 4 | 5 | @when('I save the object') 6 | def save_object(context): 7 | BehaveTestModel.objects.create(name='Behave Works', number=123) 8 | 9 | 10 | @then('I should only have one object') 11 | def should_have_only_one_object(context): 12 | assert BehaveTestModel.objects.count() == 1 13 | -------------------------------------------------------------------------------- /tests/acceptance/steps/django-unittest-asserts.py: -------------------------------------------------------------------------------- 1 | from behave import then, when 2 | 3 | 4 | @when('I use the unittest assert library') 5 | def use_unittest_assert_library(context): 6 | # If one of them works, all of them work. ;) 7 | context.test.assertEqual(1, 1) 8 | 9 | 10 | @when('I use the django assert library') 11 | def use_django_assert_library(context): 12 | response = context.test.client.get('/') 13 | context.test.assertTemplateUsed(response, 'index.html') 14 | 15 | 16 | @then('it should work properly') 17 | def asserts_should_work(context): 18 | pass 19 | -------------------------------------------------------------------------------- /tests/acceptance/steps/django_test_client.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | from behave import then, when 4 | 5 | 6 | @when('I use django\'s test client to visit "{url}"') 7 | def use_django_client(context, url): 8 | context.response = context.test.client.get(url) 9 | 10 | 11 | @then('it should return a successful response') 12 | def it_should_be_successful(context): 13 | assert context.response.status_code == HTTPStatus.OK 14 | -------------------------------------------------------------------------------- /tests/acceptance/steps/failing_feature.py: -------------------------------------------------------------------------------- 1 | from behave import then 2 | 3 | 4 | @then('this step should fail') 5 | def failing_set(context): 6 | context.test.assertEqual(0, 1) 7 | -------------------------------------------------------------------------------- /tests/acceptance/steps/fixture-loading.py: -------------------------------------------------------------------------------- 1 | from behave import given, then 2 | from test_app.models import BehaveTestModel 3 | 4 | from behave_django.decorators import fixtures 5 | 6 | 7 | @fixtures('behave-fixtures.json') 8 | @given('a step with a fixture decorator') 9 | def check_decorator_fixtures(context): 10 | pass 11 | 12 | 13 | @fixtures('behave-second-fixture.json') 14 | @given('a step with a second fixture decorator') 15 | def check_decorator_second_fixture(context): 16 | pass 17 | 18 | 19 | @fixtures('behave-fixtures.json', 'behave-second-fixture.json') 20 | @given('a step with multiple fixtures') 21 | def check_decorator_multiple(context): 22 | pass 23 | 24 | 25 | @then('the fixture should be loaded') 26 | def check_fixtures(context): 27 | context.test.assertEqual(BehaveTestModel.objects.count(), 1) 28 | 29 | 30 | @then('the fixture for the second scenario should be loaded') 31 | def check_second_fixtures(context): 32 | context.test.assertEqual(BehaveTestModel.objects.count(), 2) 33 | 34 | 35 | @then('the sequences should be reset') 36 | def check_reset_sequences(context): 37 | context.test.assertEqual(BehaveTestModel.objects.first().pk, 1) 38 | context.test.assertEqual(BehaveTestModel.objects.last().pk, 2) 39 | 40 | 41 | @then('databases should be set to all database in the Django settings') 42 | def check_databases_attribute(context): 43 | context.test.assertEqual(context.test.databases, frozenset({'default'})) 44 | -------------------------------------------------------------------------------- /tests/acceptance/steps/live_test_server.py: -------------------------------------------------------------------------------- 1 | from urllib.request import urlopen 2 | 3 | from behave import then, when 4 | 5 | 6 | @when('I visit "{url}"') 7 | def visit(context, url): 8 | page = urlopen(context.base_url + url) 9 | context.response = str(page.read()) 10 | 11 | 12 | @then('I should see "{text}"') 13 | def i_should_see(context, text): 14 | assert text in context.response 15 | -------------------------------------------------------------------------------- /tests/acceptance/steps/pageobjects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behave/behave-django/9574e23fd1cbf7fcbd7c160fa2852134232be26e/tests/acceptance/steps/pageobjects/__init__.py -------------------------------------------------------------------------------- /tests/acceptance/steps/pageobjects/pages.py: -------------------------------------------------------------------------------- 1 | from behave_django.pageobject import Link, PageObject 2 | 3 | 4 | class Welcome(PageObject): 5 | page = 'home' # view name, model or URL path 6 | elements = { 7 | 'about': Link(css='footer a[role=about]'), 8 | } 9 | 10 | 11 | class About(PageObject): 12 | page = 'about' 13 | -------------------------------------------------------------------------------- /tests/acceptance/steps/running_tests.py: -------------------------------------------------------------------------------- 1 | from behave import given, then, when 2 | 3 | 4 | @given('this step exists') 5 | def step_exists(context): 6 | pass 7 | 8 | 9 | @when('I run "python manage.py behave"') 10 | def run_command(context): 11 | pass 12 | 13 | 14 | @then('I should see the behave tests run') 15 | def is_running(context): 16 | pass 17 | 18 | 19 | @then('django_ready should be called') 20 | def django_context(context): 21 | assert context.django 22 | -------------------------------------------------------------------------------- /tests/acceptance/steps/using_pageobjects.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | 3 | from behave import then, when 4 | from bs4 import BeautifulSoup 5 | from bs4.element import Tag 6 | from pageobjects.pages import About, Welcome 7 | 8 | 9 | @when('I instantiate the Welcome page object') 10 | def new_pageobject(context): 11 | context.page = Welcome(context) 12 | 13 | 14 | @then('it provides a valid Beautiful Soup document') 15 | def pageobject_works(context): 16 | assert context.page.response.status_code == HTTPStatus.OK 17 | assert context.page.request == context.page.response.request 18 | assert isinstance(context.page.document, BeautifulSoup) 19 | assert context.page.document.title.string == 'Test App: behave-django', ( 20 | f'unexpected title: {context.page.document.title.string}' 21 | ) 22 | 23 | 24 | @then('get_link() returns the link subdocument') 25 | def getlink_subdocument(context): 26 | context.about_link = context.page.get_link('about') 27 | assert isinstance(context.about_link, Tag), ( 28 | f'should be instance of {Tag.__name__} ' 29 | f'(not {context.about_link.__class__.__name__})' 30 | ) 31 | 32 | 33 | @when('I call click() on the link') 34 | def linkelement_click(context): 35 | context.next_page = context.about_link.click() 36 | 37 | 38 | @then('it loads a new PageObject') 39 | def click_returns_pageobject(context): 40 | assert About(context) == context.next_page 41 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings') 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /tests/test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behave/behave-django/9574e23fd1cbf7fcbd7c160fa2852134232be26e/tests/test_app/__init__.py -------------------------------------------------------------------------------- /tests/test_app/admin.py: -------------------------------------------------------------------------------- 1 | # from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /tests/test_app/features/multiple-feature-directories.feature: -------------------------------------------------------------------------------- 1 | Feature: Multiple feature directories 2 | In order to have feature files wherever I want 3 | As the Maintainer 4 | I want to have configurable feature directories 5 | 6 | Scenario: We're actually in a different directory 7 | Then it should work properly 8 | -------------------------------------------------------------------------------- /tests/test_app/fixtures/behave-fixtures.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "fields": { 3 | "name": "fixture loading test", 4 | "number": 42 5 | }, 6 | "model": "test_app.behavetestmodel" 7 | }] 8 | -------------------------------------------------------------------------------- /tests/test_app/fixtures/behave-second-fixture.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "fields": { 3 | "name": "second fixture", 4 | "number": 7 5 | }, 6 | "model": "test_app.behavetestmodel" 7 | }] 8 | -------------------------------------------------------------------------------- /tests/test_app/fixtures/empty-fixture.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /tests/test_app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [ 7 | ] 8 | 9 | operations = [ 10 | migrations.CreateModel( 11 | name='BehaveTestModel', 12 | fields=[ 13 | ('id', models.AutoField(verbose_name='ID', serialize=False, 14 | auto_created=True, primary_key=True)), 15 | ('name', models.CharField(max_length=255)), 16 | ('number', models.IntegerField()), 17 | ], 18 | options={ 19 | }, 20 | bases=(models.Model,), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /tests/test_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behave/behave-django/9574e23fd1cbf7fcbd7c160fa2852134232be26e/tests/test_app/migrations/__init__.py -------------------------------------------------------------------------------- /tests/test_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class BehaveTestModel(models.Model): 5 | name = models.CharField(max_length=255) 6 | number = models.IntegerField() 7 | 8 | def get_absolute_url(self): 9 | return f'/behave/test/{self.number}/{self.name}' 10 | -------------------------------------------------------------------------------- /tests/test_app/templates/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test App: About Us 5 | 6 | 7 |

About Us

8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/test_app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Test App: behave-django 5 | 6 | 7 |

Behave Django works

8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/test_app/tests.py: -------------------------------------------------------------------------------- 1 | # from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /tests/test_app/views.py: -------------------------------------------------------------------------------- 1 | # from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /tests/test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behave/behave-django/9574e23fd1cbf7fcbd7c160fa2852134232be26e/tests/test_project/__init__.py -------------------------------------------------------------------------------- /tests/test_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for test_project project. 3 | """ 4 | 5 | from pathlib import Path 6 | 7 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 8 | BASE_DIR = Path(__file__).resolve().parent.parent 9 | 10 | # Quick-start development settings - unsuitable for production 11 | # See https://docs.djangoproject.com/en/stable/howto/deployment/checklist/ 12 | 13 | # SECURITY WARNING: keep the secret key used in production secret! 14 | SECRET_KEY = '(&7)ub7ecukla=wzzb1h-u*x3l(93-=r*edcod@%)zn=v7u+@f' 15 | 16 | # SECURITY WARNING: don't run with debug turned on in production! 17 | DEBUG = True 18 | 19 | ALLOWED_HOSTS = [] 20 | 21 | 22 | # Application definition 23 | 24 | INSTALLED_APPS = [ 25 | 'django.contrib.admin', 26 | 'django.contrib.auth', 27 | 'django.contrib.contenttypes', 28 | 'django.contrib.sessions', 29 | 'django.contrib.messages', 30 | 'django.contrib.staticfiles', 31 | 'behave_django', 32 | 'test_app', 33 | ] 34 | 35 | MIDDLEWARE = [ 36 | 'django.middleware.security.SecurityMiddleware', 37 | 'django.contrib.sessions.middleware.SessionMiddleware', 38 | 'django.middleware.common.CommonMiddleware', 39 | 'django.middleware.csrf.CsrfViewMiddleware', 40 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 41 | 'django.contrib.messages.middleware.MessageMiddleware', 42 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 43 | ] 44 | 45 | TEMPLATES = [ 46 | { 47 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 48 | 'DIRS': [], 49 | 'APP_DIRS': True, 50 | 'OPTIONS': { 51 | 'debug': DEBUG, 52 | 'context_processors': [ 53 | 'django.template.context_processors.debug', 54 | 'django.template.context_processors.request', 55 | 'django.contrib.auth.context_processors.auth', 56 | 'django.contrib.messages.context_processors.messages', 57 | ], 58 | }, 59 | }, 60 | ] 61 | 62 | ROOT_URLCONF = 'test_project.urls' 63 | 64 | WSGI_APPLICATION = 'test_project.wsgi.application' 65 | 66 | # Database 67 | # https://docs.djangoproject.com/en/stable/ref/settings/#databases 68 | 69 | DATABASES = { 70 | 'default': { 71 | 'ENGINE': 'django.db.backends.sqlite3', 72 | 'NAME': BASE_DIR / 'db.sqlite3', 73 | } 74 | } 75 | 76 | # Internationalization 77 | # https://docs.djangoproject.com/en/stable/topics/i18n/ 78 | 79 | LANGUAGE_CODE = 'en-us' 80 | TIME_ZONE = 'UTC' 81 | USE_I18N = True 82 | USE_TZ = True 83 | 84 | # Static files (CSS, JavaScript, Images) 85 | # https://docs.djangoproject.com/en/stable/howto/static-files/ 86 | 87 | STATIC_URL = '/static/' 88 | 89 | # Default primary key field type 90 | # https://docs.djangoproject.com/en/stable/ref/settings/#default-auto-field 91 | 92 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 93 | -------------------------------------------------------------------------------- /tests/test_project/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URLs config for test_project project. 3 | """ 4 | 5 | from django.contrib import admin 6 | from django.urls import path 7 | from django.views.generic import TemplateView 8 | 9 | urlpatterns = [ 10 | path('', TemplateView.as_view(template_name='index.html'), name='home'), 11 | path('about/', TemplateView.as_view(template_name='about.html'), name='about'), 12 | path('admin/', admin.site.urls), 13 | ] 14 | -------------------------------------------------------------------------------- /tests/test_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test_project project. 3 | """ 4 | 5 | import os 6 | 7 | from django.core.wsgi import get_wsgi_application 8 | 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'test_project.settings') 10 | 11 | application = get_wsgi_application() 12 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/behave/behave-django/9574e23fd1cbf7fcbd7c160fa2852134232be26e/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | from importlib import reload 3 | 4 | import pytest 5 | 6 | from .util import DjangoSetupMixin, run_silently, show_run_error 7 | 8 | 9 | class TestCommandLine(DjangoSetupMixin): 10 | def test_additional_management_command_options(self): 11 | exit_status, output = run_silently('python tests/manage.py behave --help') 12 | 13 | assert exit_status == 0, show_run_error(exit_status, output) 14 | assert (os.linesep + ' --use-existing-database' + os.linesep) in output 15 | assert (os.linesep + ' -k, --keepdb') in output 16 | assert (os.linesep + ' -S, --simple') in output 17 | assert (os.linesep + ' --runner ') in output 18 | assert ( 19 | os.linesep + ' --behave-r RUNNER_CLASS, --behave-runner RUNNER_CLASS' 20 | ) in output 21 | assert (os.linesep + ' --noinput, --no-input') in output 22 | assert (os.linesep + ' --failfast') in output 23 | assert (os.linesep + ' -r, --reverse') in output 24 | 25 | def test_should_accept_behave_arguments(self): 26 | from behave_django.management.commands.behave import Command 27 | 28 | command = Command() 29 | argv = [ 30 | 'manage.py', 31 | 'behave', 32 | '--format', 33 | 'progress', 34 | '--behave-runner', 35 | 'behave.runner:Runner', 36 | '--runner', 37 | 'behave_django.runner:BehaviorDrivenTestRunner', 38 | '--settings', 39 | 'test_project.settings', 40 | '-i', 41 | 'some-pattern', 42 | 'features/running-tests.feature', 43 | ] 44 | args = command.get_behave_args(argv=argv) 45 | 46 | assert '--format' in args 47 | assert '--runner' in args 48 | assert args[args.index('--runner') + 1] == 'behave.runner:Runner' 49 | assert 'progress' in args 50 | assert '-i' in args 51 | assert 'some-pattern' in args 52 | 53 | def test_should_not_include_non_behave_arguments(self): 54 | from behave_django.management.commands.behave import Command 55 | 56 | command = Command() 57 | argv = [ 58 | 'manage.py', 59 | 'behave', 60 | '--format', 61 | 'progress', 62 | '--settings', 63 | 'test_project.settings', 64 | 'features/running-tests.feature', 65 | ] 66 | args = command.get_behave_args(argv=argv) 67 | 68 | assert '--settings' not in args 69 | assert 'test_project.settings' not in args 70 | 71 | def test_should_return_positional_args(self): 72 | from behave_django.management.commands.behave import Command 73 | 74 | command = Command() 75 | argv = [ 76 | 'manage.py', 77 | 'behave', 78 | '--format', 79 | 'progress', 80 | '--settings', 81 | 'test_project.settings', 82 | 'features/running-tests.feature', 83 | ] 84 | args = command.get_behave_args(argv=argv) 85 | 86 | assert 'features/running-tests.feature' in args 87 | 88 | def test_no_arguments_should_not_cause_issues(self): 89 | from behave_django.management.commands.behave import Command 90 | 91 | command = Command() 92 | args = command.get_behave_args(argv=['manage.py', 'behave']) 93 | 94 | assert args == [] 95 | 96 | def test_positional_args_should_work(self): 97 | exit_status, output = run_silently( 98 | 'python tests/manage.py behave' 99 | ' tests/acceptance/features/running-tests.feature' 100 | ) 101 | assert exit_status == 0, show_run_error(exit_status, output) 102 | 103 | def test_command_import_dont_patch_behave_options(self): 104 | # We reload the tested imports because they 105 | # could have been imported by previous tests. 106 | import behave.configuration 107 | 108 | reload(behave.configuration) 109 | behave_options_backup = [ 110 | (first, second.copy()) for (first, second) in behave.configuration.OPTIONS 111 | ] 112 | import behave_django.management.commands.behave 113 | 114 | reload(behave_django.management.commands.behave) 115 | assert behave_options_backup == behave.configuration.OPTIONS 116 | 117 | def test_conflicting_options_should_get_prefixed(self): 118 | from behave_django.management.commands.behave import Command 119 | 120 | command = Command() 121 | args = command.get_behave_args( 122 | argv=['manage.py', 'behave', '--behave-k', '--behave-version'] 123 | ) 124 | assert args == ['-k', '--version'] 125 | 126 | def test_simple_and_use_existing_database_flags_raise_a_warning(self): 127 | exit_status, output = run_silently( 128 | 'python tests/manage.py behave' 129 | ' --simple --use-existing-database --tags=@skip-all' 130 | ) 131 | assert exit_status == 0, show_run_error(exit_status, output) 132 | assert ( 133 | os.linesep 134 | + '--simple flag has no effect together with --use-existing-database' 135 | + os.linesep 136 | ) in output 137 | 138 | @pytest.mark.parametrize( 139 | ('arguments', 'expect_error'), 140 | [ 141 | ( 142 | '--runner behave_django.runner:BehaviorDrivenTestRunner', 143 | False, 144 | ), 145 | ( 146 | '--runner behave_django.runner:SimpleTestRunner --simple', 147 | True, 148 | ), 149 | ( 150 | '--runner behave_django.runner:BehaviorDrivenTestRunner --simple', 151 | False, 152 | ), 153 | ( 154 | ( 155 | '--behave-runner behave.runner:Runner ' 156 | '--runner behave_django.runner:SimpleTestRunner ' 157 | '--simple' 158 | ), 159 | True, 160 | ), 161 | ( 162 | ( 163 | '--runner behave_django.runner:SimpleTestRunner ' 164 | '--use-existing-database' 165 | ), 166 | True, 167 | ), 168 | ('--behave-runner behave.runner:Runner --simple', False), 169 | ( 170 | ( 171 | '--behave-runner behave.runner:Runner ' 172 | '--runner behave_django.runner:BehaviorDrivenTestRunner' 173 | ), 174 | False, 175 | ), 176 | ], 177 | ) 178 | def test_runner_and_others_flags_raise_a_warning(self, arguments, expect_error): 179 | exit_status, output = run_silently( 180 | f'python tests/manage.py behave {arguments} --tags=@skip-all' 181 | ) 182 | assert exit_status == 0, show_run_error(exit_status, output) 183 | 184 | warning_message = ( 185 | os.linesep 186 | + '--use-existing-database or --simple has no effect together with --runner' 187 | + os.linesep 188 | ) 189 | if expect_error: 190 | assert warning_message in output 191 | else: 192 | assert warning_message not in output 193 | -------------------------------------------------------------------------------- /tests/unit/test_exit_codes.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from .util import DjangoSetupMixin 4 | 5 | 6 | class TestExitCodes(DjangoSetupMixin): 7 | @mock.patch('behave_django.management.commands.behave.behave_main', return_value=0) 8 | @mock.patch('sys.exit') 9 | def test_command_should_exit_zero_if_passing(self, mock_sys_exit, mock_behave_main): 10 | # If the exit status returned by behave_main is 0, make sure sys.exit 11 | # does not get called 12 | self.run_management_command('behave', dry_run=True) 13 | assert not mock_sys_exit.called 14 | 15 | @mock.patch('behave_django.management.commands.behave.behave_main', return_value=1) 16 | @mock.patch('sys.exit') 17 | def test_command_should_exit_nonzero_if_failing( 18 | self, mock_sys_exit, mock_behave_main 19 | ): 20 | # If the exit status returned by behave_main is anything other than 0, 21 | # make sure sys.exit gets called with the exit code 22 | 23 | # Dry run to not create the database for faster tests 24 | self.run_management_command('behave', dry_run=True) 25 | mock_sys_exit.assert_called_once_with(1) 26 | -------------------------------------------------------------------------------- /tests/unit/test_passthru_args.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from .util import DjangoSetupMixin 4 | 5 | 6 | @mock.patch('behave_django.management.commands.behave.behave_main', return_value=0) 7 | @mock.patch('behave_django.runner.BehaviorDrivenTestRunner') 8 | class TestPassThruArgs(DjangoSetupMixin): 9 | def test_keepdb_flag(self, mock_test_runner, mock_behave_main): 10 | """Test if keepdb is properly set on the test_runner.""" 11 | 12 | self.run_management_command('behave', keepdb=True) 13 | _, kwargs = mock_test_runner.call_args 14 | assert kwargs['keepdb'] is True 15 | 16 | def test_interactive_flag(self, mock_test_runner, mock_behave_main): 17 | """Test if interactive is properly set on the test_runner.""" 18 | 19 | self.run_management_command('behave', interactive=False) 20 | _, kwargs = mock_test_runner.call_args 21 | assert kwargs['interactive'] is False 22 | 23 | def test_failfast_flag(self, mock_test_runner, mock_behave_main): 24 | """Test if failfast is properly set on the test_runner.""" 25 | 26 | self.run_management_command('behave', failfast=True) 27 | _, kwargs = mock_test_runner.call_args 28 | assert kwargs['failfast'] is True 29 | 30 | def test_reverse_flag(self, mock_test_runner, mock_behave_main): 31 | """Test if reverse is properly set on the test_runner.""" 32 | 33 | self.run_management_command('behave', reverse=True) 34 | _, kwargs = mock_test_runner.call_args 35 | assert kwargs['reverse'] is True 36 | -------------------------------------------------------------------------------- /tests/unit/test_simple_testcase.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | from behave.runner import Context, Runner 5 | from django.test.testcases import TestCase 6 | 7 | from behave_django.runner import SimpleTestRunner 8 | 9 | from .util import DjangoSetupMixin 10 | 11 | 12 | class TestSimpleTestCase(DjangoSetupMixin): 13 | @mock.patch('behave_django.management.commands.behave.behave_main', return_value=0) 14 | @mock.patch('behave_django.management.commands.behave.SimpleTestRunner') 15 | def test_use_simple_test_runner(self, mock_simple_test_runner, mock_behave_main): 16 | self.run_management_command('behave', simple=True) 17 | mock_behave_main.assert_called_once_with(args=[]) 18 | mock_simple_test_runner.assert_called_once_with() 19 | 20 | def test_simple_test_runner_uses_simple_testcase(self): 21 | runner = mock.MagicMock() 22 | context = Context(runner) 23 | SimpleTestRunner().setup_testclass(context) 24 | assert isinstance(context.test, TestCase) 25 | 26 | def test_simple_testcase_fails_when_accessing_base_url(self): 27 | runner = Runner(mock.MagicMock()) 28 | runner.context = Context(runner) 29 | SimpleTestRunner().patch_context(runner.context) 30 | SimpleTestRunner().setup_testclass(runner.context) 31 | with pytest.raises(RuntimeError): 32 | assert runner.context.base_url == 'should raise an exception!' 33 | 34 | def test_simple_testcase_fails_when_calling_get_url(self): 35 | runner = Runner(mock.MagicMock()) 36 | runner.context = Context(runner) 37 | SimpleTestRunner().patch_context(runner.context) 38 | SimpleTestRunner().setup_testclass(runner.context) 39 | with pytest.raises(RuntimeError): 40 | runner.context.get_url() 41 | -------------------------------------------------------------------------------- /tests/unit/test_use_existing_db.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from .util import DjangoSetupMixin 4 | 5 | 6 | class TestUseExistingDB(DjangoSetupMixin): 7 | @mock.patch('behave_django.management.commands.behave.behave_main', return_value=0) 8 | @mock.patch('behave_django.management.commands.behave.ExistingDatabaseTestRunner') 9 | def test_dont_create_db_with_dryrun( 10 | self, mock_existing_database_runner, mock_behave_main 11 | ): 12 | self.run_management_command('behave', dry_run=True) 13 | mock_behave_main.assert_called_once_with(args=[]) 14 | mock_existing_database_runner.assert_called_once_with() 15 | 16 | @mock.patch('behave_django.management.commands.behave.behave_main', return_value=0) 17 | @mock.patch('behave_django.management.commands.behave.ExistingDatabaseTestRunner') 18 | def test_dont_create_db_with_useexistingdb( 19 | self, mock_existing_database_runner, mock_behave_main 20 | ): 21 | self.run_management_command('behave', use_existing_database=True) 22 | mock_behave_main.assert_called_once_with(args=[]) 23 | mock_existing_database_runner.assert_called_once_with() 24 | -------------------------------------------------------------------------------- /tests/unit/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | from subprocess import PIPE, Popen 3 | 4 | import django 5 | from django.core.management import call_command 6 | 7 | 8 | class DjangoSetupMixin: 9 | @classmethod 10 | def setup_class(cls): 11 | # NOTE: this may potentially have side-effects, making tests pass 12 | # that would otherwise fail, because it *always* overrides which 13 | # settings module is used. 14 | os.environ['DJANGO_SETTINGS_MODULE'] = 'test_project.settings' 15 | 16 | def run_management_command(self, command, *args, **kwargs): 17 | django.setup() 18 | call_command(command, *args, **kwargs) 19 | 20 | 21 | def run_silently(command): 22 | """Run a shell command and return both exit_status and console output.""" 23 | command_args = command.split() 24 | process = Popen(command_args, stdout=PIPE, stderr=PIPE, stdin=PIPE) 25 | stdout, stderr = process.communicate() 26 | output = ( 27 | stdout.decode('UTF-8') + os.linesep + stderr.decode('UTF-8') 28 | ).strip() + os.linesep 29 | return process.returncode, output 30 | 31 | 32 | def show_run_error(exit_status, output): 33 | """An easy-to-read error message for assert""" 34 | return f'Failed with exit status {exit_status}\n--------------\n{output}' 35 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Tox (https://tox.wiki/) - run tests in isolation using virtualenv. 2 | 3 | [tox] 4 | envlist = 5 | lint 6 | format 7 | # Python/Django combinations that are officially supported 8 | py3{8,9,10,11,12}-django{42} 9 | py3{10,11,12}-django{50,51} 10 | behave-latest 11 | package 12 | docs 13 | clean 14 | 15 | [gh-actions] 16 | python = 17 | 3.8: py38 18 | 3.9: py39 19 | 3.10: py310 20 | 3.11: py311 21 | 3.12: py312 22 | 23 | [gh-actions:env] 24 | DJANGO = 25 | 4.2: django42 26 | 5.0: django50 27 | 5.1: django51 28 | 29 | [testenv] 30 | description = Unit tests 31 | deps = 32 | coverage[toml] 33 | django42: Django>=4.2,<5.0 34 | django50: Django>=5.0,<5.1 35 | django51: Django>=5.1,<5.2 36 | latest: Django 37 | latest: git+https://github.com/behave/behave.git#egg=behave 38 | pytest 39 | commands = 40 | coverage run -m pytest {posargs} 41 | coverage run -a tests/manage.py behave --tags=~@failing --tags=~@requires-live-http --simple {posargs} 42 | coverage run -a tests/manage.py behave --tags=~@failing {posargs} 43 | coverage xml 44 | coverage report 45 | 46 | [testenv:clean] 47 | description = Remove Python bytecode and other debris 48 | skip_install = true 49 | deps = pyclean 50 | commands = pyclean {posargs:. tests --debris --erase TESTS-*.xml *-report.xml --yes --verbose} 51 | 52 | [testenv:docs] 53 | description = Build package documentation (HTML) 54 | skip_install = true 55 | deps = sphinx 56 | commands = sphinx-build -M html docs docs/_build 57 | setenv = LANG=C.UTF-8 58 | 59 | [testenv:format] 60 | description = Ensure consistent code style (Ruff) 61 | skip_install = true 62 | deps = ruff 63 | commands = ruff format {posargs:--check --diff .} 64 | 65 | [testenv:lint] 66 | description = Lightening-fast linting (Ruff) 67 | skip_install = true 68 | deps = ruff 69 | commands = ruff check {posargs:.} 70 | 71 | [testenv:package] 72 | description = Build package and check metadata (or upload package) 73 | skip_install = true 74 | deps = 75 | build 76 | twine 77 | commands = 78 | python -m build 79 | twine {posargs:check --strict} dist/* 80 | passenv = 81 | TWINE_USERNAME 82 | TWINE_PASSWORD 83 | TWINE_REPOSITORY_URL 84 | --------------------------------------------------------------------------------