Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Adding the Django Discovery TestRunner #17365 #319

Closed
wants to merge 23 commits into from

6 participants

Mahdi Yusuf Marc Tamlyn Jannis Leidel Jacob Kaplan-Moss Carl Meyer Anand Kumria
Mahdi Yusuf

This pull request adds the unittest2 test discovery to Django while specifying location for look up in settings.py

That being said, I have only updated the 1.5.txt release until the code is reviewed. Once that is complete I will update the testing documentation and what other documentation that needs to be updated as per discussion had here.

Also let me know if I can improve the testing. I think I have tested the discovery test runner well but if it can be improved to cover edge cases let me know.

Many thanks to @freakboy3742 for his guidance.

https://code.djangoproject.com/ticket/17365 is the attached ticket

Marc Tamlyn
Owner

I'm guessing there's a ticket this is associated to?

Also is the intention to replace the old test runner with the DiscoverRunner? Not that I want it, I tend to use DiscoverRunner anyway, but some people may want the old behaviour

Jannis Leidel
Owner

FWIW, given this is a copy of the django-discover-runner code and the feature in unitttest2 is called "discover", I'd prefer to use the name "DiscoverRunner" instead of "DiscoveryRunner".

Jannis Leidel
Owner

Also, the defaultTestLoader must be imported like in the change jezdez/django-discover-runner@0fdc131 since we wrap an installed unittest2 in the django.utils.unittest module and importing from django.utils.loader is the one included in Django. Not doing so prevents an isinstance check in unittest2 from succeeding, leading to the symptoms as mentioned in jezdez/django-discover-runner#2.

Jannis Leidel
Owner

As to whether we should support the old test runner, I strongly suggest to deprecate our own test runner, making the DiscoverRunner the default in Django>=1.5 projects, switching to the DiscoverRunner by default in 1.6, and remove the old runner in 1.7.

Mahdi Yusuf

@mjtamlyn I have updated the pull request to refer to the ticket.

@jezdez Those changes can be made. That is the planned depreciation schedule in place for this.

Jacob Kaplan-Moss
Owner

FTR, I'm +1 on @jezdez's proposal.

Jannis Leidel
Owner

Woot!

django/conf/global_settings.py
@@ -293,7 +293,7 @@
293 293
 
294 294
 # Maximum size, in bytes, of a request before it will be streamed to the
295 295
 # file system instead of into memory.
296  
-FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # i.e. 2.5 MB
  296
+FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440  # i.e. 2.5 MB
1

Is it necessary to have the PEP8 fixes in this pull request? I'm not sure what the recommendation is for the Django repository, but in my own repositories I ask for either a PEP8 fix before the code changes, or one just after (to separate out changes which should have no functional impact).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
django/conf/global_settings.py
@@ -346,11 +346,11 @@
346 346
 # http://docs.python.org/library/datetime.html#strftime-behavior
347 347
 # * Note that these format strings are different from the ones to display dates
