Skip to content

Commit

Permalink
Allow override_config for pytest (#338)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Paweł Zarębski <[email protected]>
  • Loading branch information
3 people authored Apr 4, 2020
1 parent 4de4114 commit bd8041c
Show file tree
Hide file tree
Showing 10 changed files with 247 additions and 8 deletions.
4 changes: 3 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[run]
source = constance
branch = 1
omit =
*/pytest.py

[report]
omit = *tests*,*migrations*
omit = *tests*,*migrations*,.tox/*,setup.py,*settings.py
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ saw2th <[email protected]>
trbs <[email protected]>
vl <[email protected]>
vl <vl@u64.(none)>
Vladas Tamoshaitis <[email protected]>
Dmitriy Tatarkin <[email protected]>
Alexandr Artemyev <[email protected]>
Elisey Zanko <[email protected]>
2 changes: 1 addition & 1 deletion constance/test/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .utils import override_config
from .unittest import override_config # pragma: no cover
79 changes: 79 additions & 0 deletions constance/test/pytest.py
Original file line number Diff line number Diff line change
@@ -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
File renamed without changes.
70 changes: 70 additions & 0 deletions docs/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 6 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,10 @@ def find_version(*file_paths):
extras_require={
'database': ['django-picklefield'],
'redis': ['redis'],
}
},
entry_points={
'pytest11': [
'pytest-django-constance = constance.test.pytest',
],
},
)
78 changes: 78 additions & 0 deletions tests/test_pytest_overrides.py
Original file line number Diff line number Diff line change
@@ -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
File renamed without changes.
14 changes: 9 additions & 5 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -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 =
Expand All @@ -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

0 comments on commit bd8041c

Please sign in to comment.