├── testapp ├── __init__.py ├── settings_with_south.py ├── settings_with_plugins.py ├── test_for_nose.py ├── test_only_this.py ├── plugin_t │ └── test_with_plugins.py ├── settings.py ├── settings_old_style.py ├── plugins.py └── runtests.py ├── django_nose ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── test.py ├── __init__.py ├── utils.py ├── fixture_tables.py ├── testcases.py ├── plugin.py └── runner.py ├── .gitignore ├── MANIFEST.in ├── changelog.txt ├── runtests.sh ├── LICENSE ├── setup.py ├── unittests └── test_databases.py └── README.rst /testapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_nose/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_nose/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *~ 4 | *.egg-info 5 | .DS_Store 6 | 7 | -------------------------------------------------------------------------------- /testapp/settings_with_south.py: -------------------------------------------------------------------------------- 1 | from settings import * 2 | 3 | 4 | INSTALLED_APPS = ('south',) + INSTALLED_APPS 5 | -------------------------------------------------------------------------------- /testapp/settings_with_plugins.py: -------------------------------------------------------------------------------- 1 | from settings import * 2 | 3 | 4 | NOSE_PLUGINS = [ 5 | 'testapp.plugins.SanityCheckPlugin' 6 | ] 7 | -------------------------------------------------------------------------------- /testapp/test_for_nose.py: -------------------------------------------------------------------------------- 1 | """Django's test runner won't find this, but nose will.""" 2 | 3 | 4 | def test_addition(): 5 | assert 1 + 1 == 2 6 | -------------------------------------------------------------------------------- /testapp/test_only_this.py: -------------------------------------------------------------------------------- 1 | """Django's test runner won't find this, but nose will.""" 2 | 3 | 4 | def test_multiplication(): 5 | assert 2 * 2 == 4 6 | -------------------------------------------------------------------------------- /testapp/plugin_t/test_with_plugins.py: -------------------------------------------------------------------------------- 1 | from nose.tools import eq_ 2 | 3 | 4 | def test_one(): 5 | from testapp import plugins 6 | eq_(plugins.plugin_began, True) 7 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include runtests.sh 4 | include changelog.txt 5 | recursive-exclude django_nose *.py[co] 6 | recursive-include testapp * 7 | recursive-exclude testapp *.py[co] 8 | -------------------------------------------------------------------------------- /django_nose/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (1, 1, 0) 2 | __version__ = '.'.join(map(str, VERSION)) 3 | 4 | from django_nose.runner import * 5 | from django_nose.testcases import * 6 | 7 | 8 | # Django < 1.2 compatibility. 9 | run_tests = run_gis_tests = NoseTestSuiteRunner 10 | -------------------------------------------------------------------------------- /testapp/settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | 'default': { 3 | 'NAME': 'django_master', 4 | 'ENGINE': 'django.db.backends.sqlite3', 5 | } 6 | } 7 | 8 | INSTALLED_APPS = ( 9 | 'django_nose', 10 | ) 11 | 12 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 13 | -------------------------------------------------------------------------------- /testapp/settings_old_style.py: -------------------------------------------------------------------------------- 1 | DATABASES = { 2 | 'default': { 3 | 'NAME': 'django_master', 4 | 'ENGINE': 'django.db.backends.sqlite3', 5 | } 6 | } 7 | 8 | INSTALLED_APPS = ( 9 | 'django_nose', 10 | ) 11 | 12 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 13 | TEST_RUNNER = 'django_nose.run_tests' 14 | -------------------------------------------------------------------------------- /testapp/plugins.py: -------------------------------------------------------------------------------- 1 | from nose.plugins import Plugin 2 | 3 | 4 | plugin_began = False 5 | 6 | class SanityCheckPlugin(Plugin): 7 | enabled = True 8 | 9 | def options(self, parser, env): 10 | """Register commandline options.""" 11 | 12 | def configure(self, options, conf): 13 | """Configure plugin.""" 14 | 15 | def begin(self): 16 | global plugin_began 17 | plugin_began = True 18 | -------------------------------------------------------------------------------- /testapp/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | from django.conf import settings 5 | 6 | if not settings.configured: 7 | settings.configure( 8 | DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3'}}, 9 | INSTALLED_APPS=[ 10 | 'django_nose', 11 | ], 12 | ) 13 | 14 | from django_nose import NoseTestSuiteRunner 15 | 16 | def runtests(*test_labels): 17 | runner = NoseTestSuiteRunner(verbosity=1, interactive=True) 18 | failures = runner.run_tests(test_labels) 19 | sys.exit(failures) 20 | 21 | 22 | if __name__ == '__main__': 23 | runtests(*sys.argv[1:]) 24 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | (Later entries are in the readme.) 2 | 3 | - 0.1.2 (08-14-10) 4 | * run_tests API support (carjm) 5 | * better coverage numbers (rozza & miracle2k) 6 | * support for adding custom nose plugins (kumar303) 7 | 8 | - 0.1.1 (06-01-10) 9 | * Cleaner installation (Michael Fladischer) 10 | 11 | - 0.1 (05-18-10) 12 | * Class-based test runner (Antti Kaihola) 13 | * Django 1.2 compatibility (Antti Kaihola) 14 | * Mapping Django verbosity to nose verbosity 15 | 16 | - 0.0.3 (12-31-09) 17 | 18 | * Python 2.4 support (Blake Winton) 19 | * GeoDjango spatial database support (Peter Baumgartner) 20 | * Return the number of failing tests on the command line 21 | 22 | - 0.0.2 (10-01-09) 23 | 24 | * rst readme (Rob Madole) 25 | 26 | - 0.0.1 (10-01-09) 27 | 28 | * birth! 29 | -------------------------------------------------------------------------------- /django_nose/management/commands/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Add extra options from the test runner to the ``test`` command, so that you can 3 | browse all the nose options from the command line. 4 | """ 5 | from django.conf import settings 6 | from django.test.utils import get_runner 7 | 8 | 9 | if 'south' in settings.INSTALLED_APPS: 10 | from south.management.commands.test import Command 11 | else: 12 | from django.core.management.commands.test import Command 13 | 14 | 15 | # Django < 1.2 compatibility 16 | test_runner = settings.TEST_RUNNER 17 | if test_runner.endswith('run_tests') or test_runner.endswith('run_gis_tests'): 18 | import warnings 19 | warnings.warn( 20 | 'Use `django_nose.NoseTestSuiteRunner` instead of `%s`' % test_runner, 21 | DeprecationWarning) 22 | 23 | 24 | TestRunner = get_runner(settings) 25 | 26 | if hasattr(TestRunner, 'options'): 27 | extra_options = TestRunner.options 28 | else: 29 | extra_options = [] 30 | 31 | 32 | class Command(Command): 33 | option_list = Command.option_list + tuple(extra_options) 34 | -------------------------------------------------------------------------------- /runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export PYTHONPATH=. 4 | 5 | django_test() { 6 | TEST="$1" 7 | $TEST 2>&1 | grep "Ran $2 test" > /dev/null 8 | if [ $? -gt 0 ] 9 | then 10 | echo FAIL: $3 11 | $TEST 12 | exit 1; 13 | else 14 | echo PASS: $3 15 | fi 16 | 17 | # Check that we're hijacking the help correctly. 18 | $TEST --help 2>&1 | grep 'NOSE_DETAILED_ERRORS' > /dev/null 19 | if [ $? -gt 0 ] 20 | then 21 | echo FAIL: $3 '(--help)' 22 | exit 1; 23 | else 24 | echo PASS: $3 '(--help)' 25 | fi 26 | } 27 | 28 | django_test 'django-admin.py test --settings=testapp.settings' '2' 'normal settings' 29 | django_test 'django-admin.py test --settings=testapp.settings_with_south' '2' 'with south in installed apps' 30 | django_test 'django-admin.py test --settings=testapp.settings_old_style' '2' 'django_nose.run_tests format' 31 | django_test 'testapp/runtests.py testapp.test_only_this' '1' 'via run_tests API' 32 | django_test 'django-admin.py test --settings=testapp.settings_with_plugins testapp/plugin_t' '1' 'with plugins' 33 | django_test 'django-admin.py test --settings=testapp.settings unittests' '4' 'unittests' 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010, Jeff Balogh. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of django-nose nor the names of its contributors may be 15 | used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | 5 | ROOT = os.path.abspath(os.path.dirname(__file__)) 6 | 7 | setup( 8 | name='django-nose', 9 | version='1.1', 10 | description='Makes your Django tests simple and snappy', 11 | long_description=open(os.path.join(ROOT, 'README.rst')).read(), 12 | author='Jeff Balogh', 13 | author_email='me@jeffbalogh.org', 14 | maintainer='Erik Rose', 15 | maintainer_email='erikrose@grinchcentral.com', 16 | url='http://github.com/jbalogh/django-nose', 17 | license='BSD', 18 | packages=find_packages(exclude=['testapp', 'testapp/*']), 19 | include_package_data=True, 20 | zip_safe=False, 21 | install_requires=['nose>=1.0', 'Django>=1.2'], 22 | tests_require=['south>=0.7'], 23 | # This blows up tox runs that install django-nose into a virtualenv, 24 | # because it causes Nose to import django_nose.runner before the Django 25 | # settings are initialized, leading to a mess of errors. There's no reason 26 | # we need FixtureBundlingPlugin declared as an entrypoint anyway, since you 27 | # need to be using django-nose to find the it useful, and django-nose knows 28 | # about it intrinsically. 29 | #entry_points=""" 30 | # [nose.plugins.0.10] 31 | # fixture_bundler = django_nose.fixture_bundling:FixtureBundlingPlugin 32 | # """, 33 | classifiers=[ 34 | 'Development Status :: 5 - Production/Stable', 35 | 'Environment :: Web Environment', 36 | 'Framework :: Django', 37 | 'Intended Audience :: Developers', 38 | 'License :: OSI Approved :: BSD License', 39 | 'Operating System :: OS Independent', 40 | 'Programming Language :: Python', 41 | 'Programming Language :: Python :: 2', 42 | 'Programming Language :: Python :: 2.6', 43 | 'Programming Language :: Python :: 2.7', 44 | 'Topic :: Software Development :: Testing' 45 | ] 46 | ) 47 | -------------------------------------------------------------------------------- /django_nose/utils.py: -------------------------------------------------------------------------------- 1 | def process_tests(suite, process): 2 | """Given a nested disaster of [Lazy]Suites, traverse to the first level 3 | that has setup or teardown, and do something to them. 4 | 5 | If we were to traverse all the way to the leaves (the Tests) 6 | indiscriminately and return them, when the runner later calls them, they'd 7 | run without reference to the suite that contained them, so they'd miss 8 | their class-, module-, and package-wide setup and teardown routines. 9 | 10 | The nested suites form basically a double-linked tree, and suites will call 11 | up to their containing suites to run their setups and teardowns, but it 12 | would be hubris to assume that something you saw fit to setup or teardown 13 | at the module level is less costly to repeat than DB fixtures. Also, those 14 | sorts of setups and teardowns are extremely rare in our code. Thus, we 15 | limit the granularity of bucketing to the first level that has setups or 16 | teardowns. 17 | 18 | :arg process: The thing to call once we get to a leaf or a test with setup 19 | or teardown 20 | 21 | """ 22 | if (not hasattr(suite, '_tests') or 23 | (hasattr(suite, 'hasFixtures') and suite.hasFixtures())): 24 | # We hit a Test or something with setup, so do the thing. (Note that 25 | # "fixtures" here means setup or teardown routines, not Django 26 | # fixtures.) 27 | process(suite) 28 | else: 29 | for t in suite._tests: 30 | process_tests(t, process) 31 | 32 | 33 | def is_subclass_at_all(cls, class_info): 34 | """Return whether ``cls`` is a subclass of ``class_info``. 35 | 36 | Even if ``cls`` is not a class, don't crash. Return False instead. 37 | 38 | """ 39 | try: 40 | return issubclass(cls, class_info) 41 | except TypeError: 42 | return False 43 | 44 | 45 | def uses_mysql(connection): 46 | """Return whether the connection represents a MySQL DB.""" 47 | return 'mysql' in connection.settings_dict['ENGINE'] 48 | -------------------------------------------------------------------------------- /unittests/test_databases.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from unittest import TestCase 3 | 4 | from django.db.models.loading import cache 5 | 6 | from django_nose.runner import NoseTestSuiteRunner 7 | 8 | 9 | class GetModelsForConnectionTests(TestCase): 10 | tables = ['test_table%d' % i for i in xrange(5)] 11 | 12 | def _connection_mock(self, tables): 13 | class FakeIntrospection(object): 14 | def get_table_list(*args, **kwargs): 15 | return tables 16 | 17 | class FakeConnection(object): 18 | introspection = FakeIntrospection() 19 | cursor = lambda x: None 20 | 21 | return FakeConnection() 22 | 23 | def _model_mock(self, db_table): 24 | class FakeModel(object): 25 | _meta = type('meta', (object,), {'db_table': db_table})() 26 | 27 | return FakeModel() 28 | 29 | @contextmanager 30 | def _cache_mock(self, tables=[]): 31 | def get_models(*args, **kwargs): 32 | return [self._model_mock(t) for t in tables] 33 | 34 | old = cache.get_models 35 | cache.get_models = get_models 36 | yield 37 | cache.get_models = old 38 | 39 | def setUp(self): 40 | self.runner = NoseTestSuiteRunner() 41 | 42 | def test_no_models(self): 43 | """For a DB with no tables, return nothing.""" 44 | connection = self._connection_mock([]) 45 | with self._cache_mock(['table1', 'table2']): 46 | self.assertEqual( 47 | self.runner._get_models_for_connection(connection), []) 48 | 49 | def test_wrong_models(self): 50 | """If no tables exists for models, return nothing.""" 51 | connection = self._connection_mock(self.tables) 52 | with self._cache_mock(['table1', 'table2']): 53 | self.assertEqual( 54 | self.runner._get_models_for_connection(connection), []) 55 | 56 | def test_some_models(self): 57 | """If some of the models has appropriate table in the DB, return matching models.""" 58 | connection = self._connection_mock(self.tables) 59 | with self._cache_mock(self.tables[1:3]): 60 | result_tables = [m._meta.db_table for m in 61 | self.runner._get_models_for_connection(connection)] 62 | self.assertEqual(result_tables, self.tables[1:3]) 63 | 64 | def test_all_models(self): 65 | """If all the models have appropriate tables in the DB, return them all.""" 66 | connection = self._connection_mock(self.tables) 67 | with self._cache_mock(self.tables): 68 | result_tables = [m._meta.db_table for m in 69 | self.runner._get_models_for_connection(connection)] 70 | self.assertEqual(result_tables, self.tables) 71 | -------------------------------------------------------------------------------- /django_nose/fixture_tables.py: -------------------------------------------------------------------------------- 1 | """A copy of Django 1.3.0's stock loaddata.py, adapted so that, instead of 2 | loading any data, it returns the tables referenced by a set of fixtures so we 3 | can truncate them (and no others) quickly after we're finished with them.""" 4 | 5 | import os 6 | import gzip 7 | import zipfile 8 | 9 | from django.conf import settings 10 | from django.core import serializers 11 | from django.db import router, DEFAULT_DB_ALIAS 12 | from django.db.models import get_apps 13 | from django.utils.itercompat import product 14 | 15 | try: 16 | import bz2 17 | has_bz2 = True 18 | except ImportError: 19 | has_bz2 = False 20 | 21 | 22 | def tables_used_by_fixtures(fixture_labels, using=DEFAULT_DB_ALIAS): 23 | """Act like Django's stock loaddata command, but, instead of loading data, 24 | return an iterable of the names of the tables into which data would be 25 | loaded.""" 26 | # Keep a count of the installed objects and fixtures 27 | fixture_count = 0 28 | loaded_object_count = 0 29 | fixture_object_count = 0 30 | tables = set() 31 | 32 | class SingleZipReader(zipfile.ZipFile): 33 | def __init__(self, *args, **kwargs): 34 | zipfile.ZipFile.__init__(self, *args, **kwargs) 35 | if settings.DEBUG: 36 | assert len(self.namelist()) == 1, "Zip-compressed fixtures must contain only one file." 37 | def read(self): 38 | return zipfile.ZipFile.read(self, self.namelist()[0]) 39 | 40 | compression_types = { 41 | None: file, 42 | 'gz': gzip.GzipFile, 43 | 'zip': SingleZipReader 44 | } 45 | if has_bz2: 46 | compression_types['bz2'] = bz2.BZ2File 47 | 48 | app_module_paths = [] 49 | for app in get_apps(): 50 | if hasattr(app, '__path__'): 51 | # It's a 'models/' subpackage 52 | for path in app.__path__: 53 | app_module_paths.append(path) 54 | else: 55 | # It's a models.py module 56 | app_module_paths.append(app.__file__) 57 | 58 | app_fixtures = [os.path.join(os.path.dirname(path), 'fixtures') for path in app_module_paths] 59 | for fixture_label in fixture_labels: 60 | parts = fixture_label.split('.') 61 | 62 | if len(parts) > 1 and parts[-1] in compression_types: 63 | compression_formats = [parts[-1]] 64 | parts = parts[:-1] 65 | else: 66 | compression_formats = compression_types.keys() 67 | 68 | if len(parts) == 1: 69 | fixture_name = parts[0] 70 | formats = serializers.get_public_serializer_formats() 71 | else: 72 | fixture_name, format = '.'.join(parts[:-1]), parts[-1] 73 | if format in serializers.get_public_serializer_formats(): 74 | formats = [format] 75 | else: 76 | formats = [] 77 | 78 | if not formats: 79 | # stderr.write(style.ERROR("Problem installing fixture '%s': %s is 80 | # not a known serialization format.\n" % (fixture_name, format))) 81 | return set() 82 | 83 | if os.path.isabs(fixture_name): 84 | fixture_dirs = [fixture_name] 85 | else: 86 | fixture_dirs = app_fixtures + list(settings.FIXTURE_DIRS) + [''] 87 | 88 | for fixture_dir in fixture_dirs: 89 | # stdout.write("Checking %s for fixtures...\n" % 90 | # humanize(fixture_dir)) 91 | 92 | label_found = False 93 | for combo in product([using, None], formats, compression_formats): 94 | database, format, compression_format = combo 95 | file_name = '.'.join( 96 | p for p in [ 97 | fixture_name, database, format, compression_format 98 | ] 99 | if p 100 | ) 101 | 102 | # stdout.write("Trying %s for %s fixture '%s'...\n" % \ 103 | # (humanize(fixture_dir), file_name, fixture_name)) 104 | full_path = os.path.join(fixture_dir, file_name) 105 | open_method = compression_types[compression_format] 106 | try: 107 | fixture = open_method(full_path, 'r') 108 | if label_found: 109 | fixture.close() 110 | # stderr.write(style.ERROR("Multiple fixtures named 111 | # '%s' in %s. Aborting.\n" % (fixture_name, 112 | # humanize(fixture_dir)))) 113 | return set() 114 | else: 115 | fixture_count += 1 116 | objects_in_fixture = 0 117 | loaded_objects_in_fixture = 0 118 | # stdout.write("Installing %s fixture '%s' from %s.\n" 119 | # % (format, fixture_name, humanize(fixture_dir))) 120 | try: 121 | objects = serializers.deserialize(format, fixture, using=using) 122 | for obj in objects: 123 | objects_in_fixture += 1 124 | if router.allow_syncdb(using, obj.object.__class__): 125 | loaded_objects_in_fixture += 1 126 | tables.add( 127 | obj.object.__class__._meta.db_table) 128 | loaded_object_count += loaded_objects_in_fixture 129 | fixture_object_count += objects_in_fixture 130 | label_found = True 131 | except (SystemExit, KeyboardInterrupt): 132 | raise 133 | except Exception: 134 | fixture.close() 135 | # stderr.write( style.ERROR("Problem installing 136 | # fixture '%s': %s\n" % (full_path, ''.join(tra 137 | # ceback.format_exception(sys.exc_type, 138 | # sys.exc_value, sys.exc_traceback))))) 139 | return set() 140 | fixture.close() 141 | 142 | # If the fixture we loaded contains 0 objects, assume that an 143 | # error was encountered during fixture loading. 144 | if objects_in_fixture == 0: 145 | # stderr.write( style.ERROR("No fixture data found 146 | # for '%s'. (File format may be invalid.)\n" % 147 | # (fixture_name))) 148 | return set() 149 | 150 | except Exception: 151 | # stdout.write("No %s fixture '%s' in %s.\n" % \ (format, 152 | # fixture_name, humanize(fixture_dir))) 153 | pass 154 | 155 | return tables 156 | -------------------------------------------------------------------------------- /django_nose/testcases.py: -------------------------------------------------------------------------------- 1 | from django import test 2 | from django.conf import settings 3 | from django.core import cache, mail 4 | from django.core.management import call_command 5 | from django.db import connections, DEFAULT_DB_ALIAS, transaction 6 | 7 | from django_nose.fixture_tables import tables_used_by_fixtures 8 | from django_nose.utils import uses_mysql 9 | 10 | 11 | __all__ = ['FastFixtureTestCase'] 12 | 13 | 14 | class FastFixtureTestCase(test.TransactionTestCase): 15 | """Test case that loads fixtures once and for all rather than once per test 16 | 17 | Using this can save huge swaths of time while still preserving test 18 | isolation. Fixture data is loaded at class setup time, and the transaction 19 | is committed. Commit and rollback methods are then monkeypatched away (like 20 | in Django's standard TestCase), and each test is run. After each test, the 21 | monkeypatching is temporarily undone, and a rollback is issued, returning 22 | the DB content to the pristine fixture state. Finally, upon class teardown, 23 | the DB is restored to a post-syncdb-like state by deleting the contents of 24 | any table that had been touched by a fixture (keeping infrastructure tables 25 | like django_content_type and auth_permission intact). 26 | 27 | Note that this is like Django's TestCase, not its TransactionTestCase, in 28 | that you cannot do your own commits or rollbacks from within tests. 29 | 30 | For best speed, group tests using the same fixtures into as few classes as 31 | possible. Better still, don't do that, and instead use the fixture-bundling 32 | plugin from django-nose, which does it dynamically at test time. 33 | 34 | """ 35 | cleans_up_after_itself = True # This is the good kind of puppy. 36 | 37 | @classmethod 38 | def setUpClass(cls): 39 | """Turn on manual commits. Load and commit the fixtures.""" 40 | if not test.testcases.connections_support_transactions(): 41 | raise NotImplementedError('%s supports only DBs with transaction ' 42 | 'capabilities.' % cls.__name__) 43 | for db in cls._databases(): 44 | # These MUST be balanced with one leave_* each: 45 | transaction.enter_transaction_management(using=db) 46 | # Don't commit unless we say so: 47 | transaction.managed(True, using=db) 48 | 49 | cls._fixture_setup() 50 | 51 | @classmethod 52 | def tearDownClass(cls): 53 | """Truncate the world, and turn manual commit management back off.""" 54 | cls._fixture_teardown() 55 | for db in cls._databases(): 56 | # Finish off any transactions that may have happened in 57 | # tearDownClass in a child method. 58 | if transaction.is_dirty(using=db): 59 | transaction.commit(using=db) 60 | transaction.leave_transaction_management(using=db) 61 | 62 | @classmethod 63 | def _fixture_setup(cls): 64 | """Load fixture data, and commit.""" 65 | for db in cls._databases(): 66 | if (hasattr(cls, 'fixtures') and 67 | getattr(cls, '_fb_should_setup_fixtures', True)): 68 | # Iff the fixture-bundling test runner tells us we're the first 69 | # suite having these fixtures, set them up: 70 | call_command('loaddata', *cls.fixtures, **{'verbosity': 0, 71 | 'commit': False, 72 | 'database': db}) 73 | # No matter what, to preserve the effect of cursor start-up 74 | # statements... 75 | transaction.commit(using=db) 76 | 77 | @classmethod 78 | def _fixture_teardown(cls): 79 | """Empty (only) the tables we loaded fixtures into, then commit.""" 80 | if hasattr(cls, 'fixtures') and \ 81 | getattr(cls, '_fb_should_teardown_fixtures', True): 82 | # If the fixture-bundling test runner advises us that the next test 83 | # suite is going to reuse these fixtures, don't tear them down. 84 | for db in cls._databases(): 85 | tables = tables_used_by_fixtures(cls.fixtures, using=db) 86 | # TODO: Think about respecting _meta.db_tablespace, not just 87 | # db_table. 88 | if tables: 89 | connection = connections[db] 90 | cursor = connection.cursor() 91 | 92 | # TODO: Rather than assuming that anything added to by a 93 | # fixture can be emptied, remove only what the fixture 94 | # added. This would probably solve input.mozilla.com's 95 | # failures (since worked around) with Site objects; they 96 | # were loading additional Sites with a fixture, and then 97 | # the Django-provided example.com site was evaporating. 98 | if uses_mysql(connection): 99 | cursor.execute('SET FOREIGN_KEY_CHECKS=0') 100 | for table in tables: 101 | # Truncate implicitly commits. 102 | cursor.execute('TRUNCATE `%s`' % table) 103 | # TODO: necessary? 104 | cursor.execute('SET FOREIGN_KEY_CHECKS=1') 105 | else: 106 | for table in tables: 107 | cursor.execute('DELETE FROM %s' % table) 108 | 109 | transaction.commit(using=db) 110 | # cursor.close() # Should be unnecessary, since we committed 111 | # any environment-setup statements that come with opening a new 112 | # cursor when we committed the fixtures. 113 | 114 | def _pre_setup(self): 115 | """Disable transaction methods, and clear some globals.""" 116 | # Repeat stuff from TransactionTestCase, because I'm not calling its 117 | # _pre_setup, because that would load fixtures again. 118 | cache.cache.clear() 119 | settings.TEMPLATE_DEBUG = settings.DEBUG = False 120 | 121 | test.testcases.disable_transaction_methods() 122 | 123 | #self._fixture_setup() 124 | self._urlconf_setup() 125 | mail.outbox = [] 126 | 127 | # Clear site cache in case somebody's mutated Site objects and then 128 | # cached the mutated stuff: 129 | from django.contrib.sites.models import Site 130 | Site.objects.clear_cache() 131 | 132 | def _post_teardown(self): 133 | """Re-enable transaction methods, and roll back any changes. 134 | 135 | Rollback clears any DB changes made by the test so the original fixture 136 | data is again visible. 137 | 138 | """ 139 | # Rollback any mutations made by tests: 140 | test.testcases.restore_transaction_methods() 141 | for db in self._databases(): 142 | transaction.rollback(using=db) 143 | 144 | self._urlconf_teardown() 145 | 146 | # We do not need to close the connection here to prevent 147 | # http://code.djangoproject.com/ticket/7572, since we commit, not 148 | # rollback, the test fixtures and thus any cursor startup statements. 149 | 150 | # Don't call through to superclass, because that would call 151 | # _fixture_teardown() and close the connection. 152 | 153 | @classmethod 154 | def _databases(cls): 155 | if getattr(cls, 'multi_db', False): 156 | return connections 157 | else: 158 | return [DEFAULT_DB_ALIAS] 159 | -------------------------------------------------------------------------------- /django_nose/plugin.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from nose.plugins.base import Plugin 4 | from nose.suite import ContextSuite 5 | 6 | from django.test.testcases import TransactionTestCase, TestCase 7 | 8 | from django_nose.testcases import FastFixtureTestCase 9 | from django_nose.utils import process_tests, is_subclass_at_all 10 | 11 | 12 | class AlwaysOnPlugin(Plugin): 13 | """A plugin that takes no options and is always enabled""" 14 | 15 | def options(self, parser, env): 16 | """Avoid adding a ``--with`` option for this plugin. 17 | 18 | We don't have any options, and this plugin is always enabled, so we 19 | don't want to use superclass's ``options()`` method which would add a 20 | ``--with-*`` option. 21 | 22 | """ 23 | 24 | def configure(self, *args, **kw_args): 25 | super(AlwaysOnPlugin, self).configure(*args, **kw_args) 26 | self.enabled = True # Force this plugin to be always enabled. 27 | 28 | 29 | class ResultPlugin(AlwaysOnPlugin): 30 | """Captures the TestResult object for later inspection 31 | 32 | nose doesn't return the full test result object from any of its runner 33 | methods. Pass an instance of this plugin to the TestProgram and use 34 | ``result`` after running the tests to get the TestResult object. 35 | 36 | """ 37 | name = 'result' 38 | 39 | def finalize(self, result): 40 | self.result = result 41 | 42 | 43 | class DjangoSetUpPlugin(AlwaysOnPlugin): 44 | """Configures Django to set up and tear down the environment 45 | 46 | This allows coverage to report on all code imported and used during the 47 | initialization of the test runner. 48 | 49 | """ 50 | name = 'django setup' 51 | 52 | def __init__(self, runner): 53 | super(DjangoSetUpPlugin, self).__init__() 54 | self.runner = runner 55 | self.sys_stdout = sys.stdout 56 | 57 | def prepareTest(self, test): 58 | """Create the Django DB and model tables, and do other setup. 59 | 60 | This isn't done in begin() because that's too early--the DB has to be 61 | set up *after* the tests are imported so the model registry contains 62 | models defined in tests.py modules. Models are registered at 63 | declaration time by their metaclass. 64 | 65 | prepareTestRunner() might also have been a sane choice, except that, if 66 | some plugin returns something from it, none of the other ones get 67 | called. I'd rather not dink with scores if I don't have to. 68 | 69 | """ 70 | # What is this stdout switcheroo for? 71 | sys_stdout = sys.stdout 72 | sys.stdout = self.sys_stdout 73 | 74 | self.runner.setup_test_environment() 75 | self.old_names = self.runner.setup_databases() 76 | 77 | sys.stdout = sys_stdout 78 | 79 | def finalize(self, result): 80 | self.runner.teardown_databases(self.old_names) 81 | self.runner.teardown_test_environment() 82 | 83 | 84 | class Bucketer(object): 85 | def __init__(self): 86 | # { (frozenset(['users.json']), True): 87 | # [ContextSuite(...), ContextSuite(...)] } 88 | self.buckets = {} 89 | 90 | # All the non-FastFixtureTestCase tests we saw, in the order they came 91 | # in: 92 | self.remainder = [] 93 | 94 | def add(self, test): 95 | """Put a test into a bucket according to its set of fixtures and the 96 | value of its exempt_from_fixture_bundling attr.""" 97 | if is_subclass_at_all(test.context, FastFixtureTestCase): 98 | # We bucket even FFTCs that don't have any fixtures, but it 99 | # shouldn't matter. 100 | key = (frozenset(getattr(test.context, 'fixtures', [])), 101 | getattr(test.context, 102 | 'exempt_from_fixture_bundling', 103 | False)) 104 | self.buckets.setdefault(key, []).append(test) 105 | else: 106 | self.remainder.append(test) 107 | 108 | 109 | class TestReorderer(AlwaysOnPlugin): 110 | """Reorder tests for various reasons.""" 111 | name = 'django-nose-test-reorderer' 112 | 113 | def options(self, parser, env): 114 | super(TestReorderer, self).options(parser, env) # pointless 115 | parser.add_option('--with-fixture-bundling', 116 | action='store_true', 117 | dest='with_fixture_bundling', 118 | default=env.get('NOSE_WITH_FIXTURE_BUNDLING', False), 119 | help='Load a unique set of fixtures only once, even ' 120 | 'across test classes. ' 121 | '[NOSE_WITH_FIXTURE_BUNDLING]') 122 | 123 | def configure(self, options, conf): 124 | super(TestReorderer, self).configure(options, conf) 125 | self.should_bundle = options.with_fixture_bundling 126 | 127 | def _put_transaction_test_cases_last(self, test): 128 | """Reorder tests in the suite so TransactionTestCase-based tests come 129 | last. 130 | 131 | Django has a weird design decision wherein TransactionTestCase doesn't 132 | clean up after itself. Instead, it resets the DB to a clean state only 133 | at the *beginning* of each test: 134 | https://docs.djangoproject.com/en/dev/topics/testing/?from=olddocs# 135 | django. test.TransactionTestCase. Thus, Django reorders tests so 136 | TransactionTestCases all come last. Here we do the same. 137 | 138 | "I think it's historical. We used to have doctests also, adding cleanup 139 | after each unit test wouldn't necessarily clean up after doctests, so 140 | you'd have to clean on entry to a test anyway." was once uttered on 141 | #django-dev. 142 | 143 | """ 144 | 145 | def filthiness(test): 146 | """Return a comparand based on whether a test is guessed to clean 147 | up after itself. 148 | 149 | Django's TransactionTestCase doesn't clean up the DB on teardown, 150 | but it's hard to guess whether subclasses (other than TestCase) do. 151 | We will assume they don't, unless they have a 152 | ``cleans_up_after_itself`` attr set to True. This is reasonable 153 | because the odd behavior of TransactionTestCase is documented, so 154 | subclasses should by default be assumed to preserve it. 155 | 156 | Thus, things will get these comparands (and run in this order): 157 | 158 | * 1: TestCase subclasses. These clean up after themselves. 159 | * 1: TransactionTestCase subclasses with 160 | cleans_up_after_itself=True. These include 161 | FastFixtureTestCases. If you're using the 162 | FixtureBundlingPlugin, it will pull the FFTCs out, reorder 163 | them, and run them first of all. 164 | * 2: TransactionTestCase subclasses. These leave a mess. 165 | * 2: Anything else (including doctests, I hope). These don't care 166 | about the mess you left, because they don't hit the DB or, if 167 | they do, are responsible for ensuring that it's clean (as per 168 | https://docs.djangoproject.com/en/dev/topics/testing/?from= 169 | olddocs#writing-doctests) 170 | 171 | """ 172 | test_class = test.context 173 | if (is_subclass_at_all(test_class, TestCase) or 174 | (is_subclass_at_all(test_class, TransactionTestCase) and 175 | getattr(test_class, 'cleans_up_after_itself', False))): 176 | return 1 177 | return 2 178 | 179 | flattened = [] 180 | process_tests(test, flattened.append) 181 | flattened.sort(key=filthiness) 182 | return ContextSuite(flattened) 183 | 184 | def _bundle_fixtures(self, test): 185 | """Reorder the tests in the suite so classes using identical 186 | sets of fixtures are contiguous. 187 | 188 | I reorder FastFixtureTestCases so ones using identical sets 189 | of fixtures run adjacently. I then put attributes on them 190 | to advise them to not reload the fixtures for each class. 191 | 192 | This takes support.mozilla.com's suite from 123s down to 94s. 193 | 194 | FastFixtureTestCases are the only ones we care about, because 195 | nobody else, in practice, pays attention to the ``_fb`` advisory 196 | bits. We return those first, then any remaining tests in the 197 | order they were received. 198 | 199 | """ 200 | def suite_sorted_by_fixtures(suite): 201 | """Flatten and sort a tree of Suites by the ``fixtures`` members of 202 | their contexts. 203 | 204 | Add ``_fb_should_setup_fixtures`` and 205 | ``_fb_should_teardown_fixtures`` attrs to each test class to advise 206 | it whether to set up or tear down (respectively) the fixtures. 207 | 208 | Return a Suite. 209 | 210 | """ 211 | bucketer = Bucketer() 212 | process_tests(suite, bucketer.add) 213 | 214 | # Lay the bundles of common-fixture-having test classes end to end 215 | # in a single list so we can make a test suite out of them: 216 | flattened = [] 217 | for ((fixtures, is_exempt), 218 | fixture_bundle) in bucketer.buckets.iteritems(): 219 | # Advise first and last test classes in each bundle to set up 220 | # and tear down fixtures and the rest not to: 221 | if fixtures and not is_exempt: 222 | # Ones with fixtures are sure to be classes, which means 223 | # they're sure to be ContextSuites with contexts. 224 | 225 | # First class with this set of fixtures sets up: 226 | first = fixture_bundle[0].context 227 | first._fb_should_setup_fixtures = True 228 | 229 | # Set all classes' 1..n should_setup to False: 230 | for cls in fixture_bundle[1:]: 231 | cls.context._fb_should_setup_fixtures = False 232 | 233 | # Last class tears down: 234 | last = fixture_bundle[-1].context 235 | last._fb_should_teardown_fixtures = True 236 | 237 | # Set all classes' 0..(n-1) should_teardown to False: 238 | for cls in fixture_bundle[:-1]: 239 | cls.context._fb_should_teardown_fixtures = False 240 | 241 | flattened.extend(fixture_bundle) 242 | flattened.extend(bucketer.remainder) 243 | 244 | return ContextSuite(flattened) 245 | 246 | return suite_sorted_by_fixtures(test) 247 | 248 | def prepareTest(self, test): 249 | """Reorder the tests.""" 250 | test = self._put_transaction_test_cases_last(test) 251 | if self.should_bundle: 252 | test = self._bundle_fixtures(test) 253 | return test 254 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | django-nose 3 | =========== 4 | 5 | Features 6 | -------- 7 | 8 | * All the goodness of `nose`_ in your Django tests, like... 9 | 10 | * Testing just your apps by default, not all the standard ones that happen to 11 | be in ``INSTALLED_APPS`` 12 | * Running the tests in one or more specific modules (or apps, or classes, or 13 | folders, or just running a specific test) 14 | * Obviating the need to import all your tests into ``tests/__init__.py``. 15 | This not only saves busy-work but also eliminates the possibility of 16 | accidentally shadowing test classes. 17 | * Taking advantage of all the useful `nose plugins`_ 18 | * Fixture bundling, an optional feature which speeds up your fixture-based 19 | tests by a factor of 4 20 | * Reuse of previously created test DBs, cutting 10 seconds off startup time 21 | * Hygienic TransactionTestCases, which can save you a DB flush per test 22 | * Support for various databases. Tested with MySQL, PostgreSQL, and SQLite. 23 | Others should work as well. 24 | 25 | .. _nose: http://somethingaboutorange.com/mrl/projects/nose/ 26 | .. _nose plugins: http://nose-plugins.jottit.com/ 27 | 28 | 29 | Installation 30 | ------------ 31 | 32 | You can get django-nose from PyPI with... :: 33 | 34 | pip install django-nose 35 | 36 | The development version can be installed with... :: 37 | 38 | pip install -e git://github.com/jbalogh/django-nose.git#egg=django-nose 39 | 40 | Since django-nose extends Django's built-in test command, you should add it to 41 | your ``INSTALLED_APPS`` in ``settings.py``:: 42 | 43 | INSTALLED_APPS = ( 44 | ... 45 | 'django_nose', 46 | ... 47 | ) 48 | 49 | Then set ``TEST_RUNNER`` in ``settings.py``:: 50 | 51 | TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' 52 | 53 | 54 | Use 55 | --- 56 | 57 | The day-to-day use of django-nose is mostly transparent; just run ``./manage.py 58 | test`` as usual. 59 | 60 | See ``./manage.py help test`` for all the options nose provides, and look to 61 | the `nose docs`_ for more help with nose. 62 | 63 | .. _nose docs: http://somethingaboutorange.com/mrl/projects/nose/ 64 | 65 | 66 | Enabling Database Reuse 67 | ----------------------- 68 | 69 | You can save several seconds at the beginning and end of your test suite by 70 | reusing the test database from the last run. To do this, set the environment 71 | variable ``REUSE_DB`` to 1:: 72 | 73 | REUSE_DB=1 ./manage.py test 74 | 75 | The one new wrinkle is that, whenever your DB schema changes, you should leave 76 | the flag off the next time you run tests. This will cue the test runner to 77 | reinitialize the test database. 78 | 79 | Also, REUSE_DB is not compatible with TransactionTestCases that leave junk in 80 | the DB, so be sure to make your TransactionTestCases hygienic (see below) if 81 | you want to use it. 82 | 83 | 84 | Enabling Fast Fixtures 85 | ---------------------- 86 | 87 | django-nose includes a fixture bundler which drastically speeds up your tests 88 | by eliminating redundant setup of Django test fixtures. To use it... 89 | 90 | 1. Subclass ``django_nose.FastFixtureTestCase`` instead of 91 | ``django.test.TestCase``. (I like to import it ``as TestCase`` in my 92 | project's ``tests/__init__.py`` and then import it from there into my actual 93 | tests. Then it's easy to sub the base class in and out.) This alone will 94 | cause fixtures to load once per class rather than once per test. 95 | 2. Activate fixture bundling by passing the ``--with-fixture-bundling`` option 96 | to ``./manage.py test``. This loads each unique set of fixtures only once, 97 | even across class, module, and app boundaries. 98 | 99 | How Fixture Bundling Works 100 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 101 | 102 | The fixture bundler reorders your test classes so that ones with identical sets 103 | of fixtures run adjacently. It then advises the first of each series to load 104 | the fixtures once for all of them (and the remaining ones not to bother). It 105 | also advises the last to tear them down. Depending on the size and repetition 106 | of your fixtures, you can expect a 25% to 50% speed increase. 107 | 108 | Incidentally, the author prefers to avoid Django fixtures, as they encourage 109 | irrelevant coupling between tests and make tests harder to comprehend and 110 | modify. For future tests, it is better to use the "model maker" pattern, 111 | creating DB objects programmatically. This way, tests avoid setup they don't 112 | need, and there is a clearer tie between a test and the exact state it 113 | requires. The fixture bundler is intended to make existing tests, which have 114 | already committed to fixtures, more tolerable. 115 | 116 | Troubleshooting 117 | ~~~~~~~~~~~~~~~ 118 | 119 | If using ``--with-fixture-bundling`` causes test failures, it likely indicates 120 | an order dependency between some of your tests. Here are the most frequent 121 | sources of state leakage we have encountered: 122 | 123 | * Locale activation, which is maintained in a threadlocal variable. Be sure to 124 | reset your locale selection between tests. 125 | * memcached contents. Be sure to flush between tests. Many test superclasses do 126 | this automatically. 127 | 128 | It's also possible that you have ``post_save`` signal handlers which create 129 | additional database rows while loading the fixtures. ``FastFixtureTestCase`` 130 | isn't yet smart enough to notice this and clean up after it, so you'll have to 131 | go back to plain old ``TestCase`` for now. 132 | 133 | Exempting A Class From Bundling 134 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 135 | 136 | In some unusual cases, it is desirable to exempt a test class from fixture 137 | bundling, forcing it to set up and tear down its fixtures at the class 138 | boundaries. For example, we might have a ``TestCase`` subclass which sets up 139 | some state outside the DB in ``setUpClass`` and tears it down in 140 | ``tearDownClass``, and it might not be possible to adapt those routines to heed 141 | the advice of the fixture bundler. In such a case, simply set the 142 | ``exempt_from_fixture_bundling`` attribute of the test class to ``True``. 143 | 144 | 145 | Speedy Hygienic TransactionTestCases 146 | ------------------------------------ 147 | 148 | Unlike the stock Django test runner, django-nose lets you write custom 149 | TransactionTestCase subclasses which expect to start with an unmarred DB, 150 | saving an entire DB flush per test. 151 | 152 | Background 153 | ~~~~~~~~~~ 154 | 155 | The default Django TransactionTestCase class `can leave the DB in an unclean 156 | state`_ when it's done. To compensate, TransactionTestCase does a 157 | time-consuming flush of the DB *before* each test to ensure it begins with a 158 | clean slate. Django's stock test runner then runs TransactionTestCases last so 159 | they don't wreck the environment for better-behaved tests. django-nose 160 | replicates this behavior. 161 | 162 | Escaping the Grime 163 | ~~~~~~~~~~~~~~~~~~ 164 | 165 | Some people, however, have made subclasses of TransactionTestCase that clean up 166 | after themselves (and can do so efficiently, since they know what they've 167 | changed). Like TestCase, these may assume they start with a clean DB. However, 168 | any TransactionTestCases that run before them and leave a mess could cause them 169 | to fail spuriously. 170 | 171 | django-nose offers to fix this. If you include a special attribute on your 172 | well-behaved TransactionTestCase... :: 173 | 174 | class MyNiceTestCase(TransactionTestCase): 175 | cleans_up_after_itself = True 176 | 177 | ...django-nose will run it before any of those nasty, trash-spewing test cases. 178 | You can thus enjoy a big speed boost any time you make a TransactionTestCase 179 | clean up after itself: skipping a whole DB flush before every test. With a 180 | large schema, this can save minutes of IO. 181 | 182 | django-nose's own FastFixtureTestCase uses this feature, even though it 183 | ultimately acts more like a TestCase than a TransactionTestCase. 184 | 185 | .. _can leave the DB in an unclean state: https://docs.djangoproject.com/en/dev/topics/testing/?from=olddocs#django.test.TransactionTestCase 186 | 187 | 188 | Test-Only Models 189 | ---------------- 190 | 191 | If you have a model that is used only by tests (for example, to test an 192 | abstract model base class), you can put it in any file that's imported in the 193 | course of loading tests. For example, if the tests that need it are in 194 | ``test_models.py``, you can put the model in there, too. django-nose will make 195 | sure its DB table gets created. 196 | 197 | 198 | Using With South 199 | ---------------- 200 | 201 | `South`_ installs its own test command that turns off migrations during 202 | testing. Make sure that django-nose comes *after* ``south`` in 203 | ``INSTALLED_APPS`` so that django_nose's test command is used. 204 | 205 | .. _South: http://south.aeracode.org/ 206 | 207 | 208 | Always Passing The Same Options 209 | ------------------------------- 210 | 211 | To always set the same command line options you can use a `nose.cfg or 212 | setup.cfg`_ (as usual) or you can specify them in settings.py like this:: 213 | 214 | NOSE_ARGS = ['--failed', '--stop'] 215 | 216 | .. _nose.cfg or setup.cfg: http://somethingaboutorange.com/mrl/projects/nose/0.11.2/usage.html#configuration 217 | 218 | 219 | Custom Plugins 220 | -------------- 221 | 222 | If you need to `make custom plugins`_, you can define each plugin class 223 | somewhere within your app and load them from settings.py like this:: 224 | 225 | NOSE_PLUGINS = [ 226 | 'yourapp.tests.plugins.SystematicDysfunctioner', 227 | # ... 228 | ] 229 | 230 | Just like middleware or anything else, each string must be a dot-separated, 231 | importable path to an actual class. Each plugin class will be instantiated and 232 | added to the Nose test runner. 233 | 234 | .. _make custom plugins: http://somethingaboutorange.com/mrl/projects/nose/0.11.2/plugins.html#writing-plugins 235 | 236 | 237 | Older Versions of Django 238 | ------------------------ 239 | Upgrading from Django <= 1.3 to Django 1.4 240 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 241 | In versions of Django < 1.4 the project folder was in fact a python package as 242 | well (note the __init__.py in your project root). In Django 1.4, there is no 243 | such file and thus the project is not a python module. 244 | 245 | **When you upgrade your Django project to the Django 1.4 layout, you need to 246 | remove the __init__.py file in the root of your project (and move any python 247 | files that reside there other than the manage.py) otherwise you will get a 248 | `ImportError: No module named urls` exception.** 249 | 250 | This happens because Nose will intelligently try to populate your sys.path, and 251 | in this particular case includes your parent directory if your project has a 252 | __init__.py file (see: https://github.com/nose-devs/nose/blob/release_1.1.2/nose/importer.py#L134). 253 | 254 | This means that even though you have set up your directory structure properly and 255 | set your `ROOT_URLCONF='my_project.urls'` to match the new structure, when running 256 | django-nose's test runner it will try to find your urls.py file in `'my_project.my_project.urls'`. 257 | 258 | 259 | 260 | 261 | Upgrading from Django < 1.2 262 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 263 | 264 | Django 1.2 switches to a `class-based test runner`_. To use django-nose 265 | with Django 1.2, change your ``TEST_RUNNER`` from ``django_nose.run_tests`` to 266 | ``django_nose.NoseTestSuiteRunner``. 267 | 268 | ``django_nose.run_tests`` will continue to work in Django 1.2 but will raise a 269 | warning. In Django 1.3, it will stop working. 270 | 271 | If you were using ``django_nose.run_gis_tests``, you should also switch to 272 | ``django_nose.NoseTestSuiteRunner`` and use one of the `spatial backends`_ in 273 | your ``DATABASES`` settings. 274 | 275 | .. _class-based test runner: http://docs.djangoproject.com/en/dev/releases/1.2/#function-based-test-runners 276 | .. _spatial backends: http://docs.djangoproject.com/en/dev/ref/contrib/gis/db-api/#id1 277 | 278 | Django 1.1 279 | ~~~~~~~~~~ 280 | 281 | If you want to use django-nose with Django 1.1, use 282 | https://github.com/jbalogh/django-nose/tree/django-1.1 or 283 | http://pypi.python.org/pypi/django-nose/0.0.3. 284 | 285 | Django 1.0 286 | ~~~~~~~~~~ 287 | 288 | django-nose does not support Django 1.0. 289 | 290 | 291 | Recent Version History 292 | ---------------------- 293 | 294 | 1.1 (2012-05-19) 295 | * Django TransactionTestCases don't clean up after themselves; they leave 296 | junk in the DB and clean it up only on ``_pre_setup``. Thus, Django makes 297 | sure these tests run last. Now django-nose does, too. This means one fewer 298 | source of failures on existing projects. (Erik Rose) 299 | * Add support for hygienic TransactionTestCases. (Erik Rose) 300 | * Support models that are used only for tests. Just put them in any file 301 | imported in the course of loading tests. No more crazy hacks necessary. 302 | (Erik Rose) 303 | * Make the fixture bundler more conservative, fixing some conceivable 304 | situations in which fixtures would not appear as intended if a 305 | TransactionTestCase found its way into the middle of a bundle. (Erik Rose) 306 | * Fix an error that would surface when using SQLAlchemy with connection 307 | pooling. (Roger Hu) 308 | * Gracefully ignore the new ``--liveserver`` option introduced in Django 1.4; 309 | don't let it through to nose. (Adam DePue) 310 | 311 | 1.0 (2012-03-12) 312 | * New fixture-bundling plugin for avoiding needless fixture setup (Erik Rose) 313 | * Moved FastFixtureTestCase in from test-utils, so now all the 314 | fixture-bundling stuff is in one library. (Erik Rose) 315 | * Added the REUSE_DB setting for faster startup and shutdown. (Erik Rose) 316 | * Fixed a crash when printing options with certain verbosities. (Daniel Abel) 317 | * Broke hard dependency on MySQL. Support PostgreSQL. (Roger Hu) 318 | * Support SQLite, both memory- and disk-based. (Roger Hu and Erik Rose) 319 | * Nail down versions of the package requirements. (Daniel Mizyrycki) 320 | 321 | 0.1.3 (2010-04-15) 322 | * Even better coverage support (rozza) 323 | * README fixes (carljm and ionelmc) 324 | * optparse OptionGroups are handled better (outofculture) 325 | * nose plugins are loaded before listing options 326 | 327 | See more in changelog.txt. 328 | -------------------------------------------------------------------------------- /django_nose/runner.py: -------------------------------------------------------------------------------- 1 | """Django test runner that invokes nose. 2 | 3 | You can use... :: 4 | 5 | NOSE_ARGS = ['list', 'of', 'args'] 6 | 7 | in settings.py for arguments that you want always passed to nose. 8 | 9 | """ 10 | import new 11 | import os 12 | import sys 13 | from optparse import make_option 14 | 15 | from django.conf import settings 16 | from django.core import exceptions 17 | from django.core.management.base import BaseCommand 18 | from django.core.management.color import no_style 19 | from django.core.management.commands.loaddata import Command 20 | from django.db import connections, transaction, DEFAULT_DB_ALIAS, models 21 | from django.db.backends.creation import BaseDatabaseCreation 22 | from django.test.simple import DjangoTestSuiteRunner 23 | from django.utils.importlib import import_module 24 | 25 | import nose.core 26 | 27 | from django_nose.plugin import DjangoSetUpPlugin, ResultPlugin, TestReorderer 28 | from django_nose.utils import uses_mysql 29 | 30 | try: 31 | any 32 | except NameError: 33 | def any(iterable): 34 | for element in iterable: 35 | if element: 36 | return True 37 | return False 38 | 39 | 40 | __all__ = ['BasicNoseRunner', 'NoseTestSuiteRunner'] 41 | 42 | 43 | # This is a table of Django's "manage.py test" options which 44 | # correspond to nosetests options with a different name: 45 | OPTION_TRANSLATION = {'--failfast': '-x', 46 | '--nose-verbosity': '--verbosity'} 47 | 48 | 49 | def translate_option(opt): 50 | if '=' in opt: 51 | long_opt, value = opt.split('=', 1) 52 | return '%s=%s' % (translate_option(long_opt), value) 53 | return OPTION_TRANSLATION.get(opt, opt) 54 | 55 | 56 | # Django v1.2 does not have a _get_test_db_name() function. 57 | if not hasattr(BaseDatabaseCreation, '_get_test_db_name'): 58 | def _get_test_db_name(self): 59 | TEST_DATABASE_PREFIX = 'test_' 60 | 61 | if self.connection.settings_dict['TEST_NAME']: 62 | return self.connection.settings_dict['TEST_NAME'] 63 | return TEST_DATABASE_PREFIX + self.connection.settings_dict['NAME'] 64 | 65 | BaseDatabaseCreation._get_test_db_name = _get_test_db_name 66 | 67 | 68 | def _get_plugins_from_settings(): 69 | plugins = (list(getattr(settings, 'NOSE_PLUGINS', [])) + 70 | ['django_nose.plugin.TestReorderer']) 71 | for plug_path in plugins: 72 | try: 73 | dot = plug_path.rindex('.') 74 | except ValueError: 75 | raise exceptions.ImproperlyConfigured( 76 | "%s isn't a Nose plugin module" % plug_path) 77 | p_mod, p_classname = plug_path[:dot], plug_path[dot + 1:] 78 | 79 | try: 80 | mod = import_module(p_mod) 81 | except ImportError, e: 82 | raise exceptions.ImproperlyConfigured( 83 | 'Error importing Nose plugin module %s: "%s"' % (p_mod, e)) 84 | 85 | try: 86 | p_class = getattr(mod, p_classname) 87 | except AttributeError: 88 | raise exceptions.ImproperlyConfigured( 89 | 'Nose plugin module "%s" does not define a "%s"' % 90 | (p_mod, p_classname)) 91 | 92 | yield p_class() 93 | 94 | 95 | def _get_options(): 96 | """Return all nose options that don't conflict with django options.""" 97 | cfg_files = nose.core.all_config_files() 98 | manager = nose.core.DefaultPluginManager() 99 | config = nose.core.Config(env=os.environ, files=cfg_files, plugins=manager) 100 | config.plugins.addPlugins(list(_get_plugins_from_settings())) 101 | options = config.getParser()._get_all_options() 102 | 103 | # copy nose's --verbosity option and rename to --nose-verbosity 104 | verbosity = [o for o in options if o.get_opt_string() == '--verbosity'][0] 105 | verbosity_attrs = dict((attr, getattr(verbosity, attr)) 106 | for attr in verbosity.ATTRS 107 | if attr not in ('dest', 'metavar')) 108 | options.append(make_option('--nose-verbosity', 109 | dest='nose_verbosity', 110 | metavar='NOSE_VERBOSITY', 111 | **verbosity_attrs)) 112 | 113 | django_opts = [opt.dest for opt in BaseCommand.option_list] + ['version'] 114 | return tuple(o for o in options if o.dest not in django_opts and 115 | o.action != 'help') 116 | 117 | 118 | class BasicNoseRunner(DjangoTestSuiteRunner): 119 | """Facade that implements a nose runner in the guise of a Django runner 120 | 121 | You shouldn't have to use this directly unless the additions made by 122 | ``NoseTestSuiteRunner`` really bother you. They shouldn't, because they're 123 | all off by default. 124 | 125 | """ 126 | __test__ = False 127 | 128 | # Replace the builtin command options with the merged django/nose options: 129 | options = _get_options() 130 | 131 | def run_suite(self, nose_argv): 132 | result_plugin = ResultPlugin() 133 | plugins_to_add = [DjangoSetUpPlugin(self), 134 | result_plugin, 135 | TestReorderer()] 136 | 137 | for plugin in _get_plugins_from_settings(): 138 | plugins_to_add.append(plugin) 139 | 140 | nose.core.TestProgram(argv=nose_argv, exit=False, 141 | addplugins=plugins_to_add) 142 | return result_plugin.result 143 | 144 | def run_tests(self, test_labels, extra_tests=None): 145 | """Run the unit tests for all the test names in the provided list. 146 | 147 | Test names specified may be file or module names, and may optionally 148 | indicate the test case to run by separating the module or file name 149 | from the test case name with a colon. Filenames may be relative or 150 | absolute. 151 | 152 | N.B.: The test_labels argument *MUST* be a sequence of 153 | strings, *NOT* just a string object. (Or you will be 154 | specifying tests for for each character in your string, and 155 | not the whole string. 156 | 157 | Examples: 158 | 159 | runner.run_tests( ('test.module',) ) 160 | runner.run_tests(['another.test:TestCase.test_method']) 161 | runner.run_tests(['a.test:TestCase']) 162 | runner.run_tests(['/path/to/test/file.py:test_function']) 163 | runner.run_tests( ('test.module', 'a.test:TestCase') ) 164 | 165 | Note: the extra_tests argument is currently ignored. You can 166 | run old non-nose code that uses it without totally breaking, 167 | but the extra tests will not be run. Maybe later. 168 | 169 | Returns the number of tests that failed. 170 | 171 | """ 172 | nose_argv = (['nosetests'] + list(test_labels)) 173 | if hasattr(settings, 'NOSE_ARGS'): 174 | nose_argv.extend(settings.NOSE_ARGS) 175 | 176 | # Skip over 'manage.py test' and any arguments handled by django. 177 | django_opts = ['--noinput', '--liveserver'] 178 | for opt in BaseCommand.option_list: 179 | django_opts.extend(opt._long_opts) 180 | django_opts.extend(opt._short_opts) 181 | 182 | nose_argv.extend(translate_option(opt) for opt in sys.argv[1:] 183 | if opt.startswith('-') 184 | and not any(opt.startswith(d) for d in django_opts)) 185 | 186 | # if --nose-verbosity was omitted, pass Django verbosity to nose 187 | if ('--verbosity' not in nose_argv and 188 | not any(opt.startswith('--verbosity=') for opt in nose_argv)): 189 | nose_argv.append('--verbosity=%s' % str(self.verbosity)) 190 | 191 | if self.verbosity >= 1: 192 | print ' '.join(nose_argv) 193 | 194 | result = self.run_suite(nose_argv) 195 | # suite_result expects the suite as the first argument. Fake it. 196 | return self.suite_result({}, result) 197 | 198 | 199 | _old_handle = Command.handle 200 | 201 | 202 | def _foreign_key_ignoring_handle(self, *fixture_labels, **options): 203 | """Wrap the the stock loaddata to ignore foreign key 204 | checks so we can load circular references from fixtures. 205 | 206 | This is monkeypatched into place in setup_databases(). 207 | 208 | """ 209 | using = options.get('database', DEFAULT_DB_ALIAS) 210 | commit = options.get('commit', True) 211 | connection = connections[using] 212 | 213 | # MySQL stinks at loading circular references: 214 | if uses_mysql(connection): 215 | cursor = connection.cursor() 216 | cursor.execute('SET foreign_key_checks = 0') 217 | 218 | _old_handle(self, *fixture_labels, **options) 219 | 220 | if uses_mysql(connection): 221 | cursor = connection.cursor() 222 | cursor.execute('SET foreign_key_checks = 1') 223 | 224 | if commit: 225 | connection.close() 226 | 227 | 228 | def _skip_create_test_db(self, verbosity=1, autoclobber=False): 229 | """Database creation class that skips both creation and flushing 230 | 231 | The idea is to re-use the perfectly good test DB already created by an 232 | earlier test run, cutting the time spent before any tests run from 5-13s 233 | (depending on your I/O luck) down to 3. 234 | 235 | """ 236 | # Notice that the DB supports transactions. Originally, this was done in 237 | # the method this overrides. Django v1.2 does not have the confirm 238 | # function. Added in https://code.djangoproject.com/ticket/12991. 239 | if callable(getattr(self.connection.features, 'confirm', None)): 240 | self.connection.features.confirm() 241 | else: 242 | can_rollback = self._rollback_works() 243 | self.connection.settings_dict['SUPPORTS_TRANSACTIONS'] = can_rollback 244 | 245 | return self._get_test_db_name() 246 | 247 | 248 | def _reusing_db(): 249 | """Return whether the ``REUSE_DB`` flag was passed""" 250 | return os.getenv('REUSE_DB', 'false').lower() in ('true', '1', '') 251 | 252 | 253 | def _can_support_reuse_db(connection): 254 | """Return whether it makes any sense to 255 | use REUSE_DB with the backend of a connection.""" 256 | # Perhaps this is a SQLite in-memory DB. Those are created implicitly when 257 | # you try to connect to them, so our usual test doesn't work. 258 | return not connection.creation._get_test_db_name() == ':memory:' 259 | 260 | 261 | def _should_create_database(connection): 262 | """Return whether we should recreate the given DB. 263 | 264 | This is true if the DB doesn't exist or the REUSE_DB env var isn't truthy. 265 | 266 | """ 267 | # TODO: Notice when the Model classes change and return True. Worst case, 268 | # we can generate sqlall and hash it, though it's a bit slow (2 secs) and 269 | # hits the DB for no good reason. Until we find a faster way, I'm inclined 270 | # to keep making people explicitly saying REUSE_DB if they want to reuse 271 | # the DB. 272 | 273 | if not _can_support_reuse_db(connection): 274 | return True 275 | 276 | # Notice whether the DB exists, and create it if it doesn't: 277 | try: 278 | connection.cursor() 279 | except StandardError: # TODO: Be more discerning but still DB agnostic. 280 | return True 281 | return not _reusing_db() 282 | 283 | 284 | def _mysql_reset_sequences(style, connection): 285 | """Return a list of SQL statements needed to 286 | reset all sequences for Django tables.""" 287 | tables = connection.introspection.django_table_names(only_existing=True) 288 | flush_statements = connection.ops.sql_flush( 289 | style, tables, connection.introspection.sequence_list()) 290 | 291 | # connection.ops.sequence_reset_sql() is not implemented for MySQL, 292 | # and the base class just returns []. TODO: Implement it by pulling 293 | # the relevant bits out of sql_flush(). 294 | return [s for s in flush_statements if s.startswith('ALTER')] 295 | # Being overzealous and resetting the sequences on non-empty tables 296 | # like django_content_type seems to be fine in MySQL: adding a row 297 | # afterward does find the correct sequence number rather than 298 | # crashing into an existing row. 299 | 300 | 301 | class NoseTestSuiteRunner(BasicNoseRunner): 302 | """A runner that optionally skips DB creation 303 | 304 | Monkeypatches connection.creation to let you skip creating databases if 305 | they already exist. Your tests will start up much faster. 306 | 307 | To opt into this behavior, set the environment variable ``REUSE_DB`` to 308 | something that isn't "0" or "false" (case insensitive). 309 | 310 | """ 311 | 312 | def _get_models_for_connection(self, connection): 313 | """Return a list of models for a connection.""" 314 | tables = connection.introspection.get_table_list(connection.cursor()) 315 | return [m for m in models.loading.cache.get_models() if 316 | m._meta.db_table in tables] 317 | 318 | def setup_databases(self): 319 | for alias in connections: 320 | connection = connections[alias] 321 | creation = connection.creation 322 | test_db_name = creation._get_test_db_name() 323 | 324 | # Mess with the DB name so other things operate on a test DB 325 | # rather than the real one. This is done in create_test_db when 326 | # we don't monkeypatch it away with _skip_create_test_db. 327 | orig_db_name = connection.settings_dict['NAME'] 328 | connection.settings_dict['NAME'] = test_db_name 329 | 330 | if _should_create_database(connection): 331 | # We're not using _skip_create_test_db, so put the DB name 332 | # back: 333 | connection.settings_dict['NAME'] = orig_db_name 334 | 335 | # Since we replaced the connection with the test DB, closing 336 | # the connection will avoid pooling issues with SQLAlchemy. The 337 | # issue is trying to CREATE/DROP the test database using a 338 | # connection to a DB that was established with that test DB. 339 | # MySQLdb doesn't allow it, and SQLAlchemy attempts to reuse 340 | # the existing connection from its pool. 341 | connection.close() 342 | else: 343 | # Reset auto-increment sequences. Apparently, SUMO's tests are 344 | # horrid and coupled to certain numbers. 345 | cursor = connection.cursor() 346 | style = no_style() 347 | 348 | if uses_mysql(connection): 349 | reset_statements = _mysql_reset_sequences( 350 | style, connection) 351 | else: 352 | reset_statements = connection.ops.sequence_reset_sql( 353 | style, self._get_models_for_connection(connection)) 354 | 355 | for reset_statement in reset_statements: 356 | cursor.execute(reset_statement) 357 | 358 | # Django v1.3 (https://code.djangoproject.com/ticket/9964) 359 | # starts using commit_unless_managed() for individual 360 | # connections. Backwards compatibility for Django 1.2 is to use 361 | # the generic transaction function. 362 | transaction.commit_unless_managed(using=connection.alias) 363 | 364 | # Each connection has its own creation object, so this affects 365 | # only a single connection: 366 | creation.create_test_db = new.instancemethod( 367 | _skip_create_test_db, creation, creation.__class__) 368 | 369 | Command.handle = _foreign_key_ignoring_handle 370 | 371 | # With our class patch, does nothing but return some connection 372 | # objects: 373 | return super(NoseTestSuiteRunner, self).setup_databases() 374 | 375 | def teardown_databases(self, *args, **kwargs): 376 | """Leave those poor, reusable databases alone if REUSE_DB is true.""" 377 | if not _reusing_db(): 378 | return super(NoseTestSuiteRunner, self).teardown_databases( 379 | *args, **kwargs) 380 | # else skip tearing down the DB so we can reuse it next time 381 | --------------------------------------------------------------------------------