348 348
 DATE_INPUT_FORMATS = (
349  
-    '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06'
350  
-    '%b %d %Y', '%b %d, %Y',            # 'Oct 25 2006', 'Oct 25, 2006'
351  
-    '%d %b %Y', '%d %b, %Y',            # '25 Oct 2006', '25 Oct, 2006'
352  
-    '%B %d %Y', '%B %d, %Y',            # 'October 25 2006', 'October 25, 2006'
353  
-    '%d %B %Y', '%d %B, %Y',            # '25 October 2006', '25 October, 2006'
  349
+    '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y',  # '2006-10-25', '10/25/2006', '10/25/06'
  350
+    '%b %d %Y', '%b %d, %Y',             # 'Oct 25 2006', 'Oct 25, 2006'
  351
+    '%d %b %Y', '%d %b, %Y',             # '25 Oct 2006', '25 Oct, 2006'
  352
+    '%B %d %Y', '%B %d, %Y',             # 'October 25 2006', 'October 25, 2006'
  353
+    '%d %B %Y', '%d %B, %Y',             # '25 October 2006', '25 October, 2006'
1

likewise ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
django/test/simple.py
@@ -110,7 +110,7 @@ def build_test(label):
110 110
 
111 111
     try:
112 112
         if issubclass(TestClass, (unittest.TestCase, real_unittest.TestCase)):
113  
-            if len(parts) == 2: # label is app.TestClass
  113
+            if len(parts) == 2:  # label is app.TestClass
1

as before ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Anand Kumria akumria commented on the diff September 11, 2012
django/utils/unittest/loader.py
@@ -24,6 +24,7 @@ def _CmpToKey(mycmp):
24 24
     class K(object):
25 25
         def __init__(self, obj):
26 26
             self.obj = obj
  27
+
1

again with this file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Mahdi Yusuf

so documentation here all that needs to be done? I would like to get this in before the freeze if possible. @jezdez ?

Jannis Leidel
Owner

Yeah, the code looks good to me, doc updates could easily land after the fact, IMO.

Mahdi Yusuf

@jezdez so @carljm said that is probably wont get into 1.5 it needs more review and adds too many setting to the setting.py file.

So I guess that's it for now.

Carl Meyer
Owner

Sorry I haven't done a fuller comment on this sooner. I worked on this some at the DjangoCon sprints and talked it over quite a bit with Russ, Jacob, and others. I have some work locally but haven't pushed it yet since it's not yet in a fully working state. I'll try to clean that work up and get it pushed soon. Specifically the things that IMO are not ready yet with this patch:

  1. @jacobian wanted to avoid adding any new settings with this; things like the test discovery pattern should be set instead as command-line arguments.

  2. If we are deprecating the old runner, it's no good to have the new runner inherit from the deprecated runner; that's just postponing a bunch of work to whoever does the final deprecation removal, deprecation removals should not require big code reorganizations. Instead the new runner needs to be standalone with all required functionality, and the old runner should inherit from the new runner, so when it is removed nothing else is affected.

  3. Docs - I don't think we should commit things without adequate docs and then clean it up later. Writing the docs helps us figure out if we've made mistakes in the API. Docs are a first-class citizen.

  4. The current code does a kind of funny thing where it tries to intelligently decide whether to use discovery or loadTestsFromModule depending what you pass on the command line and what it finds there. I did this because it was functional and what I wanted for an external project, but I'm not sure this approach belongs in Django - this is just going right back down the path of inventing our own unique discovery process. I think we should maybe stick closer to the native behavior of unittest, and discuss with voidspace possible improvements to that behavior. This requires more discussion.

And that's really the key - while I'd like to see this change in Django, it's a significant developer-facing change and I think it's worth taking the time to make sure we do it right. And that means not committing something quick before feature freeze just to get it in, it means plenty of time for consideration and review on django-developers. Unfortunately I didn't have time this summer to drive that, but I'm hoping I (or someone else?) will in the 1.6 cycle.

Carl Meyer
Owner

I meant to also say - many thanks @myusuf3 for getting things rolling with this pull request. I'll leave a note here when I get my WIP pushed, in case you or anyone else want to carry it forward before I get back to it.

Jannis Leidel
Owner

@carljm Thanks for the summary and update, I agree on all counts and updated the ticket.

Mahdi Yusuf

@carljm @jacobian @jezdez so what can we do to get the ball rolling here? I would like to contribute to make sure this gets in;

I am just trying to understand the process here. I found the ticket read the discussion spoke with @freakboy3742 with respect on how to go about solving it and what required tests. I understand having different ideas on how to go about things and having a discussion. I am just trying to understand what the process is.

Carl Meyer
Owner

Hi @myusuf3 - my apologies, I screwed up the process by saying here that I'd clean up and push my work-in-progress, and then never actually doing so. I think I outlined in a comment above what I think needs to be done in order for this to be ready to go in; I still haven't found time to finish or clean up my work-in-progress from the DjangoCon sprints, but I've now pushed it, (as-is and not-yet-working) to https://github.com/carljm/django/tree/ticket-17365

If you'd like to work on this, you can start where I left off if that's helpful. If you have questions about the direction I was heading with that branch, I'm happy to talk it over more here or on IRC. I think there are still some pretty significant design decisions to be made in terms of how the test discovery actually works (item 4 in my comment above; how much, if any, custom extra behavior we add on top of unittest2's default discovery behavior), so I don't think this is just a matter of making some minor code tweaks and getting it merged, it's probably going to merit a proposal and discussion on django-developers to gather feedback on the right behavior.

Again, sorry for throwing a wrench in the works by not following through after my last comments. The process from here on out, as far as I'm concerned, is to address points 1-3 above, make a proposal to django-developers regarding the precise discovery behavior (point 4 above), reach agreement on that and implement it, get review of the patch/pull request (I'm happy to do review at that stage), and then hopefully get it merged!

Mahdi Yusuf myusuf3 closed this May 09, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
4  django/conf/project_template/project_name/settings.py
@@ -122,6 +122,10 @@
122 122
     # 'django.contrib.admindocs',
123 123
 )
124 124
 
  125
+
  126
+TEST_RUNNER = 'django.test.runner.DiscoverRunner'
  127
+
  128
+
125 129
 # A sample logging configuration. The only tangible logging
126 130
 # performed by this configuration is to send an email to
127 131
 # the site admins on every HTTP 500 error when DEBUG=False.
231  django/test/runner.py
... ...
@@ -0,0 +1,231 @@
  1
+import os
  2
+
  3
+from django.conf import settings
  4
+from django.core.exceptions import ImproperlyConfigured
  5
+from django.test import TestCase
  6
+from django.test.utils import setup_test_environment, teardown_test_environment
  7
+from django.utils.importlib import import_module
  8
+from django.utils import unittest
  9
+from django.utils.unittest.loader import defaultTestLoader
  10
+
  11
+
  12
