Skip to content

Commit 37150d7

Browse files
committed
Adds tests for invalid template variables
Django catches all `VariableDoesNotExist` exceptions to replace them in templates with a modifiable string that you can define in your settings. Sadly that doesn't allow you to find them in unit tests. `_fail_for_invalid_template_variable` sets the setting `TEMPLATE_STRING_IF_INVALID` to a custom class that not only fails the current test but prints a pretty message including the template name. This behavior can be used with the new `--test-templates` command line option. A new marker allows disabling this behavior, eg: @pytest.mark.ignore_template_errors def test_something(): pass This marker sets the setting to None, if you want it to be a string, you can use the `settings` fixture to set it to your desired value.
1 parent 1e72766 commit 37150d7

4 files changed

Lines changed: 161 additions & 0 deletions

File tree

docs/helpers.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,23 @@ on what marks are and for notes on using_ them.
7777
assert 'Success!' in client.get('/some_url_defined_in_test_urls/')
7878

7979

80+
``pytest.mark.ignore_template_errors`` - ignore invalid template variables
81+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
82+
83+
..py:function:: pytest.mark.ignore_template_errors
84+
85+
If you run py.test using the ``--test-templates`` option, tests will fail should your templates
86+
contain any invalid variables.
87+
This marker will disable this feature by setting ``settings.TEMPLATE_STRING_IF_INVALID=None``
88+
or the ``string_if_invalid`` template option in Django>=1.7
89+
90+
Example usage::
91+
92+
@pytest.mark.ignore_template_errors
93+
def test_something(client):
94+
client('some-url-with-invalid-template-vars')
95+
96+
8097
Fixtures
8198
--------
8299

docs/usage.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ the command line::
2222
See the `py.test documentation on Usage and invocations
2323
<http://pytest.org/latest/usage.html>`_ for more help on available parameters.
2424

25+
Additional command line options
26+
-------------------------------
27+
28+
``--test-templates`` - fail for invalid variables in templates
29+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
30+
Fail tests that render templates which make use of invalid template variables.
31+
2532
Running tests in parallel with pytest-xdist
2633
-------------------------------------------
2734
pytest-django supports running tests on multiple processes to speed up test

