From f2be59849c6e6adc654edd7ea27890c1719738cb Mon Sep 17 00:00:00 2001 From: Kevin Kubasik Date: Wed, 1 Jul 2009 14:24:25 +0000 Subject: [PATCH] [gsoc2009-testing] Added support for skipping tests that cannot pass. Add auth to regression suite urls.py so reverse() works. git-svn-id: http://code.djangoproject.com/svn/django/branches/soc2009/test-improvements@11142 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/auth/tests/views.py | 23 +++-- django/test/__init__.py | 7 ++ django/test/decorators.py | 44 +++++++++ django/test/simple.py | 90 ++++++++++++++++++- docs/topics/testing.txt | 52 +++++++++++ tests/regressiontests/admin_views/urls.py | 2 +- .../regressiontests/test_decorators/tests.py | 23 +++++ tests/urls.py | 1 + 8 files changed, 233 insertions(+), 9 deletions(-) create mode 100644 django/test/decorators.py create mode 100644 tests/regressiontests/test_decorators/tests.py diff --git a/django/contrib/auth/tests/views.py b/django/contrib/auth/tests/views.py index 532f92b523..d7915fd51d 100644 --- a/django/contrib/auth/tests/views.py +++ b/django/contrib/auth/tests/views.py @@ -9,6 +9,7 @@ from django.contrib.auth.models import User from django.test import TestCase from django.core import mail from django.core.urlresolvers import reverse +from django.test.decorators import views_required class AuthViewsTestCase(TestCase): """ @@ -49,22 +50,26 @@ class PasswordResetTest(AuthViewsTestCase): def test_email_not_found(self): "Error is raised if the provided email address isn't currently registered" - response = self.client.get('/password_reset/') + response = self.client.get(reverse('django.contrib.auth.views.password_reset')) self.assertEquals(response.status_code, 200) - response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'}) + response = self.client.post(reverse('django.contrib.auth.views.password_reset'), {'email': 'not_a_real_email@email.com'}) self.assertContains(response, "That e-mail address doesn't have an associated user account") self.assertEquals(len(mail.outbox), 0) - + + test_email_not_found = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_email_not_found) + def test_email_found(self): "Email is sent if a valid email address is provided for password reset" - response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) + response = self.client.post(reverse('django.contrib.auth.views.password_reset'), {'email': 'staffmember@example.com'}) self.assertEquals(response.status_code, 302) self.assertEquals(len(mail.outbox), 1) self.assert_("http://" in mail.outbox[0].body) + + test_email_found = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_email_found) def _test_confirm_start(self): # Start by creating the email - response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'}) + response = self.client.post(reverse('django.contrib.auth.views.password_reset'), {'email': 'staffmember@example.com'}) self.assertEquals(response.status_code, 302) self.assertEquals(len(mail.outbox), 1) return self._read_signup_email(mail.outbox[0]) @@ -80,6 +85,8 @@ class PasswordResetTest(AuthViewsTestCase): # redirect to a 'complete' page: self.assertEquals(response.status_code, 200) self.assert_("Please enter your new password" in response.content) + test_confirm_valid = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_confirm_valid) + def test_confirm_invalid(self): url, path = self._test_confirm_start() @@ -90,6 +97,7 @@ class PasswordResetTest(AuthViewsTestCase): response = self.client.get(path) self.assertEquals(response.status_code, 200) self.assert_("The password reset link was invalid" in response.content) + test_confirm_invalid = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_confirm_invalid) def test_confirm_invalid_post(self): # Same as test_confirm_invalid, but trying @@ -102,6 +110,7 @@ class PasswordResetTest(AuthViewsTestCase): # Check the password has not been changed u = User.objects.get(email='staffmember@example.com') self.assert_(not u.check_password("anewpassword")) + test_confirm_invalid_post = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_confirm_invalid_post) def test_confirm_complete(self): url, path = self._test_confirm_start() @@ -117,6 +126,7 @@ class PasswordResetTest(AuthViewsTestCase): response = self.client.get(path) self.assertEquals(response.status_code, 200) self.assert_("The password reset link was invalid" in response.content) + test_confirm_complete = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_confirm_complete) def test_confirm_different_passwords(self): url, path = self._test_confirm_start() @@ -124,7 +134,8 @@ class PasswordResetTest(AuthViewsTestCase): 'new_password2':' x'}) self.assertEquals(response.status_code, 200) self.assert_("The two password fields didn't match" in response.content) - + + test_confirm_different_passwords = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_confirm_different_passwords) class ChangePasswordTest(AuthViewsTestCase): def login(self, password='password'): diff --git a/django/test/__init__.py b/django/test/__init__.py index 957b293e12..e5647243f2 100644 --- a/django/test/__init__.py +++ b/django/test/__init__.py @@ -4,3 +4,10 @@ Django Unit Test and Doctest framework. from django.test.client import Client from django.test.testcases import TestCase, TransactionTestCase + +class SkippedTest(Exception): + def __init__(self, reason): + self.reason = reason + + def __str__(self): + return self.reason \ No newline at end of file diff --git a/django/test/decorators.py b/django/test/decorators.py new file mode 100644 index 0000000000..ca621894e3 --- /dev/null +++ b/django/test/decorators.py @@ -0,0 +1,44 @@ +from django.core import urlresolvers +from django.test import SkippedTest + +def views_required(required_views=[]): + def urls_found(): + try: + for view in required_views: + urlresolvers.reverse(view) + return True + except urlresolvers.NoReverseMatch: + return False + reason = 'Required view%s for this test not found: %s' % \ + (len(required_views) > 1 and 's' or '', ', '.join(required_views)) + return conditional_skip(urls_found, reason=reason) + +def modules_required(required_modules=[]): + def modules_found(): + try: + for module in required_modules: + __import__(module) + return True + except ImportError: + return False + reason = 'Required module%s for this test not found: %s' % \ + (len(required_modules) > 1 and 's' or '', ', '.join(required_modules)) + return conditional_skip(modules_found, reason=reason) + +def skip_specific_database(database_engine): + def database_check(): + from django.conf import settings + return database_engine == settings.DATABASE_ENGINE + reason = 'Test not run for database engine %s.' % database_engine + return conditional_skip(database_check, reason=reason) + +def conditional_skip(required_condition, reason=''): + if required_condition(): + return lambda x: x + else: + return skip_test(reason) + +def skip_test(reason=''): + def _skip(x): + raise SkippedTest(reason=reason) + return lambda x: _skip diff --git a/django/test/simple.py b/django/test/simple.py index 9cf1ece291..8a60b69b36 100644 --- a/django/test/simple.py +++ b/django/test/simple.py @@ -1,4 +1,4 @@ -import unittest +import sys, time, traceback, unittest from django.conf import settings from django.db.models import get_app, get_apps from django.test import _doctest as doctest @@ -202,9 +202,95 @@ class DefaultTestRunner(object): old_name = settings.DATABASE_NAME from django.db import connection connection.creation.create_test_db(verbosity, autoclobber=not interactive) - result = unittest.TextTestRunner(verbosity=verbosity).run(suite) + result = SkipTestRunner(verbosity=verbosity).run(suite) connection.creation.destroy_test_db(old_name, verbosity) teardown_test_environment() return len(result.failures) + len(result.errors) + + +class SkipTestRunner: + """ + A test runner class that adds a Skipped category in the output layer. + + Modeled after unittest.TextTestRunner. + + Similarly to unittest.TextTestRunner, prints summary of the results at the end. + (Including a count of skipped tests.) + """ + + def __init__(self, stream=sys.stderr, descriptions=1, verbosity=1): + self.stream = unittest._WritelnDecorator(stream) + self.descriptions = descriptions + self.verbosity = verbosity + self.result = _SkipTestResult(self.stream, descriptions, verbosity) + + def run(self, test): + "Run the given test case or test suite." + startTime = time.time() + test.run(self.result) + stopTime = time.time() + timeTaken = stopTime - startTime + + self.result.printErrors() + self.stream.writeln(self.result.separator2) + run = self.result.testsRun + self.stream.writeln('Ran %d test%s in %.3fs' % + (run, run != 1 and 's' or '', timeTaken)) + self.stream.writeln() + if not self.result.wasSuccessful(): + self.stream.write('FAILED (') + failed, errored, skipped = map(len, (self.result.failures, self.result.errors, self.result.skipped)) + if failed: + self.stream.write('failures=%d' % failed) + if errored: + if failed: self.stream.write(', ') + self.stream.write('errors=%d' % errored) + if skipped: + if errored or failed: self.stream.write(', ') + self.stream.write('skipped=%d' % skipped) + self.stream.writeln(')') + else: + self.stream.writeln('OK') + return self.result + +class _SkipTestResult(unittest._TextTestResult): + """ + A test result class that adds a Skipped category in the output layer. + + Modeled after unittest._TextTestResult. + + Similarly to unittest._TextTestResult, prints out the names of tests as they are + run and errors as they occur. + """ + + def __init__(self, stream, descriptions, verbosity): + unittest._TextTestResult.__init__(self, stream, descriptions, verbosity) + self.skipped = [] + + def addError(self, test, err): + # Determine if this is a skipped test + tracebacks = traceback.extract_tb(err[2]) + if tracebacks[-1][-1].startswith('raise SkippedTest'): + self.skipped.append((test, self._exc_info_to_string(err, test))) + if self.showAll: + self.stream.writeln('SKIPPED') + elif self.dots: + self.stream.write('S') + self.stream.flush() + else: + unittest.TestResult.addError(self, test, err) + if self.showAll: + self.stream.writeln('ERROR') + elif self.dots: + self.stream.write('E') + self.stream.flush() + + def printErrors(self): + if self.dots or self.showAll: + self.stream.writeln() + self.printErrorList('SKIPPED', self.skipped) + self.printErrorList('ERROR', self.errors) + self.printErrorList('FAIL', self.failures) + diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index d4e1eb8eaf..d1a40d74c2 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -1045,6 +1045,58 @@ For example:: This test case will load the contents of ``myapp.test_models`` and add any subclass of ``django.db.models.Model`` to ``myapp.models``. +Skipping tests bound to fail +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded: 1.1 + +Occasionally it's helpful to specify tests that are skipped under certain +circumstances. To accomplish this, the Django test framework offers decorators +that you can apply to your test methods for them to be conditionally skipped. + +You can supply your own condition function as follows:: + + from django.tests.decorators import * + + class TestUnderCondition(TestCase): + + def _my_condition(): + # Condition returning True if test should be run and False if it + # should be skipped. + + @conditional_skip(_my_condition, reason='This test should be skipped sometimes') + def testOnlyOnTuesday(self): + # Test to run if _my_condition evaluates to True + +In addition, the Django test framework supplies a handful of skip conditions that +handle commonly used conditions for skipping tests. + +``views_required(required_views=[])`` + Does a ``urlresolver.Reverse`` on the required views supplied. Runs test only if + all views in ``required_views`` are in use. + +``modules_required(required_modules=[])`` + Runs tests only if all modules in ``required_modules`` can be imported. + +``skip_specific_database(database_engine)`` + Skips test if ``settings.DATABASE_ENGINE`` is equal to database_engine. + +If a test is skipped, it is added to a skipped category in the test runner and +the test results are reported as such:: + + ====================================================================== + SKIPPED: test_email_found (django.contrib.auth.tests.basic.PasswordResetTest) + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "/Users/dnaquin/Dropbox/Sandbox/django/django/test/decorators.py", line 43, in _skip + raise SkippedTest(reason=reason) + SkippedTest: Required view for this test not found: django.contrib.auth.views.password_reset + + ---------------------------------------------------------------------- + Ran 408 tests in 339.663s + + FAILED (failures=1, skipped=2) + .. _emptying-test-outbox: Emptying the test outbox diff --git a/tests/regressiontests/admin_views/urls.py b/tests/regressiontests/admin_views/urls.py index ae14d117a0..01917c0ecb 100644 --- a/tests/regressiontests/admin_views/urls.py +++ b/tests/regressiontests/admin_views/urls.py @@ -2,7 +2,7 @@ from django.conf.urls.defaults import * from django.contrib import admin import views import customadmin -admin.autodiscover() +#admin.autodiscover() urlpatterns = patterns('', (r'^admin/doc/', include('django.contrib.admindocs.urls')), (r'^admin/secure-view/$', views.secure_view), diff --git a/tests/regressiontests/test_decorators/tests.py b/tests/regressiontests/test_decorators/tests.py new file mode 100644 index 0000000000..561319bf71 --- /dev/null +++ b/tests/regressiontests/test_decorators/tests.py @@ -0,0 +1,23 @@ +""" +>>> from django.test import SkippedTest +>>> from django.test.decorators import * + +>>> skip_test()(None)(None) +Traceback (most recent call last): + ... +SkippedTest + +>>> skip_test(reason='testing')(None)(None) +Traceback (most recent call last): + ... +SkippedTest: testing + +>>> conditional_skip(lambda: False)(None)(None) +Traceback (most recent call last): + ... +SkippedTest + +>>> conditional_skip(lambda: True)(lambda: True)() +True + +""" diff --git a/tests/urls.py b/tests/urls.py index 6704829231..b06863dfb8 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -11,6 +11,7 @@ urlpatterns = patterns('', # Always provide the auth system login and logout views (r'^accounts/login/$', 'django.contrib.auth.views.login', {'template_name': 'login.html'}), (r'^accounts/logout/$', 'django.contrib.auth.views.logout'), + (r'^accounts2/', include('django.contrib.auth.urls')), # test urlconf for {% url %} template tag (r'^url_tag/', include('regressiontests.templates.urls')),