+class DjangoTestDiscoverRunner(object):
  13
+    """A Django test runner that uses unittest2 test discovery."""
  14
+    def __init__(self, verbosity=1, interactive=True, failfast=True, **kwargs):
  15
+        self.verbosity = verbosity
  16
+        self.interactive = interactive
  17
+        self.failfast = failfast
  18
+
  19
+    def setup_test_environment(self, **kwargs):
  20
+        setup_test_environment()
  21
+        settings.DEBUG = False
  22
+        unittest.installHandler()
  23
+
  24
+    def build_suite(self, test_labels, extra_tests=None, **kwargs):
  25
+        suite = None
  26
+        root = settings.TEST_DISCOVER_ROOT
  27
+        pattern = settings.TEST_DISCOVER_PATTERN
  28
+        top_level = settings.TEST_DISCOVER_TOP_LEVEL
  29
+
  30
+        if test_labels:
  31
+            suite = defaultTestLoader.loadTestsFromNames(test_labels)
  32
+            # if single named module has no tests, do discovery within the
  33
+            # module itself
  34
+            if not suite.countTestCases() and len(test_labels) == 1:
  35
+                suite = None
  36
+                root = import_module(test_labels[0]).__path__[0]
  37
+                top_level = os.path.dirname(root)
  38
+
  39
+        if suite is None:
  40
+            suite = defaultTestLoader.discover(root,
  41
+                pattern=pattern,
  42
+                top_level_dir=top_level,
  43
+                )
  44
+
  45
+        if extra_tests:
  46
+            for test in extra_tests:
  47
+                suite.addTest(test)
  48
+
  49
+        return reorder_suite(suite, (TestCase,))
  50
+
  51
+    def setup_databases(self, **kwargs):
  52
+        from django.db import connections, DEFAULT_DB_ALIAS
  53
+
  54
+        # First pass -- work out which databases actually need to be created,
  55
+        # and which ones are test mirrors or duplicate entries in DATABASES
  56
+        mirrored_aliases = {}
  57
+        test_databases = {}
  58
+        dependencies = {}
  59
+        for alias in connections:
  60
+            connection = connections[alias]
  61
+            if connection.settings_dict['TEST_MIRROR']:
  62
+                # If the database is marked as a test mirror, save
  63
+                # the alias.
  64
+                mirrored_aliases[alias] = (
  65
+                    connection.settings_dict['TEST_MIRROR'])
  66
+            else:
  67
+                # Store a tuple with DB parameters that uniquely identify it.
  68
+                # If we have two aliases with the same values for that tuple,
  69
+                # we only need to create the test database once.
  70
+                item = test_databases.setdefault(
  71
+                    connection.creation.test_db_signature(),
  72
+                    (connection.settings_dict['NAME'], set())
  73
+                )
  74
+                item[1].add(alias)
  75
+
  76
+                if 'TEST_DEPENDENCIES' in connection.settings_dict:
  77
+                    dependencies[alias] = (
  78
+                        connection.settings_dict['TEST_DEPENDENCIES'])
  79
+                else:
  80
+                    if alias != DEFAULT_DB_ALIAS:
  81
+                        dependencies[alias] = connection.settings_dict.get(
  82
+                            'TEST_DEPENDENCIES', [DEFAULT_DB_ALIAS])
  83
+
  84
+        # Second pass -- actually create the databases.
  85
+        old_names = []
  86
+        mirrors = []
  87
+
  88
+        for signature, (db_name, aliases) in dependency_ordered(
  89
+            test_databases.items(), dependencies):
  90
+            test_db_name = None
  91
+            # Actually create the database for the first connection
  92
+
  93
+            for alias in aliases:
  94
+                connection = connections[alias]
  95
+                old_names.append((connection, db_name, True))
  96
+                if test_db_name is None:
  97
+                    test_db_name = connection.creation.create_test_db(
  98
+                            self.verbosity, autoclobber=not self.interactive)
  99
+                else:
  100
+                    connection.settings_dict['NAME'] = test_db_name
  101
+
  102
+        for alias, mirror_alias in mirrored_aliases.items():
  103
+            mirrors.append((alias, connections[alias].settings_dict['NAME']))
  104
+            connections[alias].settings_dict['NAME'] = (
  105
+                connections[mirror_alias].settings_dict['NAME'])
  106
+
  107
+        return old_names, mirrors
  108
+
  109
+    def run_suite(self, suite, **kwargs):
  110
+        return unittest.TextTestRunner(
  111
+            verbosity=self.verbosity, failfast=self.failfast).run(suite)
  112
+
  113
+    def teardown_databases(self, old_config, **kwargs):
  114
+        """
  115
+        Destroys all the non-mirror databases.
  116
+        """
  117
+        old_names, mirrors = old_config
  118
+        for connection, old_name, destroy in old_names:
  119
+            if destroy:
  120
+                connection.creation.destroy_test_db(old_name, self.verbosity)
  121
+
  122
