Allow override_config for pytest (#338)

* provides: base override class; unittest and pytest overrides

* raise invalid config error earlier

* update AUTHORS

* avoid AttributeError

* fix comment

* add tests

* fix tests, update docstring

* update docs, improve tests

* fix docs

* fix markdown

* refactor pytest override, use hidden fixture, refactor base and unittest classes

* improve docstring and error

* refactor pytest override to use hooks

* set minimum pytest version

* revert empty lines removal

* introduce pytest test runner for package, refactoring

* WIP

* Finalize tox config, refactor docs, add global fixture

* skip py35

* pytest command: remove unnecessary ignore

* address comments

* Update constance/test/pytest.py

* address comments

* add test for checking nested markers

Co-authored-by: Camilo Nova <camilo.nova@gmail.com>
Co-authored-by: Paweł Zarębski <ppjzarebski@gmail.com>
This commit is contained in:
Vladas Tamoshaitis 2020-04-04 19:38:22 +03:00 committed by GitHub
parent 4de4114bbd
commit bd8041c55f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 247 additions and 8 deletions

View file

@ -1,6 +1,8 @@
[run]
source = constance
branch = 1
omit =
*/pytest.py
[report]
omit = *tests*,*migrations*
omit = *tests*,*migrations*,.tox/*,setup.py,*settings.py

View file

@ -40,6 +40,7 @@ saw2th <stephen@saw2th.co.uk>
trbs <trbs@trbs.net>
vl <1844144@gmail.com>
vl <vl@u64.(none)>
Vladas Tamoshaitis <amd.vladas@gmail.com>
Dmitriy Tatarkin <mail@dtatarkin.ru>
Alexandr Artemyev <mogost@gmail.com>
Elisey Zanko <elisey.zanko@gmail.com>

View file

@ -1 +1 @@
from .utils import override_config
from .unittest import override_config # pragma: no cover

79
constance/test/pytest.py Normal file
View file

@ -0,0 +1,79 @@
"""
Pytest constance override config plugin.
Inspired by https://github.com/pytest-dev/pytest-django/.
"""
import pytest
from contextlib import ContextDecorator
from constance import config as constance_config
@pytest.hookimpl(trylast=True)
def pytest_configure(config): # pragma: no cover
"""
Register override_config marker.
"""
config.addinivalue_line(
"markers",
(
"override_config(**kwargs): "
"mark test to override django-constance config"
)
)
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item): # pragma: no cover
"""
Validate constance override marker params. Run test with overrided config.
"""
marker = item.get_closest_marker("override_config")
if marker is not None:
if marker.args:
pytest.fail(
"Constance override can not not accept positional args"
)
with override_config(**marker.kwargs):
yield
else:
yield
class override_config(ContextDecorator):
"""
Override config while running test function.
Act as context manager and decorator.
"""
def enable(self):
"""
Store original config values and set overridden values.
"""
for key, value in self._to_override.items():
self._original_values[key] = getattr(constance_config, key)
setattr(constance_config, key, value)
def disable(self):
"""
Set original values to the config.
"""
for key, value in self._original_values.items():
setattr(constance_config, key, value)
def __init__(self, **kwargs):
self._to_override = kwargs.copy()
self._original_values = {}
def __enter__(self):
self.enable()
def __exit__(self, exc_type, exc_val, exc_tb):
self.disable()
@pytest.fixture(name="override_config")
def _override_config():
"""
Make override_config available as a function fixture.
"""
return override_config

View file

@ -38,3 +38,73 @@ method level and also as a
def test_what_is_your_favourite_color(self):
with override_config(YOUR_FAVOURITE_COLOR="Blue?"):
self.assertEqual(config.YOUR_FAVOURITE_COLOR, "Blue?")
Pytest usage
~~~~~
Django-constance provides pytest plugin that adds marker
:class:`@pytest.mark.override_config()`. It handles config override for
module/class/function, and automatically revert any changes made to the
constance config values when test is completed.
.. py:function:: pytest.mark.override_config(**kwargs)
Specify different config values for the marked tests in kwargs.
Module scope override
.. code-block:: python
pytestmark = pytest.mark.override_config(API_URL="/awesome/url/")
def test_api_url_is_awesome():
...
Class/function scope
.. code-block:: python
from constance import config
@pytest.mark.override_config(API_URL="/awesome/url/")
class SomeClassTest:
def test_is_awesome_url(self):
assert config.API_URL == "/awesome/url/"
@pytest.mark.override_config(API_URL="/another/awesome/url/")
def test_another_awesome_url(self):
assert config.API_URL == "/another/awesome/url/"
If you want to use override as a context manager or decorator, consider using
.. code-block:: python
from constance.test.pytest import override_config
def test_override_context_manager():
with override_config(BOOL_VALUE=False):
...
# or
@override_config(BOOL_VALUE=False)
def test_override_context_manager():
...
Pytest fixture as function or method parameter (
NOTE: no import needed as fixture is available globally)
.. code-block:: python
def test_api_url_is_awesome(override_config):
with override_config(API_URL="/awesome/url/"):
...
Any scope, auto-used fixture alternative can also be implemented like this
.. code-block:: python
@pytest.fixture(scope='module', autouse=True) # e.g. module scope
def api_url(override_config):
with override_config(API_URL="/awesome/url/"):
yield

View file

@ -56,5 +56,10 @@ setup(
extras_require={
'database': ['django-picklefield'],
'redis': ['redis'],
}
},
entry_points={
'pytest11': [
'pytest-django-constance = constance.test.pytest',
],
},
)

View file

@ -0,0 +1,78 @@
import unittest
try:
import pytest
from constance import config
from constance.test.pytest import override_config
class TestPytestOverrideConfigFunctionDecorator:
"""Test that the override_config decorator works correctly for Pytest classes.
Test usage of override_config on test method and as context manager.
"""
def test_default_value_is_true(self):
"""Assert that the default value of config.BOOL_VALUE is True."""
assert config.BOOL_VALUE
@pytest.mark.override_config(BOOL_VALUE=False)
def test_override_config_on_method_changes_config_value(self):
"""Assert that the pytest mark decorator changes config.BOOL_VALUE."""
assert not config.BOOL_VALUE
def test_override_config_as_context_manager_changes_config_value(self):
"""Assert that the context manager changes config.BOOL_VALUE."""
with override_config(BOOL_VALUE=False):
assert not config.BOOL_VALUE
assert config.BOOL_VALUE
@override_config(BOOL_VALUE=False)
def test_method_decorator(self):
"""Ensure `override_config` can be used as test method decorator."""
assert not config.BOOL_VALUE
@pytest.mark.override_config(BOOL_VALUE=False)
class TestPytestOverrideConfigDecorator:
"""Test that the override_config decorator works on classes."""
def test_override_config_on_class_changes_config_value(self):
"""Asser that the class decorator changes config.BOOL_VALUE."""
assert not config.BOOL_VALUE
@pytest.mark.override_config(BOOL_VALUE='True')
def test_override_config_on_overrided_value(self):
"""Ensure that method mark decorator changes already overrided value for class."""
assert config.BOOL_VALUE == 'True'
def test_fixture_override_config(override_config):
"""
Ensure `override_config` fixture is available globally
and can be used in test functions.
"""
with override_config(BOOL_VALUE=False):
assert not config.BOOL_VALUE
@override_config(BOOL_VALUE=False)
def test_func_decorator():
"""Ensure `override_config` can be used as test function decorator."""
assert not config.BOOL_VALUE
except ImportError:
pass
class PytestTests(unittest.TestCase):
def setUp(self):
self.skipTest('Skip all pytest tests when using unittest')
def test_do_not_skip_silently(self):
"""
If no at least one test present, unittest silently skips module.
"""
pass

14
tox.ini
View file

@ -1,8 +1,8 @@
[tox]
envlist =
py{35,36,37,pypy3}-django{22}
py{36,37,38}-django{30}
py{36,37,38}-django-master
py{35,36,37,pypy3}-django{22}-unittest
py{36,37,38}-django{30,-master}-unittest
py{36,37,38,pypy3}-django{22,30,-master}-pytest
[testenv]
deps =
@ -13,12 +13,16 @@ deps =
django-22: Django>=2.2,<3.0
django-30: Django>=3.0,<3.1
django-master: https://github.com/django/django/archive/master.tar.gz
pytest: pytest
pytest: pytest-cov
pytest: pytest-django
usedevelop = True
ignore_outcome =
django-master: True
commands =
coverage run {envbindir}/django-admin test -v2
coverage report
unittest: coverage run {envbindir}/django-admin test -v2
unittest: coverage report
pytest: pytest --cov=. --ignore=.tox --disable-pytest-warnings {toxinidir}
setenv =
PYTHONDONTWRITEBYTECODE=1
DJANGO_SETTINGS_MODULE=tests.settings