Code

Ticket #4788: testing-patches.diff

File testing-patches.diff, 14.2 KB (added by sciyoshi, 6 years ago)

Updated patch for Django >= 1.0

Line 
1Index: django/test/simple.py
2===================================================================
3--- django/test/simple.py       (revision 1029)
4+++ django/test/simple.py       (working copy)
5@@ -1,4 +1,4 @@
6-import unittest
7+import sys, time, traceback, unittest
8 from django.conf import settings
9 from django.db.models import get_app, get_apps
10 from django.test import _doctest as doctest
11@@ -140,9 +140,93 @@
12     old_name = settings.DATABASE_NAME
13     from django.db import connection
14     connection.creation.create_test_db(verbosity, autoclobber=not interactive)
15-    result = unittest.TextTestRunner(verbosity=verbosity).run(suite)
16+    result = SkipTestRunner(verbosity=verbosity).run(suite)
17     connection.creation.destroy_test_db(old_name, verbosity)
18     
19     teardown_test_environment()
20     
21     return len(result.failures) + len(result.errors)
22+
23+class SkipTestRunner:
24+    """
25+    A test runner class that adds a Skipped category in the output layer.
26+   
27+    Modeled after unittest.TextTestRunner.
28+
29+    Similarly to unittest.TextTestRunner, prints summary of the results at the end.
30+    (Including a count of skipped tests.)
31+    """
32+
33+    def __init__(self, stream=sys.stderr, descriptions=1, verbosity=1):
34+        self.stream = unittest._WritelnDecorator(stream)
35+        self.descriptions = descriptions
36+        self.verbosity = verbosity
37+        self.result = _SkipTestResult(self.stream, descriptions, verbosity)
38+
39+    def run(self, test):
40+        "Run the given test case or test suite."
41+        startTime = time.time()
42+        test.run(self.result)
43+        stopTime = time.time()
44+        timeTaken = stopTime - startTime
45+       
46+        self.result.printErrors()
47+        self.stream.writeln(self.result.separator2)
48+        run = self.result.testsRun
49+        self.stream.writeln('Ran %d test%s in %.3fs' %
50+                (run, run != 1 and 's' or '', timeTaken))
51+        self.stream.writeln()
52+        if not self.result.wasSuccessful():
53+            self.stream.write('FAILED (')
54+            failed, errored, skipped = map(len, (self.result.failures, self.result.errors, self.result.skipped))
55+            if failed:
56+                self.stream.write('failures=%d' % failed)
57+            if errored:
58+                if failed: self.stream.write(', ')
59+                self.stream.write('errors=%d' % errored)
60+            if skipped:
61+                if errored or failed: self.stream.write(', ')
62+                self.stream.write('skipped=%d' % skipped)
63+            self.stream.writeln(')')
64+        else:
65+            self.stream.writeln('OK')
66+        return self.result
67+
68+class _SkipTestResult(unittest._TextTestResult):
69+    """
70+    A test result class that adds a Skipped category in the output layer.
71+   
72+    Modeled after unittest._TextTestResult.
73+   
74+    Similarly to unittest._TextTestResult, prints out the names of tests as they are
75+    run and errors as they occur.
76+    """
77+
78+    def __init__(self, stream, descriptions, verbosity):
79+        unittest._TextTestResult.__init__(self, stream, descriptions, verbosity)
80+        self.skipped = []
81+
82+    def addError(self, test, err):
83+        # Determine if this is a skipped test
84+        tracebacks = traceback.extract_tb(err[2])
85+        if tracebacks[-1][-1].startswith('raise SkippedTest'):
86+            self.skipped.append((test, self._exc_info_to_string(err, test)))
87+            if self.showAll:
88+                self.stream.writeln('SKIPPED')
89+            elif self.dots:
90+                self.stream.write('S')
91+                self.stream.flush()
92+        else:
93+            unittest.TestResult.addError(self, test, err)
94+            if self.showAll:
95+                self.stream.writeln('ERROR')
96+            elif self.dots:
97+                self.stream.write('E')
98+                self.stream.flush()
99+
100+    def printErrors(self):
101+        if self.dots or self.showAll:
102+            self.stream.writeln()
103+        self.printErrorList('SKIPPED', self.skipped)
104+        self.printErrorList('ERROR', self.errors)
105+        self.printErrorList('FAIL', self.failures)
106Index: django/test/__init__.py
107===================================================================
108--- django/test/__init__.py     (revision 1029)
109+++ django/test/__init__.py     (working copy)
110@@ -4,3 +4,10 @@
111 
112 from django.test.client import Client
113 from django.test.testcases import TestCase
114+
115+class SkippedTest(Exception):
116+    def __init__(self, reason):
117+        self.reason = reason
118+       
119+    def __str__(self):
120+        return self.reason
121Index: django/test/decorators.py
122===================================================================
123--- django/test/decorators.py   (revision 0)
124+++ django/test/decorators.py   (revision 0)
125@@ -0,0 +1,44 @@
126+from django.core import urlresolvers
127+from django.test import SkippedTest
128+
129+def views_required(required_views=[]):
130+    def urls_found():
131+        try:
132+            for view in required_views:
133+                urlresolvers.reverse(view)
134+            return True
135+        except urlresolvers.NoReverseMatch:
136+            return False
137+    reason = 'Required view%s for this test not found: %s' % \
138+            (len(required_views) > 1 and 's' or '', ', '.join(required_views))
139+    return conditional_skip(urls_found, reason=reason)
140+
141+def modules_required(required_modules=[]):
142+    def modules_found():
143+        try:
144+            for module in required_modules:
145+                __import__(module)
146+            return True
147+        except ImportError:
148+            return False
149+    reason = 'Required module%s for this test not found: %s' % \
150+            (len(required_modules) > 1 and 's' or '', ', '.join(required_modules))
151+    return conditional_skip(modules_found, reason=reason)
152+
153+def skip_specific_database(database_engine):
154+    def database_check():
155+        from django.conf import settings
156+        return database_engine == settings.DATABASE_ENGINE
157+    reason = 'Test not run for database engine %s.' % database_engine
158+    return conditional_skip(database_check, reason=reason)
159+
160+def conditional_skip(required_condition, reason=''):
161+    if required_condition():
162+        return lambda x: x
163+    else:
164+        return skip_test(reason)
165+
166+def skip_test(reason=''):
167+    def _skip(x):
168+        raise SkippedTest(reason=reason)
169+    return lambda x: _skip
170Index: django/contrib/auth/tests/views.py
171===================================================================
172--- django/contrib/auth/tests/views.py  (revision 1029)
173+++ django/contrib/auth/tests/views.py  (working copy)
174@@ -5,30 +5,33 @@
175 from django.conf import settings
176 from django.contrib.auth.models import User
177 from django.test import TestCase
178+from django.test.decorators import views_required
179 from django.core import mail
180+from django.core.urlresolvers import reverse
181 
182 class PasswordResetTest(TestCase):
183     fixtures = ['authtestdata.json']
184-    urls = 'django.contrib.auth.urls'
185 
186     def test_email_not_found(self):
187         "Error is raised if the provided email address isn't currently registered"
188-        response = self.client.get('/password_reset/')
189+        response = self.client.get(reverse('django.contrib.auth.views.password_reset'))
190         self.assertEquals(response.status_code, 200)
191-        response = self.client.post('/password_reset/', {'email': 'not_a_real_email@email.com'})
192+        response = self.client.post(reverse('django.contrib.auth.views.password_reset'), {'email': 'not_a_real_email@email.com'})
193         self.assertContains(response, "That e-mail address doesn't have an associated user account")
194         self.assertEquals(len(mail.outbox), 0)
195+    test_email_not_found = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_email_not_found)
196 
197     def test_email_found(self):
198         "Email is sent if a valid email address is provided for password reset"
199-        response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
200+        response = self.client.post(reverse('django.contrib.auth.views.password_reset'), {'email': 'staffmember@example.com'})
201         self.assertEquals(response.status_code, 302)
202         self.assertEquals(len(mail.outbox), 1)
203         self.assert_("http://" in mail.outbox[0].body)
204+    test_email_found = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_email_found)
205 
206     def _test_confirm_start(self):
207         # Start by creating the email
208-        response = self.client.post('/password_reset/', {'email': 'staffmember@example.com'})
209+        response = self.client.post(reverse('django.contrib.auth.views.password_reset'), {'email': 'staffmember@example.com'})
210         self.assertEquals(response.status_code, 302)
211         self.assertEquals(len(mail.outbox), 1)
212         return self._read_signup_email(mail.outbox[0])
213@@ -44,7 +47,9 @@
214         # redirect to a 'complete' page:
215         self.assertEquals(response.status_code, 200)
216         self.assert_("Please enter your new password" in response.content)
217+    test_confirm_valid = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_confirm_valid)
218 
219+
220     def test_confirm_invalid(self):
221         url, path = self._test_confirm_start()
222         # Lets munge the token in the path, but keep the same length,
223@@ -54,6 +59,7 @@
224         response = self.client.get(path)
225         self.assertEquals(response.status_code, 200)
226         self.assert_("The password reset link was invalid" in response.content)
227+    test_confirm_invalid = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_confirm_invalid)
228 
229     def test_confirm_invalid_post(self):
230         # Same as test_confirm_invalid, but trying
231@@ -66,6 +72,7 @@
232         # Check the password has not been changed
233         u = User.objects.get(email='staffmember@example.com')
234         self.assert_(not u.check_password("anewpassword"))
235+    test_confirm_invalid_post = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_confirm_invalid_post)
236 
237     def test_confirm_complete(self):
238         url, path = self._test_confirm_start()
239@@ -81,6 +88,7 @@
240         response = self.client.get(path)
241         self.assertEquals(response.status_code, 200)
242         self.assert_("The password reset link was invalid" in response.content)
243+    test_confirm_complete = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_confirm_complete)
244 
245     def test_confirm_different_passwords(self):
246         url, path = self._test_confirm_start()
247@@ -88,8 +96,8 @@
248                                            'new_password2':' x'})
249         self.assertEquals(response.status_code, 200)
250         self.assert_("The two password fields didn't match" in response.content)
251+    test_confirm_different_passwords = views_required(required_views=['django.contrib.auth.views.password_reset'])(test_confirm_different_passwords)
252 
253-
254 class ChangePasswordTest(TestCase):
255     fixtures = ['authtestdata.json']
256     urls = 'django.contrib.auth.urls'
257Index: tests/regressiontests/test_decorators/__init__.py
258===================================================================
259Index: tests/regressiontests/test_decorators/tests.py
260===================================================================
261--- tests/regressiontests/test_decorators/tests.py      (revision 0)
262+++ tests/regressiontests/test_decorators/tests.py      (revision 0)
263@@ -0,0 +1,23 @@
264+"""
265+>>> from django.test import SkippedTest
266+>>> from django.test.decorators import *
267+
268+>>> skip_test()(None)(None)
269+Traceback (most recent call last):
270+    ...
271+SkippedTest
272+
273+>>> skip_test(reason='testing')(None)(None)
274+Traceback (most recent call last):
275+    ...
276+SkippedTest: testing
277+
278+>>> conditional_skip(lambda: False)(None)(None)
279+Traceback (most recent call last):
280+    ...
281+SkippedTest
282+
283+>>> conditional_skip(lambda: True)(lambda: True)()
284+True
285+
286+"""
287Index: tests/regressiontests/test_decorators/models.py
288===================================================================
289Index: docs/topics/testing.txt
290===================================================================
291--- docs/topics/testing.txt     (revision 1029)
292+++ docs/topics/testing.txt     (working copy)
293@@ -865,6 +865,58 @@
294 This test case will use the contents of ``myapp.test_urls`` as the
295 URLconf for the duration of the test case.
296 
297+Skipping tests bound to fail
298+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
299+
300+**New in Django development version**
301+
302+Occasionally it's helpful to specify tests that are skipped under certain
303+circumstances. To accomplish this, the Django test framework offers decorators
304+that you can apply to your test methods for them to be conditionally skipped.
305+
306+You can supply your own condition function as follows::
307+
308+    from django.tests.decorators import *
309+   
310+    class TestUnderCondition(TestCase):
311+   
312+        def _my_condition():
313+            # Condition returning True if test should be run and False if it
314+            # should be skipped.
315+
316+        @conditional_skip(_my_condition, reason='This test should be skipped sometimes')
317+        def testOnlyOnTuesday(self):
318+            # Test to run if _my_condition evaluates to True
319+
320+In addition, the Django test framework supplies a handful of skip conditions that
321+handle commonly used conditions for skipping tests.
322+
323+``views_required(required_views=[])``
324+    Does a ``urlresolver.Reverse`` on the required views supplied. Runs test only if
325+    all views in ``required_views`` are in use.
326+
327+``modules_required(required_modules=[])``
328+    Runs tests only if all modules in ``required_modules`` can be imported.
329+
330+``skip_specific_database(database_engine)``
331+    Skips test if ``settings.DATABASE_ENGINE`` is equal to database_engine.
332+
333+If a test is skipped, it is added to a skipped category in the test runner and
334+the test results are reported as such::
335+
336+    ======================================================================
337+    SKIPPED: test_email_found (django.contrib.auth.tests.basic.PasswordResetTest)
338+    ----------------------------------------------------------------------
339+    Traceback (most recent call last):
340+      File "/Users/dnaquin/Dropbox/Sandbox/django/django/test/decorators.py", line 43, in _skip
341+        raise SkippedTest(reason=reason)
342+    SkippedTest: Required view for this test not found: django.contrib.auth.views.password_reset
343+
344+    ----------------------------------------------------------------------
345+    Ran 408 tests in 339.663s
346+
347+    FAILED (failures=1, skipped=2)
348+
349 .. _emptying-test-outbox:
350 
351 Emptying the test outbox