+    def teardown_test_environment(self, **kwargs):
  123
+        unittest.removeHandler()
  124
+        teardown_test_environment()
  125
+
  126
+    def suite_result(self, suite, result, **kwargs):
  127
+        return len(result.failures) + len(result.errors)
  128
+
  129
+    def run_tests(self, test_labels, discovery_root, extra_tests=None, **kwargs):
  130
+        """
  131
+        Run the unit tests for all the test labels in the provided list.
  132
+
  133
+        Test labels should be dotted Python paths to test modules, test
  134
+        classes, or test methods.
  135
+
  136
+        If no test labels are provided, tests will be discovered under the
  137
+        filesystem path ``discovery_root``.
  138
+
  139
+        A list of 'extra' tests may also be provided; these tests
  140
+        will be added to the test suite.
  141
+
  142
+        Returns the number of tests that failed.
  143
+        """
  144
+        self.setup_test_environment()
  145
+        suite = self.build_suite(test_labels, extra_tests)
  146
+        old_config = self.setup_databases()
  147
+        result = self.run_suite(suite)
  148
+        self.teardown_databases(old_config)
  149
+        self.teardown_test_environment()
  150
+        return self.suite_result(suite, result)
  151
+
  152
+
  153
+def dependency_ordered(test_databases, dependencies):
  154
+    """
  155
+    Reorder test_databases into an order that honors the dependencies
  156
+    described in TEST_DEPENDENCIES.
  157
+    """
  158
+    ordered_test_databases = []
  159
+    resolved_databases = set()
  160
+
  161
+    # Maps db signature to dependencies of all it's aliases
  162
+    dependencies_map = {}
  163
+
  164
+    # sanity check - no DB can depend on it's own alias
  165
+    for sig, (_, aliases) in test_databases:
  166
+        all_deps = set()
  167
+        for alias in aliases:
  168
+            all_deps.update(dependencies.get(alias, []))
  169
+        if not all_deps.isdisjoint(aliases):
  170
+            raise ImproperlyConfigured(
  171
+                "Circular dependency: databases %r depend on each other, "
  172
+                "but are aliases." % aliases)
  173
+        dependencies_map[sig] = all_deps
  174
+
  175
+    while test_databases:
  176
+        changed = False
  177
+        deferred = []
  178
+
  179
+        # Try to find a DB that has all it's dependencies met
  180
+        for signature, (db_name, aliases) in test_databases:
  181
+            if dependencies_map[signature].issubset(resolved_databases):
  182
+                resolved_databases.update(aliases)
  183
+                ordered_test_databases.append((signature, (db_name, aliases)))
  184
+                changed = True
  185
+            else:
  186
+                deferred.append((signature, (db_name, aliases)))
  187
+
  188
+        if not changed:
  189
+            raise ImproperlyConfigured(
  190
+                "Circular dependency in TEST_DEPENDENCIES")
  191
+        test_databases = deferred
  192
+    return ordered_test_databases
  193
+
  194
+
  195
+def reorder_suite(suite, classes):
  196
+    """
  197
+    Reorders a test suite by test type.
  198
+
  199
+    `classes` is a sequence of types
  200
+
  201
+    All tests of type classes[0] are placed first, then tests of type
  202
+    classes[1], etc. Tests with no match in classes are placed last.
  203
+    """
  204
+    class_count = len(classes)
  205
+    bins = [unittest.TestSuite() for i in range(class_count+1)]
  206
+    partition_suite(suite, classes, bins)
  207
+    for i in range(class_count):
  208
+        bins[0].addTests(bins[i+1])
  209
+    return bins[0]
  210
+
  211
+
  212
+def partition_suite(suite, classes, bins):
  213
+    """
  214
+    Partitions a test suite by test type.
  215
+
  216
+    classes is a sequence of types
  217
+    bins is a sequence of TestSuites, one more than classes
  218
+
  219
+    Tests of type classes[i] are added to bins[i],
  220
+    tests with no match found in classes are place in bins[-1]
  221
+    """
  222
+    for test in suite:
  223
+        if isinstance(test, unittest.TestSuite):
  224
+            partition_suite(test, classes, bins)
  225
+        else:
  226
+            for i in range(len(classes)):
  227
+                if isinstance(test, classes[i]):
  228
+                    bins[i].addTest(test)
  229
+                    break
  230
+            else:
  231
+                bins[-1].addTest(test)
218  django/test/simple.py
... ...
@@ -1,10 +1,17 @@
  1
+"""
  2
+This module is pending deprecation as of Django 1.5 and will be removed in 1.7.
  3
+
  4
+When it is removed, the default value of the ``TEST_RUNNER`` setting in
  5
+django.conf.global_settings should be changed to
  6
+``django.test.runner.DjangoTestDiscoverRunner``.
  7
+
  8
+"""
1 9
 import unittest as real_unittest
  10
+import warnings
2 11
 
