diff --git a/django/test/testcases.py b/django/test/testcases.py index 6f8cbabd864..311b50cfb7c 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -231,6 +231,10 @@ class _AssertTemplateNotUsedContext(_AssertTemplateUsedContext): class SimpleTestCase(ut2.TestCase): + # The class we'll use for the test client self.client. + # Can be overridden in derived classes. + client_class = Client + _warn_txt = ("save_warnings_state/restore_warnings_state " "django.test.*TestCase methods are deprecated. Use Python's " "warnings.catch_warnings context manager instead.") @@ -264,10 +268,31 @@ class SimpleTestCase(ut2.TestCase): return def _pre_setup(self): - pass + """Performs any pre-test setup. This includes: + + * If the Test Case class has a 'urls' member, replace the + ROOT_URLCONF with it. + * Clearing the mail test outbox. + """ + self.client = self.client_class() + self._urlconf_setup() + mail.outbox = [] + + def _urlconf_setup(self): + set_urlconf(None) + if hasattr(self, 'urls'): + self._old_root_urlconf = settings.ROOT_URLCONF + settings.ROOT_URLCONF = self.urls + clear_url_caches() def _post_teardown(self): - pass + self._urlconf_teardown() + + def _urlconf_teardown(self): + set_urlconf(None) + if hasattr(self, '_old_root_urlconf'): + settings.ROOT_URLCONF = self._old_root_urlconf + clear_url_caches() def save_warnings_state(self): """ @@ -291,258 +316,6 @@ class SimpleTestCase(ut2.TestCase): """ return override_settings(**kwargs) - def assertRaisesMessage(self, expected_exception, expected_message, - callable_obj=None, *args, **kwargs): - """ - Asserts that the message in a raised exception matches the passed - value. - - Args: - expected_exception: Exception class expected to be raised. - expected_message: expected error message string value. - callable_obj: Function to be called. - args: Extra args. - kwargs: Extra kwargs. - """ - return six.assertRaisesRegex(self, expected_exception, - re.escape(expected_message), callable_obj, *args, **kwargs) - - def assertFieldOutput(self, fieldclass, valid, invalid, field_args=None, - field_kwargs=None, empty_value=''): - """ - Asserts that a form field behaves correctly with various inputs. - - Args: - fieldclass: the class of the field to be tested. - valid: a dictionary mapping valid inputs to their expected - cleaned values. - invalid: a dictionary mapping invalid inputs to one or more - raised error messages. - field_args: the args passed to instantiate the field - field_kwargs: the kwargs passed to instantiate the field - empty_value: the expected clean output for inputs in empty_values - - """ - if field_args is None: - field_args = [] - if field_kwargs is None: - field_kwargs = {} - required = fieldclass(*field_args, **field_kwargs) - optional = fieldclass(*field_args, - **dict(field_kwargs, required=False)) - # test valid inputs - for input, output in valid.items(): - self.assertEqual(required.clean(input), output) - self.assertEqual(optional.clean(input), output) - # test invalid inputs - for input, errors in invalid.items(): - with self.assertRaises(ValidationError) as context_manager: - required.clean(input) - self.assertEqual(context_manager.exception.messages, errors) - - with self.assertRaises(ValidationError) as context_manager: - optional.clean(input) - self.assertEqual(context_manager.exception.messages, errors) - # test required inputs - error_required = [force_text(required.error_messages['required'])] - for e in required.empty_values: - with self.assertRaises(ValidationError) as context_manager: - required.clean(e) - self.assertEqual(context_manager.exception.messages, - error_required) - self.assertEqual(optional.clean(e), empty_value) - # test that max_length and min_length are always accepted - if issubclass(fieldclass, CharField): - field_kwargs.update({'min_length':2, 'max_length':20}) - self.assertTrue(isinstance(fieldclass(*field_args, **field_kwargs), - fieldclass)) - - def assertHTMLEqual(self, html1, html2, msg=None): - """ - Asserts that two HTML snippets are semantically the same. - Whitespace in most cases is ignored, and attribute ordering is not - significant. The passed-in arguments must be valid HTML. - """ - dom1 = assert_and_parse_html(self, html1, msg, - 'First argument is not valid HTML:') - dom2 = assert_and_parse_html(self, html2, msg, - 'Second argument is not valid HTML:') - - if dom1 != dom2: - standardMsg = '%s != %s' % ( - safe_repr(dom1, True), safe_repr(dom2, True)) - diff = ('\n' + '\n'.join(difflib.ndiff( - six.text_type(dom1).splitlines(), - six.text_type(dom2).splitlines()))) - standardMsg = self._truncateMessage(standardMsg, diff) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertHTMLNotEqual(self, html1, html2, msg=None): - """Asserts that two HTML snippets are not semantically equivalent.""" - dom1 = assert_and_parse_html(self, html1, msg, - 'First argument is not valid HTML:') - dom2 = assert_and_parse_html(self, html2, msg, - 'Second argument is not valid HTML:') - - if dom1 == dom2: - standardMsg = '%s == %s' % ( - safe_repr(dom1, True), safe_repr(dom2, True)) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertInHTML(self, needle, haystack, count = None, msg_prefix=''): - needle = assert_and_parse_html(self, needle, None, - 'First argument is not valid HTML:') - haystack = assert_and_parse_html(self, haystack, None, - 'Second argument is not valid HTML:') - real_count = haystack.count(needle) - if count is not None: - self.assertEqual(real_count, count, - msg_prefix + "Found %d instances of '%s' in response" - " (expected %d)" % (real_count, needle, count)) - else: - self.assertTrue(real_count != 0, - msg_prefix + "Couldn't find '%s' in response" % needle) - - def assertJSONEqual(self, raw, expected_data, msg=None): - try: - data = json.loads(raw) - except ValueError: - self.fail("First argument is not valid JSON: %r" % raw) - if isinstance(expected_data, six.string_types): - try: - expected_data = json.loads(expected_data) - except ValueError: - self.fail("Second argument is not valid JSON: %r" % expected_data) - self.assertEqual(data, expected_data, msg=msg) - - def assertXMLEqual(self, xml1, xml2, msg=None): - """ - Asserts that two XML snippets are semantically the same. - Whitespace in most cases is ignored, and attribute ordering is not - significant. The passed-in arguments must be valid XML. - """ - try: - result = compare_xml(xml1, xml2) - except Exception as e: - standardMsg = 'First or second argument is not valid XML\n%s' % e - self.fail(self._formatMessage(msg, standardMsg)) - else: - if not result: - standardMsg = '%s != %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) - self.fail(self._formatMessage(msg, standardMsg)) - - def assertXMLNotEqual(self, xml1, xml2, msg=None): - """ - Asserts that two XML snippets are not semantically equivalent. - Whitespace in most cases is ignored, and attribute ordering is not - significant. The passed-in arguments must be valid XML. - """ - try: - result = compare_xml(xml1, xml2) - except Exception as e: - standardMsg = 'First or second argument is not valid XML\n%s' % e - self.fail(self._formatMessage(msg, standardMsg)) - else: - if result: - standardMsg = '%s == %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) - self.fail(self._formatMessage(msg, standardMsg)) - - -class TransactionTestCase(SimpleTestCase): - - # The class we'll use for the test client self.client. - # Can be overridden in derived classes. - client_class = Client - - # Subclasses can ask for resetting of auto increment sequence before each - # test case - reset_sequences = False - - def _pre_setup(self): - """Performs any pre-test setup. This includes: - - * Flushing the database. - * If the Test Case class has a 'fixtures' member, installing the - named fixtures. - * If the Test Case class has a 'urls' member, replace the - ROOT_URLCONF with it. - * Clearing the mail test outbox. - """ - self.client = self.client_class() - self._fixture_setup() - self._urlconf_setup() - mail.outbox = [] - - def _databases_names(self, include_mirrors=True): - # If the test case has a multi_db=True flag, act on all databases, - # including mirrors or not. Otherwise, just on the default DB. - if getattr(self, 'multi_db', False): - return [alias for alias in connections - if include_mirrors or not connections[alias].settings_dict['TEST_MIRROR']] - else: - return [DEFAULT_DB_ALIAS] - - def _reset_sequences(self, db_name): - conn = connections[db_name] - if conn.features.supports_sequence_reset: - sql_list = \ - conn.ops.sequence_reset_by_name_sql(no_style(), - conn.introspection.sequence_list()) - if sql_list: - with transaction.commit_on_success_unless_managed(using=db_name): - cursor = conn.cursor() - for sql in sql_list: - cursor.execute(sql) - - def _fixture_setup(self): - for db_name in self._databases_names(include_mirrors=False): - # Reset sequences - if self.reset_sequences: - self._reset_sequences(db_name) - - if hasattr(self, 'fixtures'): - # We have to use this slightly awkward syntax due to the fact - # that we're using *args and **kwargs together. - call_command('loaddata', *self.fixtures, - **{'verbosity': 0, 'database': db_name, 'skip_validation': True}) - - def _urlconf_setup(self): - set_urlconf(None) - if hasattr(self, 'urls'): - self._old_root_urlconf = settings.ROOT_URLCONF - settings.ROOT_URLCONF = self.urls - clear_url_caches() - - def _post_teardown(self): - """ Performs any post-test things. This includes: - - * Putting back the original ROOT_URLCONF if it was changed. - * Force closing the connection, so that the next test gets - a clean cursor. - """ - self._fixture_teardown() - self._urlconf_teardown() - # Some DB cursors include SQL statements as part of cursor - # creation. If you have a test that does rollback, the effect - # of these statements is lost, which can effect the operation - # of tests (e.g., losing a timezone setting causing objects to - # be created with the wrong time). - # To make sure this doesn't happen, get a clean connection at the - # start of every test. - for conn in connections.all(): - conn.close() - - def _fixture_teardown(self): - for db in self._databases_names(include_mirrors=False): - call_command('flush', verbosity=0, interactive=False, database=db, - skip_validation=True, reset_sequences=False) - - def _urlconf_teardown(self): - set_urlconf(None) - if hasattr(self, '_old_root_urlconf'): - settings.ROOT_URLCONF = self._old_root_urlconf - clear_url_caches() - def assertRedirects(self, response, expected_url, status_code=302, target_status_code=200, host=None, msg_prefix=''): """Asserts that a response redirected to a specific URL, and that the @@ -787,6 +560,236 @@ class TransactionTestCase(SimpleTestCase): msg_prefix + "Template '%s' was used unexpectedly in rendering" " the response" % template_name) + def assertRaisesMessage(self, expected_exception, expected_message, + callable_obj=None, *args, **kwargs): + """ + Asserts that the message in a raised exception matches the passed + value. + + Args: + expected_exception: Exception class expected to be raised. + expected_message: expected error message string value. + callable_obj: Function to be called. + args: Extra args. + kwargs: Extra kwargs. + """ + return six.assertRaisesRegex(self, expected_exception, + re.escape(expected_message), callable_obj, *args, **kwargs) + + def assertFieldOutput(self, fieldclass, valid, invalid, field_args=None, + field_kwargs=None, empty_value=''): + """ + Asserts that a form field behaves correctly with various inputs. + + Args: + fieldclass: the class of the field to be tested. + valid: a dictionary mapping valid inputs to their expected + cleaned values. + invalid: a dictionary mapping invalid inputs to one or more + raised error messages. + field_args: the args passed to instantiate the field + field_kwargs: the kwargs passed to instantiate the field + empty_value: the expected clean output for inputs in empty_values + + """ + if field_args is None: + field_args = [] + if field_kwargs is None: + field_kwargs = {} + required = fieldclass(*field_args, **field_kwargs) + optional = fieldclass(*field_args, + **dict(field_kwargs, required=False)) + # test valid inputs + for input, output in valid.items(): + self.assertEqual(required.clean(input), output) + self.assertEqual(optional.clean(input), output) + # test invalid inputs + for input, errors in invalid.items(): + with self.assertRaises(ValidationError) as context_manager: + required.clean(input) + self.assertEqual(context_manager.exception.messages, errors) + + with self.assertRaises(ValidationError) as context_manager: + optional.clean(input) + self.assertEqual(context_manager.exception.messages, errors) + # test required inputs + error_required = [force_text(required.error_messages['required'])] + for e in required.empty_values: + with self.assertRaises(ValidationError) as context_manager: + required.clean(e) + self.assertEqual(context_manager.exception.messages, + error_required) + self.assertEqual(optional.clean(e), empty_value) + # test that max_length and min_length are always accepted + if issubclass(fieldclass, CharField): + field_kwargs.update({'min_length':2, 'max_length':20}) + self.assertTrue(isinstance(fieldclass(*field_args, **field_kwargs), + fieldclass)) + + def assertHTMLEqual(self, html1, html2, msg=None): + """ + Asserts that two HTML snippets are semantically the same. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid HTML. + """ + dom1 = assert_and_parse_html(self, html1, msg, + 'First argument is not valid HTML:') + dom2 = assert_and_parse_html(self, html2, msg, + 'Second argument is not valid HTML:') + + if dom1 != dom2: + standardMsg = '%s != %s' % ( + safe_repr(dom1, True), safe_repr(dom2, True)) + diff = ('\n' + '\n'.join(difflib.ndiff( + six.text_type(dom1).splitlines(), + six.text_type(dom2).splitlines()))) + standardMsg = self._truncateMessage(standardMsg, diff) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertHTMLNotEqual(self, html1, html2, msg=None): + """Asserts that two HTML snippets are not semantically equivalent.""" + dom1 = assert_and_parse_html(self, html1, msg, + 'First argument is not valid HTML:') + dom2 = assert_and_parse_html(self, html2, msg, + 'Second argument is not valid HTML:') + + if dom1 == dom2: + standardMsg = '%s == %s' % ( + safe_repr(dom1, True), safe_repr(dom2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertInHTML(self, needle, haystack, count=None, msg_prefix=''): + needle = assert_and_parse_html(self, needle, None, + 'First argument is not valid HTML:') + haystack = assert_and_parse_html(self, haystack, None, + 'Second argument is not valid HTML:') + real_count = haystack.count(needle) + if count is not None: + self.assertEqual(real_count, count, + msg_prefix + "Found %d instances of '%s' in response" + " (expected %d)" % (real_count, needle, count)) + else: + self.assertTrue(real_count != 0, + msg_prefix + "Couldn't find '%s' in response" % needle) + + def assertJSONEqual(self, raw, expected_data, msg=None): + try: + data = json.loads(raw) + except ValueError: + self.fail("First argument is not valid JSON: %r" % raw) + if isinstance(expected_data, six.string_types): + try: + expected_data = json.loads(expected_data) + except ValueError: + self.fail("Second argument is not valid JSON: %r" % expected_data) + self.assertEqual(data, expected_data, msg=msg) + + def assertXMLEqual(self, xml1, xml2, msg=None): + """ + Asserts that two XML snippets are semantically the same. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid XML. + """ + try: + result = compare_xml(xml1, xml2) + except Exception as e: + standardMsg = 'First or second argument is not valid XML\n%s' % e + self.fail(self._formatMessage(msg, standardMsg)) + else: + if not result: + standardMsg = '%s != %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + def assertXMLNotEqual(self, xml1, xml2, msg=None): + """ + Asserts that two XML snippets are not semantically equivalent. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid XML. + """ + try: + result = compare_xml(xml1, xml2) + except Exception as e: + standardMsg = 'First or second argument is not valid XML\n%s' % e + self.fail(self._formatMessage(msg, standardMsg)) + else: + if result: + standardMsg = '%s == %s' % (safe_repr(xml1, True), safe_repr(xml2, True)) + self.fail(self._formatMessage(msg, standardMsg)) + + +class TransactionTestCase(SimpleTestCase): + + # Subclasses can ask for resetting of auto increment sequence before each + # test case + reset_sequences = False + + def _pre_setup(self): + """Performs any pre-test setup. This includes: + + * Flushing the database. + * If the Test Case class has a 'fixtures' member, installing the + named fixtures. + """ + super(TransactionTestCase, self)._pre_setup() + self._fixture_setup() + + def _databases_names(self, include_mirrors=True): + # If the test case has a multi_db=True flag, act on all databases, + # including mirrors or not. Otherwise, just on the default DB. + if getattr(self, 'multi_db', False): + return [alias for alias in connections + if include_mirrors or not connections[alias].settings_dict['TEST_MIRROR']] + else: + return [DEFAULT_DB_ALIAS] + + def _reset_sequences(self, db_name): + conn = connections[db_name] + if conn.features.supports_sequence_reset: + sql_list = \ + conn.ops.sequence_reset_by_name_sql(no_style(), + conn.introspection.sequence_list()) + if sql_list: + with transaction.commit_on_success_unless_managed(using=db_name): + cursor = conn.cursor() + for sql in sql_list: + cursor.execute(sql) + + def _fixture_setup(self): + for db_name in self._databases_names(include_mirrors=False): + # Reset sequences + if self.reset_sequences: + self._reset_sequences(db_name) + + if hasattr(self, 'fixtures'): + # We have to use this slightly awkward syntax due to the fact + # that we're using *args and **kwargs together. + call_command('loaddata', *self.fixtures, + **{'verbosity': 0, 'database': db_name, 'skip_validation': True}) + + def _post_teardown(self): + """Performs any post-test things. This includes: + + * Putting back the original ROOT_URLCONF if it was changed. + * Force closing the connection, so that the next test gets + a clean cursor. + """ + self._fixture_teardown() + super(TransactionTestCase, self)._post_teardown() + # Some DB cursors include SQL statements as part of cursor + # creation. If you have a test that does rollback, the effect + # of these statements is lost, which can effect the operation + # of tests (e.g., losing a timezone setting causing objects to + # be created with the wrong time). + # To make sure this doesn't happen, get a clean connection at the + # start of every test. + for conn in connections.all(): + conn.close() + + def _fixture_teardown(self): + for db_name in self._databases_names(include_mirrors=False): + call_command('flush', verbosity=0, interactive=False, database=db_name, + skip_validation=True, reset_sequences=False) + def assertQuerysetEqual(self, qs, values, transform=repr, ordered=True): items = six.moves.map(transform, qs) if not ordered: @@ -841,14 +844,14 @@ class TestCase(TransactionTestCase): # Remove this when the legacy transaction management goes away. disable_transaction_methods() - for db in self._databases_names(include_mirrors=False): + for db_name in self._databases_names(include_mirrors=False): if hasattr(self, 'fixtures'): try: call_command('loaddata', *self.fixtures, **{ 'verbosity': 0, 'commit': False, - 'database': db, + 'database': db_name, 'skip_validation': True, }) except Exception: diff --git a/docs/intro/tutorial05.txt b/docs/intro/tutorial05.txt index a276763d672..39c3785f7c5 100644 --- a/docs/intro/tutorial05.txt +++ b/docs/intro/tutorial05.txt @@ -503,8 +503,8 @@ of the process of creating polls. message: "No polls are available." and verifies the ``latest_poll_list`` is empty. Note that the :class:`django.test.TestCase` class provides some additional assertion methods. In these examples, we use -:meth:`~django.test.TestCase.assertContains()` and -:meth:`~django.test.TestCase.assertQuerysetEqual()`. +:meth:`~django.test.SimpleTestCase.assertContains()` and +:meth:`~django.test.TransactionTestCase.assertQuerysetEqual()`. In ``test_index_view_with_a_past_poll``, we create a poll and verify that it appears in the list. diff --git a/docs/ref/contrib/contenttypes.txt b/docs/ref/contrib/contenttypes.txt index 4fa119bc709..1bb0802442d 100644 --- a/docs/ref/contrib/contenttypes.txt +++ b/docs/ref/contrib/contenttypes.txt @@ -329,7 +329,7 @@ model: .. admonition:: Serializing references to ``ContentType`` objects If you're serializing data (for example, when generating - :class:`~django.test.TestCase.fixtures`) from a model that implements + :class:`~django.test.TransactionTestCase.fixtures`) from a model that implements generic relations, you should probably be using a natural key to uniquely identify related :class:`~django.contrib.contenttypes.models.ContentType` objects. See :ref:`natural keys` and diff --git a/docs/releases/1.3-alpha-1.txt b/docs/releases/1.3-alpha-1.txt index 42947d9a44b..634e6afaf2d 100644 --- a/docs/releases/1.3-alpha-1.txt +++ b/docs/releases/1.3-alpha-1.txt @@ -154,7 +154,7 @@ requests. These include: requests in tests. * A new test assertion -- - :meth:`~django.test.TestCase.assertNumQueries` -- making it + :meth:`~django.test.TransactionTestCase.assertNumQueries` -- making it easier to test the database activity associated with a view. diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index 89cece941b0..45ebb2f1fee 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -299,7 +299,7 @@ requests. These include: in tests. * A new test assertion -- - :meth:`~django.test.TestCase.assertNumQueries` -- making it + :meth:`~django.test.TransactionTestCase.assertNumQueries` -- making it easier to test the database activity associated with a view. * Support for lookups spanning relations in admin's diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt index 83a5f54fc71..a013665ad34 100644 --- a/docs/releases/1.4.txt +++ b/docs/releases/1.4.txt @@ -541,8 +541,8 @@ compare HTML directly with the new :meth:`~django.test.SimpleTestCase.assertHTMLEqual` and :meth:`~django.test.SimpleTestCase.assertHTMLNotEqual` assertions, or use the ``html=True`` flag with -:meth:`~django.test.TestCase.assertContains` and -:meth:`~django.test.TestCase.assertNotContains` to test whether the +:meth:`~django.test.SimpleTestCase.assertContains` and +:meth:`~django.test.SimpleTestCase.assertNotContains` to test whether the client's response contains a given HTML fragment. See the :ref:`assertions documentation ` for more. @@ -1093,8 +1093,8 @@ wild, because they would confuse browsers too. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It's now possible to check whether a template was used within a block of -code with :meth:`~django.test.TestCase.assertTemplateUsed` and -:meth:`~django.test.TestCase.assertTemplateNotUsed`. And they +code with :meth:`~django.test.SimpleTestCase.assertTemplateUsed` and +:meth:`~django.test.SimpleTestCase.assertTemplateNotUsed`. And they can be used as a context manager:: with self.assertTemplateUsed('index.html'): diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index f8e1fd6339f..0eab8540b05 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -271,9 +271,10 @@ The changes in transaction management may result in additional statements to create, release or rollback savepoints. This is more likely to happen with SQLite, since it didn't support savepoints until this release. -If tests using :meth:`~django.test.TestCase.assertNumQueries` fail because of -a higher number of queries than expected, check that the extra queries are -related to savepoints, and adjust the expected number of queries accordingly. +If tests using :meth:`~django.test.TransactionTestCase.assertNumQueries` fail +because of a higher number of queries than expected, check that the extra +queries are related to savepoints, and adjust the expected number of queries +accordingly. Autocommit option for PostgreSQL ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/python3.txt b/docs/topics/python3.txt index 22e609c75cb..9a0438e9e59 100644 --- a/docs/topics/python3.txt +++ b/docs/topics/python3.txt @@ -201,8 +201,8 @@ According to :pep:`3333`: Specifically, :attr:`HttpResponse.content ` contains ``bytes``, which may become an issue if you compare it with a ``str`` in your tests. The preferred solution is to rely on -:meth:`~django.test.TestCase.assertContains` and -:meth:`~django.test.TestCase.assertNotContains`. These methods accept a +:meth:`~django.test.SimpleTestCase.assertContains` and +:meth:`~django.test.SimpleTestCase.assertNotContains`. These methods accept a response and a unicode string as arguments. Coding guidelines diff --git a/docs/topics/testing/overview.txt b/docs/topics/testing/overview.txt index fc2b3938984..d543099ae69 100644 --- a/docs/topics/testing/overview.txt +++ b/docs/topics/testing/overview.txt @@ -21,17 +21,16 @@ module defines tests using a class-based approach. .. admonition:: unittest2 - Python 2.7 introduced some major changes to the unittest library, + Python 2.7 introduced some major changes to the ``unittest`` library, adding some extremely useful features. To ensure that every Django project can benefit from these new features, Django ships with a - copy of unittest2_, a copy of the Python 2.7 unittest library, - backported for Python 2.6 compatibility. + copy of unittest2_, a copy of Python 2.7's ``unittest``, backported for + Python 2.6 compatibility. To access this library, Django provides the ``django.utils.unittest`` module alias. If you are using Python - 2.7, or you have installed unittest2 locally, Django will map the - alias to the installed version of the unittest library. Otherwise, - Django will use its own bundled version of unittest2. + 2.7, or you have installed ``unittest2`` locally, Django will map the alias + to it. Otherwise, Django will use its own bundled version of ``unittest2``. To use this alias, simply use:: @@ -41,8 +40,8 @@ module defines tests using a class-based approach. import unittest - If you want to continue to use the base unittest library, you can -- - you just won't get any of the nice new unittest2 features. + If you want to continue to use the legacy ``unittest`` library, you can -- + you just won't get any of the nice new ``unittest2`` features. .. _unittest2: http://pypi.python.org/pypi/unittest2 @@ -858,24 +857,46 @@ SimpleTestCase .. class:: SimpleTestCase() -A very thin subclass of :class:`unittest.TestCase`, it extends it with some -basic functionality like: +A thin subclass of :class:`unittest.TestCase`, it extends it with some basic +functionality like: * Saving and restoring the Python warning machinery state. -* Checking that a callable :meth:`raises a certain exception `. -* :meth:`Testing form field rendering `. -* Testing server :ref:`HTML responses for the presence/lack of a given fragment `. -* The ability to run tests with :ref:`modified settings ` +* Some useful assertions like: + + * Checking that a callable :meth:`raises a certain exception + `. + * Testing form field :meth:`rendering and error treatment + `. + * Testing :meth:`HTML responses for the presence/lack of a given fragment + `. + * Verifying that a template :meth:`has/hasn't been used to generate a given + response content `. + * Verifying a HTTP :meth:`redirect ` is + performed by the app. + * Robustly testing two :meth:`HTML fragments ` + for equality/inequality or :meth:`containment `. + * Robustly testing two :meth:`XML fragments ` + for equality/inequality. + * Robustly testing two :meth:`JSON fragments ` + for equality. + +* The ability to run tests with :ref:`modified settings `. +* Using the :attr:`~SimpleTestCase.client` :class:`~django.test.client.Client`. +* Custom test-time :attr:`URL maps `. + +.. versionchanged:: 1.6 + + The latter two features were moved from ``TransactionTestCase`` to + ``SimpleTestCase`` in Django 1.6. If you need any of the other more complex and heavyweight Django-specific features like: -* Using the :attr:`~TestCase.client` :class:`~django.test.client.Client`. * Testing or using the ORM. -* Database :attr:`~TestCase.fixtures`. -* Custom test-time :attr:`URL maps `. +* Database :attr:`~TransactionTestCase.fixtures`. * Test :ref:`skipping based on database backend features `. -* The remaining specialized :ref:`assert* ` methods. +* The remaining specialized :meth:`assert* + ` methods. then you should use :class:`~django.test.TransactionTestCase` or :class:`~django.test.TestCase` instead. @@ -1137,9 +1158,9 @@ Test cases features Default test client ~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.client +.. attribute:: SimpleTestCase.client -Every test case in a ``django.test.TestCase`` instance has access to an +Every test case in a ``django.test.*TestCase`` instance has access to an instance of a Django test client. This client can be accessed as ``self.client``. This client is recreated for each test, so you don't have to worry about state (such as cookies) carrying over from one test to another. @@ -1176,10 +1197,10 @@ This means, instead of instantiating a ``Client`` in each test:: Customizing the test client ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.client_class +.. attribute:: SimpleTestCase.client_class If you want to use a different ``Client`` class (for example, a subclass -with customized behavior), use the :attr:`~TestCase.client_class` class +with customized behavior), use the :attr:`~SimpleTestCase.client_class` class attribute:: from django.test import TestCase @@ -1200,11 +1221,12 @@ attribute:: Fixture loading ~~~~~~~~~~~~~~~ -.. attribute:: TestCase.fixtures +.. attribute:: TransactionTestCase.fixtures A test case for a database-backed Web site isn't much use if there isn't any data in the database. To make it easy to put test data into the database, -Django's custom ``TestCase`` class provides a way of loading **fixtures**. +Django's custom ``TransactionTestCase`` class provides a way of loading +**fixtures**. A fixture is a collection of data that Django knows how to import into a database. For example, if your site has user accounts, you might set up a @@ -1273,7 +1295,7 @@ or by the order of test execution. URLconf configuration ~~~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.urls +.. attribute:: SimpleTestCase.urls If your application provides views, you may want to include tests that use the test client to exercise those views. However, an end user is free to deploy the @@ -1282,9 +1304,9 @@ tests can't rely upon the fact that your views will be available at a particular URL. In order to provide a reliable URL space for your test, -``django.test.TestCase`` provides the ability to customize the URLconf +``django.test.*TestCase`` classes provide the ability to customize the URLconf configuration for the duration of the execution of a test suite. If your -``TestCase`` instance defines an ``urls`` attribute, the ``TestCase`` will use +``*TestCase`` instance defines an ``urls`` attribute, the ``*TestCase`` will use the value of that attribute as the :setting:`ROOT_URLCONF` for the duration of that test. @@ -1307,7 +1329,7 @@ URLconf for the duration of the test case. Multi-database support ~~~~~~~~~~~~~~~~~~~~~~ -.. attribute:: TestCase.multi_db +.. attribute:: TransactionTestCase.multi_db Django sets up a test database corresponding to every database that is defined in the :setting:`DATABASES` definition in your settings @@ -1340,12 +1362,12 @@ This test case will flush *all* the test databases before running Overriding settings ~~~~~~~~~~~~~~~~~~~ -.. method:: TestCase.settings +.. method:: SimpleTestCase.settings For testing purposes it's often useful to change a setting temporarily and revert to the original value after running the testing code. For this use case Django provides a standard Python context manager (see :pep:`343`) -:meth:`~django.test.TestCase.settings`, which can be used like this:: +:meth:`~django.test.SimpleTestCase.settings`, which can be used like this:: from django.test import TestCase @@ -1435,8 +1457,8 @@ MEDIA_ROOT, DEFAULT_FILE_STORAGE Default file storage Emptying the test outbox ~~~~~~~~~~~~~~~~~~~~~~~~ -If you use Django's custom ``TestCase`` class, the test runner will clear the -contents of the test email outbox at the start of each test case. +If you use any of Django's custom ``TestCase`` classes, the test runner will +clear thecontents of the test email outbox at the start of each test case. For more detail on email services during tests, see `Email services`_ below. @@ -1486,31 +1508,7 @@ your test suite. self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': [u'Enter a valid email address.']}) - -.. method:: TestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False) - - Asserts that a ``Response`` instance produced the given ``status_code`` and - that ``text`` appears in the content of the response. If ``count`` is - provided, ``text`` must occur exactly ``count`` times in the response. - - Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with - the response content will be based on HTML semantics instead of - character-by-character equality. Whitespace is ignored in most cases, - attribute ordering is not significant. See - :meth:`~SimpleTestCase.assertHTMLEqual` for more details. - -.. method:: TestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False) - - Asserts that a ``Response`` instance produced the given ``status_code`` and - that ``text`` does not appears in the content of the response. - - Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with - the response content will be based on HTML semantics instead of - character-by-character equality. Whitespace is ignored in most cases, - attribute ordering is not significant. See - :meth:`~SimpleTestCase.assertHTMLEqual` for more details. - -.. method:: TestCase.assertFormError(response, form, field, errors, msg_prefix='') +.. method:: SimpleTestCase.assertFormError(response, form, field, errors, msg_prefix='') Asserts that a field on a form raises the provided list of errors when rendered on the form. @@ -1525,7 +1523,30 @@ your test suite. ``errors`` is an error string, or a list of error strings, that are expected as a result of form validation. -.. method:: TestCase.assertTemplateUsed(response, template_name, msg_prefix='') +.. method:: SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False) + + Asserts that a ``Response`` instance produced the given ``status_code`` and + that ``text`` appears in the content of the response. If ``count`` is + provided, ``text`` must occur exactly ``count`` times in the response. + + Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with + the response content will be based on HTML semantics instead of + character-by-character equality. Whitespace is ignored in most cases, + attribute ordering is not significant. See + :meth:`~SimpleTestCase.assertHTMLEqual` for more details. + +.. method:: SimpleTestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False) + + Asserts that a ``Response`` instance produced the given ``status_code`` and + that ``text`` does not appears in the content of the response. + + Set ``html`` to ``True`` to handle ``text`` as HTML. The comparison with + the response content will be based on HTML semantics instead of + character-by-character equality. Whitespace is ignored in most cases, + attribute ordering is not significant. See + :meth:`~SimpleTestCase.assertHTMLEqual` for more details. + +.. method:: SimpleTestCase.assertTemplateUsed(response, template_name, msg_prefix='') Asserts that the template with the given name was used in rendering the response. @@ -1539,15 +1560,15 @@ your test suite. with self.assertTemplateUsed(template_name='index.html'): render_to_string('index.html') -.. method:: TestCase.assertTemplateNotUsed(response, template_name, msg_prefix='') +.. method:: SimpleTestCase.assertTemplateNotUsed(response, template_name, msg_prefix='') Asserts that the template with the given name was *not* used in rendering the response. You can use this as a context manager in the same way as - :meth:`~TestCase.assertTemplateUsed`. + :meth:`~SimpleTestCase.assertTemplateUsed`. -.. method:: TestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='') +.. method:: SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='') Asserts that the response return a ``status_code`` redirect status, it redirected to ``expected_url`` (including any GET data), and the final @@ -1557,44 +1578,6 @@ your test suite. ``target_status_code`` will be the url and status code for the final point of the redirect chain. -.. method:: TestCase.assertQuerysetEqual(qs, values, transform=repr, ordered=True) - - Asserts that a queryset ``qs`` returns a particular list of values ``values``. - - The comparison of the contents of ``qs`` and ``values`` is performed using - the function ``transform``; by default, this means that the ``repr()`` of - each value is compared. Any other callable can be used if ``repr()`` doesn't - provide a unique or helpful comparison. - - By default, the comparison is also ordering dependent. If ``qs`` doesn't - provide an implicit ordering, you can set the ``ordered`` parameter to - ``False``, which turns the comparison into a Python set comparison. - - .. versionchanged:: 1.6 - - The method now checks for undefined order and raises ``ValueError`` - if undefined order is spotted. The ordering is seen as undefined if - the given ``qs`` isn't ordered and the comparison is against more - than one ordered values. - -.. method:: TestCase.assertNumQueries(num, func, *args, **kwargs) - - Asserts that when ``func`` is called with ``*args`` and ``**kwargs`` that - ``num`` database queries are executed. - - If a ``"using"`` key is present in ``kwargs`` it is used as the database - alias for which to check the number of queries. If you wish to call a - function with a ``using`` parameter you can do it by wrapping the call with - a ``lambda`` to add an extra parameter:: - - self.assertNumQueries(7, lambda: my_function(using=7)) - - You can also use this as a context manager:: - - with self.assertNumQueries(2): - Person.objects.create(name="Aaron") - Person.objects.create(name="Daniel") - .. method:: SimpleTestCase.assertHTMLEqual(html1, html2, msg=None) Asserts that the strings ``html1`` and ``html2`` are equal. The comparison @@ -1624,6 +1607,8 @@ your test suite. ``html1`` and ``html2`` must be valid HTML. An ``AssertionError`` will be raised if one of them cannot be parsed. + Output in case of error can be customized with the ``msg`` argument. + .. method:: SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None) Asserts that the strings ``html1`` and ``html2`` are *not* equal. The @@ -1633,6 +1618,8 @@ your test suite. ``html1`` and ``html2`` must be valid HTML. An ``AssertionError`` will be raised if one of them cannot be parsed. + Output in case of error can be customized with the ``msg`` argument. + .. method:: SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None) .. versionadded:: 1.5 @@ -1644,6 +1631,8 @@ your test suite. syntax differences. When unvalid XML is passed in any parameter, an ``AssertionError`` is always raised, even if both string are identical. + Output in case of error can be customized with the ``msg`` argument. + .. method:: SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None) .. versionadded:: 1.5 @@ -1652,6 +1641,68 @@ your test suite. comparison is based on XML semantics. See :meth:`~SimpleTestCase.assertXMLEqual` for details. + Output in case of error can be customized with the ``msg`` argument. + +.. method:: SimpleTestCase.assertInHTML(needle, haystack, count=None, msg_prefix='') + + .. versionadded:: 1.5 + + Asserts that the HTML fragment ``needle`` is contained in the ``haystack`` one. + + If the ``count`` integer argument is specified, then additionally the number + of ``needle`` occurrences will be strictly verified. + + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid HTML. + +.. method:: SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None) + + .. versionadded:: 1.5 + + Asserts that the JSON fragments ``raw`` and ``expected_data`` are equal. + Usual JSON non-significant whitespace rules apply as the heavyweight is + delegated to the :mod:`json` library. + + Output in case of error can be customized with the ``msg`` argument. + +.. method:: TransactionTestCase.assertQuerysetEqual(qs, values, transform=repr, ordered=True) + + Asserts that a queryset ``qs`` returns a particular list of values ``values``. + + The comparison of the contents of ``qs`` and ``values`` is performed using + the function ``transform``; by default, this means that the ``repr()`` of + each value is compared. Any other callable can be used if ``repr()`` doesn't + provide a unique or helpful comparison. + + By default, the comparison is also ordering dependent. If ``qs`` doesn't + provide an implicit ordering, you can set the ``ordered`` parameter to + ``False``, which turns the comparison into a Python set comparison. + + .. versionchanged:: 1.6 + + The method now checks for undefined order and raises ``ValueError`` + if undefined order is spotted. The ordering is seen as undefined if + the given ``qs`` isn't ordered and the comparison is against more + than one ordered values. + +.. method:: TransactionTestCase.assertNumQueries(num, func, *args, **kwargs) + + Asserts that when ``func`` is called with ``*args`` and ``**kwargs`` that + ``num`` database queries are executed. + + If a ``"using"`` key is present in ``kwargs`` it is used as the database + alias for which to check the number of queries. If you wish to call a + function with a ``using`` parameter you can do it by wrapping the call with + a ``lambda`` to add an extra parameter:: + + self.assertNumQueries(7, lambda: my_function(using=7)) + + You can also use this as a context manager:: + + with self.assertNumQueries(2): + Person.objects.create(name="Aaron") + Person.objects.create(name="Daniel") + .. _topics-testing-email: Email services @@ -1701,7 +1752,7 @@ and contents:: self.assertEqual(mail.outbox[0].subject, 'Subject here') As noted :ref:`previously `, the test outbox is emptied -at the start of every test in a Django ``TestCase``. To empty the outbox +at the start of every test in a Django ``*TestCase``. To empty the outbox manually, assign the empty list to ``mail.outbox``:: from django.core import mail