diff --git a/django/test/testcases.py b/django/test/testcases.py index 53ea02a4f09..2e011e4ceae 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -3,6 +3,7 @@ from __future__ import with_statement import os import re import sys +from copy import copy from functools import wraps from urlparse import urlsplit, urlunsplit from xml.dom.minidom import parseString, Node @@ -28,8 +29,10 @@ from django.forms.fields import CharField from django.http import QueryDict from django.test import _doctest as doctest from django.test.client import Client +from django.test.signals import template_rendered from django.test.utils import (get_warnings_state, restore_warnings_state, override_settings) +from django.test.utils import ContextList from django.utils import simplejson, unittest as ut2 from django.utils.encoding import smart_str, force_unicode from django.views.static import serve @@ -260,8 +263,53 @@ class _AssertNumQueriesContext(object): ) -class SimpleTestCase(ut2.TestCase): +class _AssertTemplateUsedContext(object): + def __init__(self, test_case, template_name): + self.test_case = test_case + self.template_name = template_name + self.rendered_templates = [] + self.rendered_template_names = [] + self.context = ContextList() + def on_template_render(self, sender, signal, template, context, **kwargs): + self.rendered_templates.append(template) + self.rendered_template_names.append(template.name) + self.context.append(copy(context)) + + def test(self): + return self.template_name in self.rendered_template_names + + def message(self): + return u'%s was not rendered.' % self.template_name + + def __enter__(self): + template_rendered.connect(self.on_template_render) + return self + + def __exit__(self, exc_type, exc_value, traceback): + template_rendered.disconnect(self.on_template_render) + if exc_type is not None: + return + + if not self.test(): + message = self.message() + if len(self.rendered_templates) == 0: + message += u' No template was rendered.' + else: + message += u' Following templates were rendered: %s' % ( + ', '.join(self.rendered_template_names)) + self.test_case.fail(message) + + +class _AssertTemplateNotUsedContext(_AssertTemplateUsedContext): + def test(self): + return self.template_name not in self.rendered_template_names + + def message(self): + return u'%s was rendered.' % self.template_name + + +class SimpleTestCase(ut2.TestCase): def save_warnings_state(self): """ Saves the state of the warnings module @@ -612,14 +660,25 @@ class TransactionTestCase(SimpleTestCase): self.fail(msg_prefix + "The form '%s' was not used to render the" " response" % form) - def assertTemplateUsed(self, response, template_name, msg_prefix=''): + def assertTemplateUsed(self, response=None, template_name=None, msg_prefix=''): """ Asserts that the template with the provided name was used in rendering - the response. + the response. Also useable as context manager. """ + if response is None and template_name is None: + raise TypeError(u'response and/or template_name argument must be provided') + if msg_prefix: msg_prefix += ": " + # use assertTemplateUsed as context manager + if not hasattr(response, 'templates') or (response is None and template_name): + if response: + template_name = response + response = None + context = _AssertTemplateUsedContext(self, template_name) + return context + template_names = [t.name for t in response.templates] if not template_names: self.fail(msg_prefix + "No templates used to render the response") @@ -628,14 +687,25 @@ class TransactionTestCase(SimpleTestCase): " the response. Actual template(s) used: %s" % (template_name, u', '.join(template_names))) - def assertTemplateNotUsed(self, response, template_name, msg_prefix=''): + def assertTemplateNotUsed(self, response=None, template_name=None, msg_prefix=''): """ Asserts that the template with the provided name was NOT used in - rendering the response. + rendering the response. Also useable as context manager. """ + if response is None and template_name is None: + raise TypeError(u'response and/or template_name argument must be provided') + if msg_prefix: msg_prefix += ": " + # use assertTemplateUsed as context manager + if not hasattr(response, 'templates') or (response is None and template_name): + if response: + template_name = response + response = None + context = _AssertTemplateNotUsedContext(self, template_name) + return context + template_names = [t.name for t in response.templates] self.assertFalse(template_name in template_names, msg_prefix + "Template '%s' was used unexpectedly in rendering" diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index 8a7bfb20117..f2c97f603a7 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -946,6 +946,21 @@ apply URL escaping again. This is wrong for URLs whose unquoted form contains a ``%xx`` sequence, but such URLs are very unlikely to happen in the wild, since they would confuse browsers too. +``assertTemplateUsed`` and ``assertTemplateNotUsed`` as context manager +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is now possible to check whether a template was used or not in a block of +code with the :meth:`~django.test.testcase.TestCase.assertTemplateUsed` and +:meth:`~django.test.testcase.TestCase.assertTemplateNotUsed` assertions. They +can be used as a context manager:: + + with self.assertTemplateUsed('index.html'): + render_to_string('index.html') + with self.assertTemplateNotUsed('base.html'): + render_to_string('index.html') + +See the :ref:`assertion documentation` for more information. + Database connections after running the test suite ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index ea2c52b097b..f0f0b445f9a 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -1575,11 +1575,30 @@ your test suite. The name is a string such as ``'admin/index.html'``. + .. versionadded:: 1.4 + + You can also use this as a context manager. The code that is executed + under the with statement is then observed instead of a response:: + + # This is necessary in Python 2.5 to enable the with statement, in 2.6 + # and up it is no longer necessary. + from __future__ import with_statement + + with self.assertTemplateUsed('index.html'): + render_to_string('index.html') + with self.assertTemplateUsed(template_name='index.html'): + render_to_string('index.html') + .. method:: TestCase.assertTemplateNotUsed(response, template_name, msg_prefix='') Asserts that the template with the given name was *not* used in rendering the response. + .. versionadded:: 1.4 + + You can use this as a context manager in the same way as + :func:`~TestCase.assertTemplateUsed`. + .. method:: TestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='') Asserts that the response return a ``status_code`` redirect status, it diff --git a/tests/regressiontests/test_utils/templates/template_used/alternative.html b/tests/regressiontests/test_utils/templates/template_used/alternative.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regressiontests/test_utils/templates/template_used/base.html b/tests/regressiontests/test_utils/templates/template_used/base.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/regressiontests/test_utils/templates/template_used/extends.html b/tests/regressiontests/test_utils/templates/template_used/extends.html new file mode 100644 index 00000000000..d14bfa27790 --- /dev/null +++ b/tests/regressiontests/test_utils/templates/template_used/extends.html @@ -0,0 +1 @@ +{% extends "template_used/base.html" %} diff --git a/tests/regressiontests/test_utils/templates/template_used/include.html b/tests/regressiontests/test_utils/templates/template_used/include.html new file mode 100644 index 00000000000..2d6c954f384 --- /dev/null +++ b/tests/regressiontests/test_utils/templates/template_used/include.html @@ -0,0 +1 @@ +{% include "template_used/base.html" %} diff --git a/tests/regressiontests/test_utils/tests.py b/tests/regressiontests/test_utils/tests.py index eab6895f0dd..b578bffc1a3 100644 --- a/tests/regressiontests/test_utils/tests.py +++ b/tests/regressiontests/test_utils/tests.py @@ -1,6 +1,7 @@ from __future__ import with_statement, absolute_import from django.forms import EmailField, IntegerField +from django.template.loader import render_to_string from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature from django.utils.unittest import skip @@ -88,6 +89,92 @@ class AssertNumQueriesContextManagerTests(TestCase): self.client.get("/test_utils/get_person/%s/" % person.pk) +class AssertTemplateUsedContextManagerTests(TestCase): + def test_usage(self): + with self.assertTemplateUsed('template_used/base.html'): + render_to_string('template_used/base.html') + + with self.assertTemplateUsed(template_name='template_used/base.html'): + render_to_string('template_used/base.html') + + with self.assertTemplateUsed('template_used/base.html'): + render_to_string('template_used/include.html') + + with self.assertTemplateUsed('template_used/base.html'): + render_to_string('template_used/extends.html') + + with self.assertTemplateUsed('template_used/base.html'): + render_to_string('template_used/base.html') + render_to_string('template_used/base.html') + + def test_nested_usage(self): + with self.assertTemplateUsed('template_used/base.html'): + with self.assertTemplateUsed('template_used/include.html'): + render_to_string('template_used/include.html') + + with self.assertTemplateUsed('template_used/extends.html'): + with self.assertTemplateUsed('template_used/base.html'): + render_to_string('template_used/extends.html') + + with self.assertTemplateUsed('template_used/base.html'): + with self.assertTemplateUsed('template_used/alternative.html'): + render_to_string('template_used/alternative.html') + render_to_string('template_used/base.html') + + with self.assertTemplateUsed('template_used/base.html'): + render_to_string('template_used/extends.html') + with self.assertTemplateNotUsed('template_used/base.html'): + render_to_string('template_used/alternative.html') + render_to_string('template_used/base.html') + + def test_not_used(self): + with self.assertTemplateNotUsed('template_used/base.html'): + pass + with self.assertTemplateNotUsed('template_used/alternative.html'): + pass + + def test_error_message(self): + try: + with self.assertTemplateUsed('template_used/base.html'): + pass + except AssertionError, e: + self.assertTrue('template_used/base.html' in e.message) + + try: + with self.assertTemplateUsed(template_name='template_used/base.html'): + pass + except AssertionError, e: + self.assertTrue('template_used/base.html' in e.message) + + try: + with self.assertTemplateUsed('template_used/base.html'): + render_to_string('template_used/alternative.html') + except AssertionError, e: + self.assertTrue('template_used/base.html' in e.message, e.message) + self.assertTrue('template_used/alternative.html' in e.message, e.message) + + def test_failure(self): + with self.assertRaises(TypeError): + with self.assertTemplateUsed(): + pass + + with self.assertRaises(AssertionError): + with self.assertTemplateUsed(''): + pass + + with self.assertRaises(AssertionError): + with self.assertTemplateUsed(''): + render_to_string('template_used/base.html') + + with self.assertRaises(AssertionError): + with self.assertTemplateUsed(template_name=''): + pass + + with self.assertRaises(AssertionError): + with self.assertTemplateUsed('template_used/base.html'): + render_to_string('template_used/alternative.html') + + class SaveRestoreWarningState(TestCase): def test_save_restore_warnings_state(self): """