3  
-from django.conf import settings
4  
-from django.core.exceptions import ImproperlyConfigured
5 12
 from django.db.models import get_app, get_apps
6 13
 from django.test import _doctest as doctest
7  
-from django.test.utils import setup_test_environment, teardown_test_environment
  14
+from django.test import runner
8 15
 from django.test.testcases import OutputChecker, DocTestRunner
9 16
 from django.utils import unittest
10 17
 from django.utils.importlib import import_module
@@ -12,6 +19,11 @@
12 19
 
13 20
 __all__ = ('DjangoTestSuiteRunner')
14 21
 
  22
+warnings.warn(
  23
+    "The django.test.simple module and DjangoTestSuiteRunner are deprecated; "
  24
+    "use django.test.runner.DjangoDiscoverTestRunner instead.",
  25
+    PendingDeprecationWarning)
  26
+
15 27
 # The module name for tests outside models.py
16 28
 TEST_MODULE = 'tests'
17 29
 
@@ -153,98 +165,7 @@ def build_test(label):
153 165
     return unittest.TestSuite(tests)
154 166
 
155 167
 
156  
-def partition_suite(suite, classes, bins):
157  
-    """
158  
-    Partitions a test suite by test type.
159  
-
160  
-    classes is a sequence of types
161  
-    bins is a sequence of TestSuites, one more than classes
162  
-
163  
-    Tests of type classes[i] are added to bins[i],
164  
-    tests with no match found in classes are place in bins[-1]
165  
-    """
166  
-    for test in suite:
167  
-        if isinstance(test, unittest.TestSuite):
168  
-            partition_suite(test, classes, bins)
169  
-        else:
170  
-            for i in range(len(classes)):
171  
-                if isinstance(test, classes[i]):
172  
-                    bins[i].addTest(test)
173  
-                    break
174  
-            else:
175  
-                bins[-1].addTest(test)
176  
-
177  
-
178  
-def reorder_suite(suite, classes):
179  
-    """
180  
-    Reorders a test suite by test type.
181  
-
182  
-    `classes` is a sequence of types
183  
-
184  
-    All tests of type classes[0] are placed first, then tests of type
185  
-    classes[1], etc. Tests with no match in classes are placed last.
186  
-    """
187  
-    class_count = len(classes)
188  
-    bins = [unittest.TestSuite() for i in range(class_count+1)]
189  
-    partition_suite(suite, classes, bins)
190  
-    for i in range(class_count):
191  
-        bins[0].addTests(bins[i+1])
192  
-    return bins[0]
193  
-
194  
-
195  
-def dependency_ordered(test_databases, dependencies):
196  
-    """
197  
-    Reorder test_databases into an order that honors the dependencies
198  
-    described in TEST_DEPENDENCIES.
199  
-    """
200  
-    ordered_test_databases = []
201  
-    resolved_databases = set()
202  
-
203  
-    # Maps db signature to dependencies of all it's aliases
204  
-    dependencies_map = {}
205  
-
206  
-    # sanity check - no DB can depend on it's own alias
207  
-    for sig, (_, aliases) in test_databases:
208  
-        all_deps = set()
209  
-        for alias in aliases:
210  
-            all_deps.update(dependencies.get(alias, []))
211  
-        if not all_deps.isdisjoint(aliases):
212  
-            raise ImproperlyConfigured(
213  
-                "Circular dependency: databases %r depend on each other, "
214  
-                "but are aliases." % aliases)
215  
-        dependencies_map[sig] = all_deps
216  
-
217  
-    while test_databases:
218  
-        changed = False
219  
-        deferred = []
220  
-
221  
-        # Try to find a DB that has all it's dependencies met
222  
-        for signature, (db_name, aliases) in test_databases:
223  
-            if dependencies_map[signature].issubset(resolved_databases):
224  
-                resolved_databases.update(aliases)
225  
-                ordered_test_databases.append((signature, (db_name, aliases)))
226  
-                changed = True
227  
-            else:
228  
-                deferred.append((signature, (db_name, aliases)))
229  
-
230  
-        if not changed:
231  
-            raise ImproperlyConfigured(
232  
-                "Circular dependency in TEST_DEPENDENCIES")
233  
-        test_databases = deferred
234  
-    return ordered_test_databases
235  
-
236  
-
237  
-class DjangoTestSuiteRunner(object):
238  
-    def __init__(self, verbosity=1, interactive=True, failfast=True, **kwargs):
239  
-        self.verbosity = verbosity
240  
-        self.interactive = interactive
241  
-        self.failfast = failfast
242  
-
243  
-    def setup_test_environment(self, **kwargs):
244  
-        setup_test_environment()
245  
-        settings.DEBUG = False
246  
-        unittest.installHandler()
247  
-
  168
+class DjangoTestSuiteRunner(runner.DjangoDiscoverTestRunner):
248 169
     def build_suite(self, test_labels, extra_tests=None, **kwargs):
249 170
         suite = unittest.TestSuite()
250 171
 
