Code

Ticket #17604: 17604-assertTemplateUsed.1.diff

File 17604-assertTemplateUsed.1.diff, 12.9 KB (added by gregmuellegger, 2 years ago)
Line 
1diff --git a/django/test/testcases.py b/django/test/testcases.py
2index 53ea02a..2e011e4 100644
3--- a/django/test/testcases.py
4+++ b/django/test/testcases.py
5@@ -3,6 +3,7 @@ from __future__ import with_statement
6 import os
7 import re
8 import sys
9+from copy import copy
10 from functools import wraps
11 from urlparse import urlsplit, urlunsplit
12 from xml.dom.minidom import parseString, Node
13@@ -28,8 +29,10 @@ from django.forms.fields import CharField
14 from django.http import QueryDict
15 from django.test import _doctest as doctest
16 from django.test.client import Client
17+from django.test.signals import template_rendered
18 from django.test.utils import (get_warnings_state, restore_warnings_state,
19     override_settings)
20+from django.test.utils import ContextList
21 from django.utils import simplejson, unittest as ut2
22 from django.utils.encoding import smart_str, force_unicode
23 from django.views.static import serve
24@@ -260,8 +263,53 @@ class _AssertNumQueriesContext(object):
25         )
26 
27 
28-class SimpleTestCase(ut2.TestCase):
29+class _AssertTemplateUsedContext(object):
30+    def __init__(self, test_case, template_name):
31+        self.test_case = test_case
32+        self.template_name = template_name
33+        self.rendered_templates = []
34+        self.rendered_template_names = []
35+        self.context = ContextList()
36+
37+    def on_template_render(self, sender, signal, template, context, **kwargs):
38+        self.rendered_templates.append(template)
39+        self.rendered_template_names.append(template.name)
40+        self.context.append(copy(context))
41+
42+    def test(self):
43+        return self.template_name in self.rendered_template_names
44+
45+    def message(self):
46+        return u'%s was not rendered.' % self.template_name
47+
48+    def __enter__(self):
49+        template_rendered.connect(self.on_template_render)
50+        return self
51+
52+    def __exit__(self, exc_type, exc_value, traceback):
53+        template_rendered.disconnect(self.on_template_render)
54+        if exc_type is not None:
55+            return
56 
57+        if not self.test():
58+            message = self.message()
59+            if len(self.rendered_templates) == 0:
60+                message += u' No template was rendered.'
61+            else:
62+                message += u' Following templates were rendered: %s' % (
63+                    ', '.join(self.rendered_template_names))
64+            self.test_case.fail(message)
65+
66+
67+class _AssertTemplateNotUsedContext(_AssertTemplateUsedContext):
68+    def test(self):
69+        return self.template_name not in self.rendered_template_names
70+
71+    def message(self):
72+        return u'%s was rendered.' % self.template_name
73+
74+
75+class SimpleTestCase(ut2.TestCase):
76     def save_warnings_state(self):
77         """
78         Saves the state of the warnings module
79@@ -612,14 +660,25 @@ class TransactionTestCase(SimpleTestCase):
80             self.fail(msg_prefix + "The form '%s' was not used to render the"
81                       " response" % form)
82 
83-    def assertTemplateUsed(self, response, template_name, msg_prefix=''):
84+    def assertTemplateUsed(self, response=None, template_name=None, msg_prefix=''):
85         """
86         Asserts that the template with the provided name was used in rendering
87-        the response.
88+        the response. Also useable as context manager.
89         """
90+        if response is None and template_name is None:
91+            raise TypeError(u'response and/or template_name argument must be provided')
92+
93         if msg_prefix:
94             msg_prefix += ": "
95 
96+        # use assertTemplateUsed as context manager
97+        if not hasattr(response, 'templates') or (response is None and template_name):
98+            if response:
99+                template_name = response
100+                response = None
101+            context = _AssertTemplateUsedContext(self, template_name)
102+            return context
103+
104         template_names = [t.name for t in response.templates]
105         if not template_names:
106             self.fail(msg_prefix + "No templates used to render the response")
107@@ -628,14 +687,25 @@ class TransactionTestCase(SimpleTestCase):
108             " the response. Actual template(s) used: %s" %
109                 (template_name, u', '.join(template_names)))
110 
111-    def assertTemplateNotUsed(self, response, template_name, msg_prefix=''):
112+    def assertTemplateNotUsed(self, response=None, template_name=None, msg_prefix=''):
113         """
114         Asserts that the template with the provided name was NOT used in
115-        rendering the response.
116+        rendering the response. Also useable as context manager.
117         """
118+        if response is None and template_name is None:
119+            raise TypeError(u'response and/or template_name argument must be provided')
120+
121         if msg_prefix:
122             msg_prefix += ": "
123 
124+        # use assertTemplateUsed as context manager
125+        if not hasattr(response, 'templates') or (response is None and template_name):
126+            if response:
127+                template_name = response
128+                response = None
129+            context = _AssertTemplateNotUsedContext(self, template_name)
130+            return context
131+
132         template_names = [t.name for t in response.templates]
133         self.assertFalse(template_name in template_names,
134             msg_prefix + "Template '%s' was used unexpectedly in rendering"
135diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
136index 9b3c219..2c7cbf1 100644
137--- a/docs/releases/1.4.txt
138+++ b/docs/releases/1.4.txt
139@@ -942,6 +942,21 @@ apply URL escaping again. This is wrong for URLs whose unquoted form contains
140 a ``%xx`` sequence, but such URLs are very unlikely to happen in the wild,
141 since they would confuse browsers too.
142 
143+``assertTemplateUsed`` and ``assertTemplateNotUsed`` as context manager
144+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
145+
146+It is now possible to check whether a template was used or not in a block of
147+code with the :meth:`~django.test.testcase.TestCase.assertTemplateUsed` and
148+:meth:`~django.test.testcase.TestCase.assertTemplateNotUsed` assertions. They
149+can be used as a context manager::
150+
151+    with self.assertTemplateUsed('index.html'):
152+        render_to_string('index.html')
153+    with self.assertTemplateNotUsed('base.html'):
154+        render_to_string('index.html')
155+
156+See the :ref:`assertion documentation<assertions>` for more information.
157+
158 Features deprecated in 1.4
159 ==========================
160 
161diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt
162index ea2c52b..f0f0b44 100644
163--- a/docs/topics/testing.txt
164+++ b/docs/topics/testing.txt
165@@ -1575,11 +1575,30 @@ your test suite.
166 
167     The name is a string such as ``'admin/index.html'``.
168 
169+    .. versionadded:: 1.4
170+
171+    You can also use this as a context manager. The code that is executed
172+    under the with statement is then observed instead of a response::
173+
174+        # This is necessary in Python 2.5 to enable the with statement, in 2.6
175+        # and up it is no longer necessary.
176+        from __future__ import with_statement
177+
178+        with self.assertTemplateUsed('index.html'):
179+            render_to_string('index.html')
180+        with self.assertTemplateUsed(template_name='index.html'):
181+            render_to_string('index.html')
182+
183 .. method:: TestCase.assertTemplateNotUsed(response, template_name, msg_prefix='')
184 
185     Asserts that the template with the given name was *not* used in rendering
186     the response.
187 
188+    .. versionadded:: 1.4
189+
190+    You can use this as a context manager in the same way as
191+    :func:`~TestCase.assertTemplateUsed`.
192+
193 .. method:: TestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='')
194 
195     Asserts that the response return a ``status_code`` redirect status, it
196diff --git a/tests/regressiontests/test_utils/templates/template_used/alternative.html b/tests/regressiontests/test_utils/templates/template_used/alternative.html
197new file mode 100644
198index 0000000..e69de29
199diff --git a/tests/regressiontests/test_utils/templates/template_used/base.html b/tests/regressiontests/test_utils/templates/template_used/base.html
200new file mode 100644
201index 0000000..e69de29
202diff --git a/tests/regressiontests/test_utils/templates/template_used/extends.html b/tests/regressiontests/test_utils/templates/template_used/extends.html
203new file mode 100644
204index 0000000..d14bfa2
205--- /dev/null
206+++ b/tests/regressiontests/test_utils/templates/template_used/extends.html
207@@ -0,0 +1 @@
208+{% extends "template_used/base.html" %}
209diff --git a/tests/regressiontests/test_utils/templates/template_used/include.html b/tests/regressiontests/test_utils/templates/template_used/include.html
210new file mode 100644
211index 0000000..2d6c954
212--- /dev/null
213+++ b/tests/regressiontests/test_utils/templates/template_used/include.html
214@@ -0,0 +1 @@
215+{% include "template_used/base.html" %}
216diff --git a/tests/regressiontests/test_utils/tests.py b/tests/regressiontests/test_utils/tests.py
217index eab6895..b578bff 100644
218--- a/tests/regressiontests/test_utils/tests.py
219+++ b/tests/regressiontests/test_utils/tests.py
220@@ -1,6 +1,7 @@
221 from __future__ import with_statement, absolute_import
222 
223 from django.forms import EmailField, IntegerField
224+from django.template.loader import render_to_string
225 from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
226 from django.utils.unittest import skip
227 
228@@ -88,6 +89,92 @@ class AssertNumQueriesContextManagerTests(TestCase):
229             self.client.get("/test_utils/get_person/%s/" % person.pk)
230 
231 
232+class AssertTemplateUsedContextManagerTests(TestCase):
233+    def test_usage(self):
234+        with self.assertTemplateUsed('template_used/base.html'):
235+            render_to_string('template_used/base.html')
236+
237+        with self.assertTemplateUsed(template_name='template_used/base.html'):
238+            render_to_string('template_used/base.html')
239+
240+        with self.assertTemplateUsed('template_used/base.html'):
241+            render_to_string('template_used/include.html')
242+
243+        with self.assertTemplateUsed('template_used/base.html'):
244+            render_to_string('template_used/extends.html')
245+
246+        with self.assertTemplateUsed('template_used/base.html'):
247+            render_to_string('template_used/base.html')
248+            render_to_string('template_used/base.html')
249+
250+    def test_nested_usage(self):
251+        with self.assertTemplateUsed('template_used/base.html'):
252+            with self.assertTemplateUsed('template_used/include.html'):
253+                render_to_string('template_used/include.html')
254+
255+        with self.assertTemplateUsed('template_used/extends.html'):
256+            with self.assertTemplateUsed('template_used/base.html'):
257+                render_to_string('template_used/extends.html')
258+
259+        with self.assertTemplateUsed('template_used/base.html'):
260+            with self.assertTemplateUsed('template_used/alternative.html'):
261+                render_to_string('template_used/alternative.html')
262+            render_to_string('template_used/base.html')
263+
264+        with self.assertTemplateUsed('template_used/base.html'):
265+            render_to_string('template_used/extends.html')
266+            with self.assertTemplateNotUsed('template_used/base.html'):
267+                render_to_string('template_used/alternative.html')
268+            render_to_string('template_used/base.html')
269+
270+    def test_not_used(self):
271+        with self.assertTemplateNotUsed('template_used/base.html'):
272+            pass
273+        with self.assertTemplateNotUsed('template_used/alternative.html'):
274+            pass
275+
276+    def test_error_message(self):
277+        try:
278+            with self.assertTemplateUsed('template_used/base.html'):
279+                pass
280+        except AssertionError, e:
281+            self.assertTrue('template_used/base.html' in e.message)
282+
283+        try:
284+            with self.assertTemplateUsed(template_name='template_used/base.html'):
285+                pass
286+        except AssertionError, e:
287+            self.assertTrue('template_used/base.html' in e.message)
288+
289+        try:
290+            with self.assertTemplateUsed('template_used/base.html'):
291+                render_to_string('template_used/alternative.html')
292+        except AssertionError, e:
293+            self.assertTrue('template_used/base.html' in e.message, e.message)
294+            self.assertTrue('template_used/alternative.html' in e.message, e.message)
295+
296+    def test_failure(self):
297+        with self.assertRaises(TypeError):
298+            with self.assertTemplateUsed():
299+                pass
300+
301+        with self.assertRaises(AssertionError):
302+            with self.assertTemplateUsed(''):
303+                pass
304+
305+        with self.assertRaises(AssertionError):
306+            with self.assertTemplateUsed(''):
307+                render_to_string('template_used/base.html')
308+
309+        with self.assertRaises(AssertionError):
310+            with self.assertTemplateUsed(template_name=''):
311+                pass
312+
313+        with self.assertRaises(AssertionError):
314+            with self.assertTemplateUsed('template_used/base.html'):
315+                render_to_string('template_used/alternative.html')
316+
317+
318 class SaveRestoreWarningState(TestCase):
319     def test_save_restore_warnings_state(self):
320         """