pytest_django/plugin.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"""
66

77
import contextlib
8+
import inspect
9+
from functools import reduce
810
import os
911
import sys
1012
import types
@@ -62,6 +64,9 @@ def pytest_addoption(parser):
6264
'Automatically find and add a Django project to the '
6365
'Python path.',
6466
default=True)
67+
group._addoption('--test-templates',
68+
action='store_true', dest='test-templates', default=False,
69+
help='Fail for invalid variables in templates.')
6570

6671

6772
def _exists(path, ignore=EnvironmentError):
@@ -327,6 +332,88 @@ def restore():
327332
request.addfinalizer(restore)
328333

329334

335+
@pytest.fixture(autouse=True, scope='session')
336+
def _fail_for_invalid_template_variable(request):
337+
"""Fixture that fails for invalid variables in templates.
338+
339+
This fixture will fail each test that uses django template rendering
340+
should a template contain an invalid template variable.
341+
The fail message will include the name of the invalid variable and
342+
in most cases the template name.
343+
344+
It does not raise an exception, but fails, as the stack trace doesn't
345+
offer any helpful information to debug.
346+
This behavior can be switched of using the marker:
347+
``ignore_template_errors``
348+
"""
349+
class InvalidVarException(object):
350+
"""Custom handler for invalid strings in templates."""
351+
352+
def __init__(self):
353+
self.fail = True
354+
355+
def __contains__(self, key):
356+
"""There is a test for '%s' in TEMPLATE_STRING_IF_INVALID."""
357+
return key == '%s'
358+
359+
def _get_template(self):
360+
from django.template import Template
361+
362+
stack = inspect.stack()
363+
# finding the ``render`` needle in the stack
364+
frame = reduce(
365+
lambda x, y: y[3] == 'render' and 'base.py' in y[1] and y or x,
366+
stack
367+
)
368+
# assert 0, stack
369+
frame = frame[0]
370+
# finding only the frame locals in all frame members
371+
f_locals = reduce(
372+
lambda x, y: y[0] == 'f_locals' and y or x,
373+
inspect.getmembers(frame)
374+
)[1]
375+
# ``django.template.base.Template``
376+
template = f_locals['self']
377+
if isinstance(template, Template):
378+
return template
379+
380+
def __mod__(self, var):
381+
"""Handle TEMPLATE_STRING_IF_INVALID % var."""
382+
template = self._get_template()
383+
if template:
384+
msg = "Undefined template variable '%s' in '%s'" % (var, template.name)
385+
else:
386+
msg = "Undefined template variable '%s'" % var
387+
if self.fail:
388+
pytest.fail(msg, pytrace=False)
389+
else:
390+
return msg
391+
if django_settings_is_configured() \
392+
and request.config.getvalue('test-templates'):
393+
import django
394+
from django.conf import settings
395+
396+
if django.VERSION >= (1, 8) and settings.TEMPLATES:
397+
settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'] = InvalidVarException()
398+
else:
399+
settings.TEMPLATE_STRING_IF_INVALID = InvalidVarException()
400+
401+
402+
@pytest.fixture(autouse=True)
403+
def _template_string_if_invalid_marker(request):
404+
"""Apply the @pytest.mark.ignore_template_errors marker,
405+
internal to pytest-django."""
406+
marker = request.keywords.get('ignore_template_errors', None)
407+
if marker and django_settings_is_configured() \
408+
and request.config.getvalue('test-templates'):
409+
import django
410+
from django.conf import settings
411+
412+
if django.VERSION >= (1, 8) and settings.TEMPLATES:
413+
settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'].fail = False
414+
else:
415+
settings.TEMPLATE_STRING_IF_INVALID.fail = False
416+
330417
# ############### Helper Functions ################
331418

332419

tests/test_environment.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,56 @@ def test_mail_again():
2828
test_mail()
2929

3030

31+
@pytest.mark.django_project(extra_settings="""
32+
INSTALLED_APPS = [
33+
'tpkg.app',
34+
]
35+
TEMPLATE_LOADERS = (
36+
'django.template.loaders.filesystem.Loader',
37+
'django.template.loaders.app_directories.Loader',
38+
)
39+
ROOT_URLCONF = 'tpkg.app.urls'
40+
""")
41+
def test_invalid_template_variable(django_testdir):
42+
django_testdir.create_app_file("""
43+
try:
44+
from django.conf.urls import patterns # Django >1.4
45+
except ImportError:
46+
from django.conf.urls.defaults import patterns # Django 1.3
47+
48+
urlpatterns = patterns(
49+
'',
50+
(r'invalid_template/', 'tpkg.app.views.invalid_template'),
51+
)
52+
""", 'urls.py')
53+
django_testdir.create_app_file("""
54+
from django.shortcuts import render
55+
56+
57+
def invalid_template(request):
58+
return render(request, 'invalid_template.html', {})
59+
""", 'views.py')
60+
django_testdir.create_app_file(
61+
"<div>{{ invalid_var }}</div>",
62+
'templates/invalid_template.html'
63+
)
64+
django_testdir.create_test_module('''
65+
import pytest
66+
67+
def test_for_invalid_template(client):
68+
client.get('/invalid_template/')
69+
70+
@pytest.mark.ignore_template_errors
71+
def test_ignore(client):
72+
client.get('/invalid_template/')
73+
''')
74+
result = django_testdir.runpytest('-s', '--test-templates')
75+
result.stdout.fnmatch_lines_random([
76+
"tpkg/test_the_test.py F.",
77+
"Undefined template variable 'invalid_var' in 'invalid_template.html'",
78+
])
79+
80+
3181
@pytest.mark.django_db
3282
def test_database_rollback():
3383
assert Item.objects.count() == 0

0 commit comments

Comments
 (0)