@@ -263,109 +184,4 @@ def build_suite(self, test_labels, extra_tests=None, **kwargs):
263 184
             for test in extra_tests:
264 185
                 suite.addTest(test)
265 186
 
266  
-        return reorder_suite(suite, (unittest.TestCase,))
267  
-
268  
-    def setup_databases(self, **kwargs):
269  
-        from django.db import connections, DEFAULT_DB_ALIAS
270  
-
271  
-        # First pass -- work out which databases actually need to be created,
272  
-        # and which ones are test mirrors or duplicate entries in DATABASES
273  
-        mirrored_aliases = {}
274  
-        test_databases = {}
275  
-        dependencies = {}
276  
-        for alias in connections:
277  
-            connection = connections[alias]
278  
-            if connection.settings_dict['TEST_MIRROR']:
279  
-                # If the database is marked as a test mirror, save
280  
-                # the alias.
281  
-                mirrored_aliases[alias] = (
282  
-                    connection.settings_dict['TEST_MIRROR'])
283  
-            else:
284  
-                # Store a tuple with DB parameters that uniquely identify it.
285  
-                # If we have two aliases with the same values for that tuple,
286  
-                # we only need to create the test database once.
287  
-                item = test_databases.setdefault(
288  
-                    connection.creation.test_db_signature(),
289  
-                    (connection.settings_dict['NAME'], set())
290  
-                )
291  
-                item[1].add(alias)
292  
-
293  
-                if 'TEST_DEPENDENCIES' in connection.settings_dict:
294  
-                    dependencies[alias] = (
295  
-                        connection.settings_dict['TEST_DEPENDENCIES'])
296  
-                else:
297  
-                    if alias != DEFAULT_DB_ALIAS:
298  
-                        dependencies[alias] = connection.settings_dict.get(
299  
-                            'TEST_DEPENDENCIES', [DEFAULT_DB_ALIAS])
300  
-
301  
-        # Second pass -- actually create the databases.
302  
-        old_names = []
303  
-        mirrors = []
304  
-
305  
-        for signature, (db_name, aliases) in dependency_ordered(
306  
-            test_databases.items(), dependencies):
307  
-            test_db_name = None
308  
-            # Actually create the database for the first connection
309  
-
310  
-            for alias in aliases:
311  
-                connection = connections[alias]
312  
-                old_names.append((connection, db_name, True))
313  
-                if test_db_name is None:
314  
-                    test_db_name = connection.creation.create_test_db(
315  
-                            self.verbosity, autoclobber=not self.interactive)
316  
-                else:
317  
-                    connection.settings_dict['NAME'] = test_db_name
318  
-
319  
-        for alias, mirror_alias in mirrored_aliases.items():
320  
-            mirrors.append((alias, connections[alias].settings_dict['NAME']))
321  
-            connections[alias].settings_dict['NAME'] = (
322  
-                connections[mirror_alias].settings_dict['NAME'])
323  
-
324  
-        return old_names, mirrors
325  
-
326  
-    def run_suite(self, suite, **kwargs):
327  
-        return unittest.TextTestRunner(
328  
-            verbosity=self.verbosity, failfast=self.failfast).run(suite)
329  
-
330  
-    def teardown_databases(self, old_config, **kwargs):
331  
-        """
332  
-        Destroys all the non-mirror databases.
333  
-        """
334  
-        old_names, mirrors = old_config
335  
-        for connection, old_name, destroy in old_names:
336  
-            if destroy:
337  
-                connection.creation.destroy_test_db(old_name, self.verbosity)
338  
-
339  
-    def teardown_test_environment(self, **kwargs):
340  
-        unittest.removeHandler()
341  
-        teardown_test_environment()
342  
-
343  
-    def suite_result(self, suite, result, **kwargs):
344  
-        return len(result.failures) + len(result.errors)
345  
-
346  
-    def run_tests(self, test_labels, extra_tests=None, **kwargs):
347  
-        """
348  
-        Run the unit tests for all the test labels in the provided list.
349  
-        Labels must be of the form:
350  
-         - app.TestClass.test_method
351  
-            Run a single specific test method
352  
-         - app.TestClass
353  
-            Run all the test methods in a given class
354  
-         - app
355  
-            Search for doctests and unittests in the named application.
356  
-
357  
-        When looking for tests, the test runner will look in the models and
358  
-        tests modules for the application.
359  
-
360  
-        A list of 'extra' tests may also be provided; these tests
361  
-        will be added to the test suite.
362  
-
363  
-        Returns the number of tests that failed.
364  
-        """
365  
-        self.setup_test_environment()
366  
-        suite = self.build_suite(test_labels, extra_tests)
367  
-        old_config = self.setup_databases()
368  
-        result = self.run_suite(suite)
369  
-        self.teardown_databases(old_config)
370  
-        self.teardown_test_environment()
371  
-        return self.suite_result(suite, result)
  187
+        return runner.reorder_suite(suite, (unittest.TestCase,))
6  django/utils/unittest/loader.py
@@ -24,6 +24,7 @@ def _CmpToKey(mycmp):
24 24
     class K(object):
25 25
         def __init__(self, obj):
26 26
             self.obj = obj
  27
+
27 28
         def __lt__(self, other):
28 29
             return mycmp(self.obj, other.obj) == -1
29 30
     return K
@@ -44,9 +45,11 @@ def _make_failed_import_test(name, suiteClass):
44 45
     return _make_failed_test('ModuleImportFailure', name, ImportError(message),
45 46
                              suiteClass)
46 47
 
  48
+
47 49
 def _make_failed_load_tests(name, exception, suiteClass):
48 50
     return _make_failed_test('LoadTestsFailure', name, exception, suiteClass)
49 51
 
  52
+
50 53
 def _make_failed_test(classname, methodname, exception, suiteClass):
51 54
     def testFailure(self):
52 55
         raise exception
@@ -310,13 +313,16 @@ def _makeLoader(prefix, sortUsing, suiteClass=None):
310 313
         loader.suiteClass = suiteClass
311 314
     return loader
312 315
 
  316
+
313 317
 def getTestCaseNames(testCaseClass, prefix, sortUsing=cmp):
314 318
     return _makeLoader(prefix, sortUsing).getTestCaseNames(testCaseClass)
315 319
 
  320
+
316 321
 def makeSuite(testCaseClass, prefix='test', sortUsing=cmp,
317 322
               suiteClass=suite.TestSuite):
318 323
     return _makeLoader(prefix, sortUsing, suiteClass).loadTestsFromTestCase(testCaseClass)
319 324
 
  325
+
320 326
 def findTestCases(module, prefix='test', sortUsing=cmp,
321 327
                   suiteClass=suite.TestSuite):
322 328
     return _makeLoader(prefix, sortUsing, suiteClass).loadTestsFromModule(module)
4  docs/internals/deprecation.txt
@@ -286,6 +286,10 @@ these changes.
286 286
 * The ``mimetype`` argument to :class:`~django.http.HttpResponse` ``__init__``
287 287
   will be removed (``content_type`` should be used instead).
288 288
 
  289
+* The module ``django.test.simple`` and the class
  290
+  ``django.test.simple.DjangoTestSuiteRunner`` will be removed. Instead use
  291
+  `django.test.runner.DjangoTestDiscoverRunner`.
  292
+
289 293
 2.0
290 294
 ---
291 295
 
18  docs/releases/1.5.txt
@@ -49,6 +49,24 @@ field will also get updated on save.
49 49
 See the :meth:`Model.save() <django.db.models.Model.save()>` documentation for
50 50
 more details.
51 51
 
  52
+
  53
+Discovery of tests in any test module
  54
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  55
+
  56
+Django 1.5 ships with a new test runner that allows more flexibility in the
  57
+location of tests. With the previous runner
  58
+(``django.test.simple.DjangoTestSuiteRunner``), Django would look for tests
  59
+only in the ``models.py`` and ``tests.py`` modules of any Python package in
  60
+:setting:`INSTALLED_APPS`, and specific tests to be run had to be specified by
  61
+app-label and testcase-class name.
  62
+
  63
+The new runner (``django.test.runner.DjangoTestDiscoverRunner``) will find
  64
+tests by default in any module whose name begins with ``test``, anywhere
  65
+beneath a given root directory. When running ``manage.py test``, the default
  66
+root directory is the current working directory.
  67
+
  68
+For more information, see :doc:`topics/testing`.
  69
+
52 70
 Caching of related model instances
53 71
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
54 72
 
0  tests/regressiontests/test_runner/discovery_app/__init__.py
No changes.
0  tests/regressiontests/test_runner/discovery_app/models/__init__.py
No changes.
16  tests/regressiontests/test_runner/discovery_app/models/test_models.py
... ...
@@ -0,0 +1,16 @@
  1
+"""
  2
+This file demonstrates writing tests using the unittest module. These will pass
  3
+when you run "manage.py test".
  4
+
  5
+Replace this with more appropriate tests for your application.
  6
+"""
  7
+
  8
+from django.test import TestCase
  9
+
  10
+
  11
+class SimpleTest(TestCase):
  12
+    def test_basic_addition(self):
  13
+        """
  14
+        Tests that 1 + 1 always equals 2.
  15
+        """
  16
+        self.assertEqual(1 + 1, 2)
3  tests/regressiontests/test_runner/discovery_app/tests/__init__.py
... ...
@@ -0,0 +1,3 @@
  1
+from test_forms import SimpleTest
  2
+from test_models import SimpleTest
  3
+from views_test import SimpleTest
16  tests/regressiontests/test_runner/discovery_app/tests/test_forms.py
... ...
@@ -0,0 +1,16 @@
  1
+"""
  2
+This file demonstrates writing tests using the unittest module. These will pass
  3
+when you run "manage.py test".
  4
+
  5
+Replace this with more appropriate tests for your application.
  6
+"""
  7
+
  8
+from django.test import TestCase
  9
+
  10
+
  11
+class SimpleTest(TestCase):
  12
+    def test_basic_addition(self):
  13
+        """
  14
+        Tests that 1 + 1 always equals 2.
  15
+        """
  16
+        self.assertEqual(1 + 1, 2)
16  tests/regressiontests/test_runner/discovery_app/tests/test_models.py
... ...
@@ -0,0 +1,16 @@
  1
+"""
  2
+This file demonstrates writing tests using the unittest module. These will pass
  3
+when you run "manage.py test".
  4
+
  5
+Replace this with more appropriate tests for your application.
  6
+"""
  7
+
  8
+from django.test import TestCase
  9
+
  10
+
  11
+class SimpleTest(TestCase):
  12
+    def test_basic_addition(self):
  13
+        """
  14
+        Tests that 1 + 1 always equals 2.
  15
+        """
  16
+        self.assertEqual(1 + 1, 2)
37  tests/regressiontests/test_runner/discovery_app/tests/views_test.py
... ...
@@ -0,0 +1,37 @@
  1
+"""
  2
+This file demonstrates writing tests using the unittest module. These will pass
  3
+when you run "manage.py test".
  4
+
  5
+Replace this with more appropriate tests for your application.
  6
+"""
  7
+
  8
+from django.test import TestCase
  9
+
  10
+
  11
+class SimpleTest(TestCase):
  12
+    def test_basic_addition(self):
  13
+        """
  14
+        Tests that 1 + 1 always equals 2.
  15
+        """
  16
+        self.assertEqual(1 + 1, 2)
  17
+
  18
+    def test_basic_subtraction(self):
  19
+        """
  20
+        Tests that 2 - 1 always equals 1.
  21
+        """
  22
+        self.assertEqual(2 - 1, 1)
  23
+
  24
+    def test_basic_subtraction_again(self):
  25
+        """
  26
+        Tests that 2 - 1 always equals 1.
  27
+        """
  28
+        self.assertEqual(2 - 1, 1)
  29
+
  30
+
  31
+
  32
+class AnotherSimpleTest(TestCase):
  33
+    def test_basic_addition(self):
  34
+        """
  35
+        Tests that 1 + 1 always equals 2.
  36
+        """
  37
+        self.assertEqual(1 + 1, 2)
23  tests/regressiontests/test_runner/tests.py
@@ -10,9 +10,11 @@
10 10
 from django import db
11 11
 from django.test import simple, TransactionTestCase, skipUnlessDBFeature
12 12
 from django.test.simple import DjangoTestSuiteRunner, get_tests
  13
+from django.test.runner import DiscoverRunner
13 14
 from django.test.testcases import connections_support_transactions
14 15
 from django.utils import unittest
15 16
 from django.utils.importlib import import_module
  17
+from django.test.utils import override_settings
16 18
 
17 19
 from ..admin_scripts.tests import AdminScriptTestCase
18 20
 from .models import Person
@@ -20,6 +22,8 @@
20 22
 
21 23
 TEST_APP_OK = 'regressiontests.test_runner.valid_app.models'
22 24
 TEST_APP_ERROR = 'regressiontests.test_runner.invalid_app.models'
  25
+DISCOVER_APP_TESTS = 'regressiontests.test_runner.discovery_app'
  26
+DISCOVER_APP_MODEL_TESTS = 'regressiontests.test_runner.discovery_app.models'
23 27
 
24 28
 
25 29
 class DependencyOrderingTests(unittest.TestCase):
@@ -279,3 +283,22 @@ def test_autoincrement_reset1(self):
279 283
     def test_autoincrement_reset2(self):
280 284
         p = Person.objects.create(first_name='Jack', last_name='Smith')
281 285
         self.assertEqual(p.pk, 1)
  286
+
  287
+
  288
+class DiscoverTestRunnerTests(AdminScriptTestCase):
  289
+
  290
+    def test_discovery_of_tests_default_settings(self):
  291
+        suite = DiscoverRunner().build_suite((DISCOVER_APP_TESTS, ))
  292
+        discovered_tests = suite.countTestCases()
  293
+        self.assertEqual(3, discovered_tests)
  294
+
  295
+    def test_discovery_of_tests_inside_module(self):
  296
+        suite = DiscoverRunner().build_suite((DISCOVER_APP_MODEL_TESTS, ))
  297
+        discovered_tests = suite.countTestCases()
  298
+        self.assertEqual(1, discovered_tests)
  299
+
  300
+    @override_settings(TEST_DISCOVER_PATTERN='*_test.py')
  301
+    def test_discovery_of_tests_with_pattern(self):
  302
+        suite = DiscoverRunner().build_suite((DISCOVER_APP_TESTS, ))
  303
+        discovered_tests = suite.countTestCases()
  304
+        self.assertEqual(4, discovered_tests)
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.