Repository: django-extensions/django-extensions

Issue:
Test Runner Issue
Somehow django 1.6 does not run all the tests:

https://travis-ci.org/django-extensions/django-extensions/jobs/16780166#L132

It was running only 6, meanwhile other django versions where running 35 tests:

https://travis-ci.org/django-extensions/django-extensions/jobs/16780167#L173

So I have made this change https://github.com/django-extensions/django-extensions/commit/5285fd664c7ff4c0fee98aef03de8d1d91b22c1d

It works for django 1.6 but somehow does not work for django 1.5 and 1.4:

https://travis-ci.org/django-extensions/django-extensions/builds/16789212

Maybe the best solution would be to change the way we use the test runner, but since I don't know much about it I'll leave it here.



Discussion:
I agree we should look at the way we do tests. The current run_test script comes from the old days I think :)

We should probably write a testrunner and/or testcase classes for ourselves to do any custom setup and teardown. So we would not need a seperate run_tests.py anymore for that and have all the tests run fine from other peoples Django projects as well.

As workaround until such time I committed a change to have the old behavior for <1.6 so that our tests work again for all versions.

Awesome, thanks.

@trbs any updates on this?

Not yet, sorry.

Maybe one year later? ;)

Hehe :) It's clearly not been on my priority list... sorry about that.

Right now Travis seems to indicate that we run 57, 57, 40 and 40 tests for Django 1.7, 1.6, 1,5 and 1,4 respectively. Since 1.5 and 1.4 are very old now and 1.5 is unsupported I don't think we have to care too much about those.

However the tests on master are broken so we might need to take a good look at the testsuite anyways.

Yay!

@camilonova what are your thoughts on replacing our current test suite with something based on py.test like suggested by https://github.com/benjaminabel ?

Think that might solve this issue as well right ?

Yes it is.

@benjaminabel what do you think?

@camilonova  `py.test django` find 59 tests for all django versions, so I think it could close the issue. However this need a little more work because all tests don't pass for all django versions.


Resolving patch:
diff --git a/conftest.py b/conftest.py
new file mode 100644
--- /dev/null
+++ b/conftest.py
@@ -0,0 +1,73 @@
+from django.conf import settings
+
+
+def pytest_configure():
+    import sys
+    import tempfile
+
+    try:
+        import django  # NOQA
+    except ImportError:
+        print("Error: missing test dependency:")
+        print("  django library is needed to run test suite")
+        print("  you can install it with 'pip install django'")
+        print("  or use tox to automatically handle test dependencies")
+        sys.exit(1)
+
+    try:
+        import shortuuid  # NOQA
+    except ImportError:
+        print("Error: missing test dependency:")
+        print("  shortuuid library is needed to run test suite")
+        print("  you can install it with 'pip install shortuuid'")
+        print("  or use tox to automatically handle test dependencies")
+        sys.exit(1)
+
+    try:
+        import dateutil  # NOQA
+    except ImportError:
+        print("Error: missing test dependency:")
+        print("  dateutil library is needed to run test suite")
+        print("  you can install it with 'pip install python-dateutil'")
+        print("  or use tox to automatically handle test dependencies")
+        sys.exit(1)
+
+    try:
+        import six  # NOQA
+    except ImportError:
+        print("Error: missing test dependency:")
+        print("  six library is needed to run test suite")
+        print("  you can install it with 'pip install six'")
+        print("  or use tox to automatically handle test dependencies")
+        sys.exit(1)
+
+    # Dynamically configure the Django settings with the minimum necessary to
+    # get Django running tests.
+    settings.configure(
+        INSTALLED_APPS=[
+            'django.contrib.auth',
+            'django.contrib.contenttypes',
+            'django.contrib.admin',
+            'django.contrib.sessions',
+            'tests.testapp',
+            'django_extensions',
+        ],
+        MIDDLEWARE_CLASSES=(
+            'django.contrib.sessions.middleware.SessionMiddleware',
+            'django.contrib.auth.middleware.AuthenticationMiddleware',
+            'django.contrib.messages.middleware.MessageMiddleware',
+        ),
+        # Django replaces this, but it still wants it. *shrugs*
+        DATABASE_ENGINE='django.db.backends.sqlite3',
+        DATABASES={
+            'default': {
+                'ENGINE': 'django.db.backends.sqlite3',
+                'NAME': ':memory:',
+            }
+        },
+        MEDIA_ROOT='/tmp/django_extensions_test_media/',
+        MEDIA_PATH='/media/',
+        ROOT_URLCONF='tests.urls',
+        DEBUG=True,
+        TEMPLATE_DEBUG=True,
+    )


Test patch:
diff --git a/django_extensions/tests/__init__.py b/django_extensions/tests/__init__.py
deleted file mode 100644
--- a/django_extensions/tests/__init__.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from django.db import models  # NOQA
-from django_extensions.tests.test_dumpscript import DumpScriptTests
-from django_extensions.tests.utils import TruncateLetterTests
-from django_extensions.tests.json_field import JsonFieldTest
-from django_extensions.tests.uuid_field import (UUIDFieldTest,
-                                                PostgreSQLUUIDFieldTest)
-from django_extensions.tests.shortuuid_field import ShortUUIDFieldTest
-from django_extensions.tests.fields import AutoSlugFieldTest
-from django_extensions.tests.management_command import CommandTest, \
-    ShowTemplateTagsTests, UpdatePermissionsTests, CommandSignalTests
-from django_extensions.tests.test_templatetags import TemplateTagsTests
-from django_extensions.tests.test_clean_pyc import CleanPycTests
-from django_extensions.tests.test_compile_pyc import CompilePycTests
-
-__test_classes__ = [
-    DumpScriptTests, JsonFieldTest, UUIDFieldTest, AutoSlugFieldTest,
-    CommandTest, ShowTemplateTagsTests, TruncateLetterTests, TemplateTagsTests,
-    ShortUUIDFieldTest, PostgreSQLUUIDFieldTest, CleanPycTests, CompilePycTests,
-    UpdatePermissionsTests, CommandSignalTests
-]
-
-try:
-    from django_extensions.tests.encrypted_fields import EncryptedFieldsTestCase
-    __test_classes__.append(EncryptedFieldsTestCase)
-except ImportError:
-    pass
diff --git a/django_extensions/tests/shortuuid_field.py b/django_extensions/tests/shortuuid_field.py
deleted file mode 100644
--- a/django_extensions/tests/shortuuid_field.py
+++ /dev/null
@@ -1,39 +0,0 @@
-import six
-from django.conf import settings
-from django.core.management import call_command
-from django.db.models import loading
-from django.utils import unittest
-
-from django_extensions.tests.testapp.models import ShortUUIDTestModel_field, ShortUUIDTestModel_pk, ShortUUIDTestAgregateModel, ShortUUIDTestManyToManyModel
-
-
-class ShortUUIDFieldTest(unittest.TestCase):
-    def setUp(self):
-        self.old_installed_apps = settings.INSTALLED_APPS
-        settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
-        settings.INSTALLED_APPS.append('django_extensions.tests')
-        loading.cache.loaded = False
-        call_command('syncdb', verbosity=0)
-
-    def tearDown(self):
-        settings.INSTALLED_APPS = self.old_installed_apps
-
-    def testUUIDFieldCreate(self):
-        j = ShortUUIDTestModel_field.objects.create(a=6, uuid_field=six.u('vytxeTZskVKR7C7WgdSP3d'))
-        self.assertEqual(j.uuid_field, six.u('vytxeTZskVKR7C7WgdSP3d'))
-
-    def testUUIDField_pkCreate(self):
-        j = ShortUUIDTestModel_pk.objects.create(uuid_field=six.u('vytxeTZskVKR7C7WgdSP3d'))
-        self.assertEqual(j.uuid_field, six.u('vytxeTZskVKR7C7WgdSP3d'))
-        self.assertEqual(j.pk, six.u('vytxeTZskVKR7C7WgdSP3d'))
-
-    def testUUIDField_pkAgregateCreate(self):
-        j = ShortUUIDTestAgregateModel.objects.create(a=6)
-        self.assertEqual(j.a, 6)
-        self.assertIsInstance(j.pk, six.string_types)
-        self.assertTrue(len(j.pk) < 23)
-
-    def testUUIDFieldManyToManyCreate(self):
-        j = ShortUUIDTestManyToManyModel.objects.create(uuid_field=six.u('vytxeTZskVKR7C7WgdSP3e'))
-        self.assertEqual(j.uuid_field, six.u('vytxeTZskVKR7C7WgdSP3e'))
-        self.assertEqual(j.pk, six.u('vytxeTZskVKR7C7WgdSP3e'))
diff --git a/django_extensions/tests/utils.py b/django_extensions/tests/utils.py
deleted file mode 100644
--- a/django_extensions/tests/utils.py
+++ /dev/null
@@ -1,76 +0,0 @@
-# -*- coding: utf-8 -*-
-import sys
-import six
-
-from django.test import TestCase
-from django.utils.unittest import skipIf
-
-from django_extensions.utils.text import truncate_letters
-try:
-    import uuid
-    assert uuid
-except ImportError:
-    from django_extensions.utils import uuid
-
-
-class TruncateLetterTests(TestCase):
-    def test_truncate_more_than_text_length(self):
-        self.assertEqual(six.u("hello tests"), truncate_letters("hello tests", 100))
-
-    def test_truncate_text(self):
-        self.assertEqual(six.u("hello..."), truncate_letters("hello tests", 5))
-
-    def test_truncate_with_range(self):
-        for i in range(10, -1, -1):
-            self.assertEqual(
-                six.u('hello tests'[:i]) + '...',
-                truncate_letters("hello tests", i)
-            )
-
-    def test_with_non_ascii_characters(self):
-        self.assertEqual(
-            six.u('\u5ce0 (\u3068\u3046\u3052 t\u014dg...'),
-            truncate_letters("峠 (とうげ tōge - mountain pass)", 10)
-        )
-
-
-class UUIDTests(TestCase):
-    @skipIf(sys.version_info >= (2, 5, 0), 'uuid already in stdlib')
-    def test_uuid3(self):
-        # make a UUID using an MD5 hash of a namespace UUID and a name
-        self.assertEqual(
-            uuid.UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e'),
-            uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org')
-        )
-
-    @skipIf(sys.version_info >= (2, 5, 0), 'uuid already in stdlib')
-    def test_uuid5(self):
-        # make a UUID using a SHA-1 hash of a namespace UUID and a name
-        self.assertEqual(
-            uuid.UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d'),
-            uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org')
-        )
-
-    @skipIf(sys.version_info >= (2, 5, 0), 'uuid already in stdlib')
-    def test_uuid_str(self):
-        # make a UUID from a string of hex digits (braces and hyphens ignored)
-        x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}')
-        # convert a UUID to a string of hex digits in standard form
-        self.assertEqual('00010203-0405-0607-0809-0a0b0c0d0e0f', str(x))
-
-    @skipIf(sys.version_info >= (2, 5, 0), 'uuid already in stdlib')
-    def test_uuid_bytes(self):
-        # make a UUID from a string of hex digits (braces and hyphens ignored)
-        x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}')
-        # get the raw 16 bytes of the UUID
-        self.assertEqual(
-            '\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\t\\n\\x0b\\x0c\\r\\x0e\\x0f',
-            x.bytes
-        )
-
-    @skipIf(sys.version_info >= (2, 5, 0), 'uuid already in stdlib')
-    def test_make_uuid_from_byte_string(self):
-        self.assertEqual(
-            uuid.UUID(bytes='\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\t\\n\\x0b\\x0c\\r\\x0e\\x0f'),
-            uuid.UUID('00010203-0405-0607-0809-0a0b0c0d0e0f')
-        )
diff --git a/run_tests.py b/run_tests.py
deleted file mode 100755
--- a/run_tests.py
+++ /dev/null
@@ -1,146 +0,0 @@
-#!/usr/bin/env python
-
-import sys
-import shutil
-import tempfile
-
-try:
-    import django
-except ImportError:
-    print("Error: missing test dependency:")
-    print("  django library is needed to run test suite")
-    print("  you can install it with 'pip install django'")
-    print("  or use tox to automatically handle test dependencies")
-    sys.exit(1)
-
-try:
-    import shortuuid
-except ImportError:
-    print("Error: missing test dependency:")
-    print("  shortuuid library is needed to run test suite")
-    print("  you can install it with 'pip install shortuuid'")
-    print("  or use tox to automatically handle test dependencies")
-    sys.exit(1)
-
-try:
-    import dateutil
-except ImportError:
-    print("Error: missing test dependency:")
-    print("  dateutil library is needed to run test suite")
-    print("  you can install it with 'pip install python-dateutil'")
-    print("  or use tox to automatically handle test dependencies")
-    sys.exit(1)
-
-try:
-    import six
-except ImportError:
-    print("Error: missing test dependency:")
-    print("  six library is needed to run test suite")
-    print("  you can install it with 'pip install six'")
-    print("  or use tox to automatically handle test dependencies")
-    sys.exit(1)
-
-__test_libs__ = [
-    django,
-    shortuuid,
-    dateutil,
-    six
-]
-
-from django.conf import settings
-
-
-def main():
-    # Dynamically configure the Django settings with the minimum necessary to
-    # get Django running tests.
-    KEY_LOCS = {}
-    try:
-        try:
-            # If KeyCzar is available, set up the environment.
-            from keyczar import keyczart, keyinfo
-
-            # Create an RSA private key.
-            keys_dir = tempfile.mkdtemp("django_extensions_tests_keyzcar_rsa_dir")
-            keyczart.Create(keys_dir, "test", keyinfo.DECRYPT_AND_ENCRYPT, asymmetric=True)
-            keyczart.AddKey(keys_dir, "PRIMARY", size=4096)
-            KEY_LOCS['DECRYPT_AND_ENCRYPT'] = keys_dir
-
-            # Create an RSA public key.
-            pub_dir = tempfile.mkdtemp("django_extensions_tests_keyzcar_pub_dir")
-            keyczart.PubKey(keys_dir, pub_dir)
-            KEY_LOCS['ENCRYPT'] = pub_dir
-        except ImportError:
-            pass
-
-        settings.configure(
-            INSTALLED_APPS=[
-                'django.contrib.auth',
-                'django.contrib.contenttypes',
-                'django.contrib.admin',
-                'django.contrib.sessions',
-                'django_extensions.tests.testapp',
-                'django_extensions',
-            ],
-            MIDDLEWARE_CLASSES=(
-                'django.contrib.sessions.middleware.SessionMiddleware',
-                'django.contrib.auth.middleware.AuthenticationMiddleware',
-                'django.contrib.messages.middleware.MessageMiddleware',
-            ),
-            # Django replaces this, but it still wants it. *shrugs*
-            DATABASE_ENGINE='django.db.backends.sqlite3',
-            DATABASES={
-                'default': {
-                    'ENGINE': 'django.db.backends.sqlite3',
-                    'NAME': ':memory:',
-                }
-            },
-            MEDIA_ROOT='/tmp/django_extensions_test_media/',
-            MEDIA_PATH='/media/',
-            ROOT_URLCONF='django_extensions.tests.urls',
-            DEBUG=True,
-            TEMPLATE_DEBUG=True,
-            ENCRYPTED_FIELD_KEYS_DIR=KEY_LOCS,
-        )
-
-        if django.VERSION >= (1, 7):
-            django.setup()
-
-        apps = ['django_extensions']
-        if django.VERSION >= (1, 6):
-            apps.append('django_extensions.tests.testapp')
-            apps.append('django_extensions.tests')
-
-        from django.core.management import call_command
-        from django.test.utils import get_runner
-
-        try:
-            from django.contrib.auth import get_user_model
-        except ImportError:
-            USERNAME_FIELD = "username"
-        else:
-            USERNAME_FIELD = get_user_model().USERNAME_FIELD
-
-        DjangoTestRunner = get_runner(settings)
-
-        class TestRunner(DjangoTestRunner):
-            def setup_databases(self, *args, **kwargs):
-                result = super(TestRunner, self).setup_databases(*args, **kwargs)
-                kwargs = {
-                    "interactive": False,
-                    "email": "admin@doesnotexit.com",
-                    USERNAME_FIELD: "admin",
-                }
-                call_command("createsuperuser", **kwargs)
-                return result
-
-        failures = TestRunner(verbosity=2, interactive=True).run_tests(apps)
-        sys.exit(failures)
-
-    finally:
-        for name, path in KEY_LOCS.items():
-            # cleanup crypto key temp dirs
-            shutil.rmtree(path)
-
-
-if __name__ == '__main__':
-    main()
diff --git a/django_extensions/tests/management/__init__.py b/tests/__init__.py
similarity index 100%
rename from django_extensions/tests/management/__init__.py
rename to tests/__init__.py
diff --git a/django_extensions/tests/management/commands/__init__.py b/tests/management/__init__.py
similarity index 100%
rename from django_extensions/tests/management/commands/__init__.py
rename to tests/management/__init__.py
diff --git a/django_extensions/tests/testapp/__init__.py b/tests/management/commands/__init__.py
similarity index 100%
rename from django_extensions/tests/testapp/__init__.py
rename to tests/management/commands/__init__.py
diff --git a/django_extensions/tests/management/commands/error_raising_command.py b/tests/management/commands/error_raising_command.py
similarity index 100%
rename from django_extensions/tests/management/commands/error_raising_command.py
rename to tests/management/commands/error_raising_command.py
diff --git a/django_extensions/tests/fields.py b/tests/test_autoslug_fields.py
similarity index 74%
rename from django_extensions/tests/fields.py
rename to tests/test_autoslug_fields.py
--- a/django_extensions/tests/fields.py
+++ b/tests/test_autoslug_fields.py
@@ -1,58 +1,33 @@
+import pytest
+
 import django
-from django.conf import settings
-from django.core.management import call_command
-from django.db.models import loading
 from django.db import models
-from django.utils import unittest
-
+from django.test import TestCase
 from django_extensions.db.fields import AutoSlugField
-from django_extensions.tests.testapp.models import SluggedTestModel, ChildSluggedTestModel
 
-if django.VERSION[:2] >= (1, 7):
+from .testapp.models import ChildSluggedTestModel, SluggedTestModel
+
+
+if django.VERSION >= (1, 7):
     from django.db import migrations  # NOQA
     from django.db.migrations.writer import MigrationWriter  # NOQA
     from django.utils import six  # NOQA
     import django_extensions  # NOQA
 
 
-class FieldTestCase(unittest.TestCase):
-    def setUp(self):
-        self.old_installed_apps = settings.INSTALLED_APPS
-        #settings.INSTALLED_APPS = list(settings.INSTALLED_APPS)
-        #settings.INSTALLED_APPS.append('django_extensions.tests.testapp')
-        loading.cache.loaded = False
-
-        # Don't migrate if south is installed
-        migrate = 'south' not in settings.INSTALLED_APPS
-        call_command('syncdb', verbosity=0, migrate=migrate)
-
-    def tearDown(self):
-        settings.INSTALLED_APPS = self.old_installed_apps
-
-    def safe_exec(self, string, value=None):
-        l = {}
-        try:
-            exec(string, globals(), l)
-        except Exception as e:
-            if value:
-                self.fail("Could not exec %r (from value %r): %s" % (string.strip(), value, e))
-            else:
-                self.fail("Could not exec %r: %s" % (string.strip(), e))
-        return l
-
-
-class AutoSlugFieldTest(FieldTestCase):
+@pytest.mark.usefixtures("admin_user")
+class AutoSlugFieldTest(TestCase):
     def tearDown(self):
         super(AutoSlugFieldTest, self).tearDown()
 
         SluggedTestModel.objects.all().delete()
 
-    def testAutoCreateSlug(self):
+    def test_auto_create_slug(self):
         m = SluggedTestModel(title='foo')
         m.save()
         self.assertEqual(m.slug, 'foo')
 
-    def testAutoCreateNextSlug(self):
+    def test_auto_create_next_slug(self):
         m = SluggedTestModel(title='foo')
         m.save()
 
@@ -60,18 +35,18 @@ def testAutoCreateNextSlug(self):
         m.save()
         self.assertEqual(m.slug, 'foo-2')
 
-    def testAutoCreateSlugWithNumber(self):
+    def test_auto_create_slug_with_number(self):
         m = SluggedTestModel(title='foo 2012')
         m.save()
         self.assertEqual(m.slug, 'foo-2012')
 
-    def testAutoUpdateSlugWithNumber(self):
+    def test_auto_update_slug_with_number(self):
         m = SluggedTestModel(title='foo 2012')
         m.save()
         m.save()
         self.assertEqual(m.slug, 'foo-2012')
 
-    def testUpdateSlug(self):
+    def test_update_slug(self):
         m = SluggedTestModel(title='foo')
         m.save()
         self.assertEqual(m.slug, 'foo')
@@ -92,7 +67,7 @@ def testUpdateSlug(self):
         self.assertEqual(m.title, 'bar')
         self.assertEqual(m.slug, 'foo-2012')
 
-    def testSimpleSlugSource(self):
+    def test_simple_slug_source(self):
         m = SluggedTestModel(title='-foo')
         m.save()
         self.assertEqual(m.slug, 'foo')
@@ -104,7 +79,7 @@ def testSimpleSlugSource(self):
         n.save()
         self.assertEqual(n.slug, 'foo-2')
 
-    def testEmptySlugSource(self):
+    def test_empty_slug_source(self):
         # regression test
 
         m = SluggedTestModel(title='')
@@ -118,7 +93,7 @@ def testEmptySlugSource(self):
         n.save()
         self.assertEqual(n.slug, '-3')
 
-    def testInheritanceCreatesNextSlug(self):
+    def test_inheritance_creates_next_slug(self):
         m = SluggedTestModel(title='foo')
         m.save()
 
@@ -130,8 +105,21 @@ def testInheritanceCreatesNextSlug(self):
         o.save()
         self.assertEqual(o.slug, 'foo-3')
 
-    @unittest.skipIf(django.VERSION[0] <= 1 and django.VERSION[1] <= 6,
-                     "Migrations are handled by south in Django <1.7")
+
+@pytest.mark.skipif(django.VERSION < (1, 7),
+                    reason="Migrations are handled by south in Django <1.7")
+class MigrationTest(TestCase):
+    def safe_exec(self, string, value=None):
+        l = {}
+        try:
+            exec(string, globals(), l)
+        except Exception as e:
+            if value:
+                self.fail("Could not exec %r (from value %r): %s" % (string.strip(), value, e))
+            else:
+                self.fail("Could not exec %r: %s" % (string.strip(), e))
+        return l
+
     def test_17_migration(self):
         """
         Tests making migrations with Django 1.7+'s migration framework
diff --git a/django_extensions/tests/test_clean_pyc.py b/tests/test_clean_pyc.py
similarity index 93%
rename from django_extensions/tests/test_clean_pyc.py
rename to tests/test_clean_pyc.py
--- a/django_extensions/tests/test_clean_pyc.py
+++ b/tests/test_clean_pyc.py
@@ -1,9 +1,10 @@
+import fnmatch
 import os
-import six
 import shutil
-import fnmatch
-from django.test import TestCase
+import six
+
 from django.core.management import call_command
+from django.test import TestCase
 from django_extensions.management.utils import get_project_root
 
 
@@ -35,7 +36,7 @@ def test_removes_pyc_files(self):
 
     def test_takes_path(self):
         out = six.StringIO()
-        project_root = os.path.join(get_project_root(), 'tests', 'testapp')
+        project_root = os.path.join('tests', 'testapp')
         call_command('compile_pyc', path=project_root)
         pyc_glob = self._find_pyc(project_root)
         self.assertTrue(len(pyc_glob) > 0)
@@ -45,7 +46,7 @@ def test_takes_path(self):
 
     def test_removes_pyo_files(self):
         out = six.StringIO()
-        project_root = os.path.join(get_project_root(), 'tests', 'testapp')
+        project_root = os.path.join('tests', 'testapp')
         call_command('compile_pyc', path=project_root)
         pyc_glob = self._find_pyc(project_root)
         self.assertTrue(len(pyc_glob) > 0)
diff --git a/django_extensions/tests/test_compile_pyc.py b/tests/test_compile_pyc.py
similarity index 99%
rename from django_extensions/tests/test_compile_pyc.py
rename to tests/test_compile_pyc.py
--- a/django_extensions/tests/test_compile_pyc.py
+++ b/tests/test_compile_pyc.py
@@ -1,8 +1,9 @@
+import fnmatch
 import os
 import six
-import fnmatch
-from django.test import TestCase
+
 from django.core.management import call_command
+from django.test import TestCase
 from django_extensions.management.utils import get_project_root
 
 
diff --git a/django_extensions/tests/test_dumpscript.py b/tests/test_dumpscript.py
similarity index 72%
rename from django_extensions/tests/test_dumpscript.py
rename to tests/test_dumpscript.py
--- a/django_extensions/tests/test_dumpscript.py
+++ b/tests/test_dumpscript.py
@@ -1,32 +1,18 @@
-import sys
+import ast
 import six
+import sys
 
 from django.core.management import call_command
+from django.test import TestCase
 
-from django_extensions.tests.testapp.models import Name, Note, Person
-from django_extensions.tests.fields import FieldTestCase
-
-if sys.version_info[:2] >= (2, 6):
-    import ast as compiler  # NOQA
-else:
-    import compiler  # NOQA
+from .testapp.models import Name, Note, Person
 
 
-class DumpScriptTests(FieldTestCase):
+class DumpScriptTests(TestCase):
     def setUp(self):
-        super(DumpScriptTests, self).setUp()
-
-        self.real_stdout = sys.stdout
-        self.real_stderr = sys.stderr
         sys.stdout = six.StringIO()
         sys.stderr = six.StringIO()
 
-    def tearDown(self):
-        super(DumpScriptTests, self).tearDown()
-
-        sys.stdout = self.real_stdout
-        sys.stderr = self.real_stderr
-
     def test_runs(self):
         # lame test...does it run?
         n = Name(name='Gabriel')
@@ -34,7 +20,6 @@ def test_runs(self):
         call_command('dumpscript', 'django_extensions')
         self.assertTrue('Gabriel' in sys.stdout.getvalue())
 
-    #----------------------------------------------------------------------
     def test_replaced_stdout(self):
         # check if stdout can be replaced
         sys.stdout = six.StringIO()
@@ -46,7 +31,6 @@ def test_replaced_stdout(self):
         self.assertEqual(0, len(sys.stdout.getvalue()))  # there should not be any output to sys.stdout
         tmp_out.close()
 
-    #----------------------------------------------------------------------
     def test_replaced_stderr(self):
         # check if stderr can be replaced, without changing stdout
         n = Name(name='Fred')
@@ -59,7 +43,6 @@ def test_replaced_stderr(self):
         self.assertEqual(0, len(sys.stderr.getvalue()))  # there should not be any output to sys.stderr
         tmp_err.close()
 
-    #----------------------------------------------------------------------
     def test_valid_syntax(self):
         n1 = Name(name='John')
         n1.save()
@@ -77,7 +60,7 @@ def test_valid_syntax(self):
         p2.notes.add(note1, note2)
         tmp_out = six.StringIO()
         call_command('dumpscript', 'django_extensions', stdout=tmp_out)
-        ast_syntax_tree = compiler.parse(tmp_out.getvalue())
+        ast_syntax_tree = ast.parse(tmp_out.getvalue())
         if hasattr(ast_syntax_tree, 'body'):
             self.assertTrue(len(ast_syntax_tree.body) > 1)
         else:
diff --git a/django_extensions/tests/encrypted_fields.py b/tests/test_encrypted_fields.py
similarity index 84%
rename from django_extensions/tests/encrypted_fields.py
rename to tests/test_encrypted_fields.py
--- a/django_extensions/tests/encrypted_fields.py
+++ b/tests/test_encrypted_fields.py
@@ -1,12 +1,15 @@
+import tempfile
 from contextlib import contextmanager
-import functools
 
+import pytest
+
+import django
 from django.conf import settings
 from django.db import connection, models
-from django.db.models import loading
+from django.test import TestCase
+
+from .testapp.models import Secret
 
-from django_extensions.tests.models import Secret
-from django_extensions.tests.fields import FieldTestCase
 
 # Only perform encrypted fields tests if keyczar is present. Resolves
 # http://github.com/django-extensions/django-extensions/issues/#issue/17
@@ -17,20 +20,31 @@
 except ImportError:
     keyczar_active = False
 
+# Locations of both private and public keys.
+KEY_LOCS = {}
 
-def run_if_active(func):
-    "Method decorator that only runs a test if KeyCzar is available."
 
-    @functools.wraps(func)
-    def inner(self):
-        if not keyczar_active:
-            return
-        return func(self)
-    return inner
+@pytest.fixture(scope="class")
+def keyczar_keys(request):
+    # If KeyCzar is available, set up the environment.
+    if keyczar_active:
+        # Create an RSA private key.
+        keys_dir = tempfile.mkdtemp("django_extensions_tests_keyzcar_rsa_dir")
+        keyczart.Create(keys_dir, "test", keyinfo.DECRYPT_AND_ENCRYPT, asymmetric=True)
+        keyczart.AddKey(keys_dir, "PRIMARY", size=4096)
+        KEY_LOCS['DECRYPT_AND_ENCRYPT'] = keys_dir
 
+        # Create an RSA public key.
+        pub_dir = tempfile.mkdtemp("django_extensions_tests_keyzcar_pub_dir")
+        keyczart.PubKey(keys_dir, pub_dir)
+        KEY_LOCS['ENCRYPT'] = pub_dir
 
-# Locations of both private and public keys.
-KEY_LOCS = getattr(settings, 'ENCRYPTED_FIELD_KEYS_DIR', {})
+    # cleanup crypto key temp dirs
+    def cleanup():
+        import shutil
+        for name, path in KEY_LOCS.items():
+            shutil.rmtree(path)
+    request.addfinalizer(cleanup)
 
 
 @contextmanager
@@ -94,21 +108,12 @@ def secret_model():
         #differences-between-proxy-inheritance-and-unmanaged-models
     """
 
-    # Store Django's cached model, if present, so we can restore when the
-    # manager exits.
-    orig_model = None
-    try:
-        orig_model = loading.cache.app_models['tests']['secret']
-        del loading.cache.app_models['tests']['secret']
-    except KeyError:
-        pass
-
     try:
         # Create a new class that shadows tests.models.Secret.
         attrs = {
             'name': EncryptedCharField("Name", max_length=Secret._meta.get_field('name').max_length),
             'text': EncryptedTextField("Text"),
-            '__module__': 'django_extensions.tests.models',
+            '__module__': 'tests.testapp.models',
             'Meta': type('Meta', (object, ), {
                 'managed': False,
                 'db_table': Secret._meta.db_table
@@ -119,17 +124,12 @@ def secret_model():
     except:
         raise  # Reraise any exceptions.
 
-    finally:
-        # Restore Django's model cache.
-        try:
-            loading.cache.app_models['tests']['secret'] = orig_model
-        except KeyError:
-            pass
-
 
-class EncryptedFieldsTestCase(FieldTestCase):
-    @run_if_active
-    def testCharFieldCreate(self):
+@pytest.mark.skipif(keyczar_active is False or django.VERSION < (1, 7),
+                    reason="Encrypted fields needs that keyczar is installed")
+@pytest.mark.usefixtures("admin_user", "keyczar_keys")
+class EncryptedFieldsTestCase(TestCase):
+    def test_char_field_create(self):
         """
         Uses a private key to encrypt data on model creation.
         Verifies the data is encrypted in the database and can be decrypted.
@@ -146,8 +146,7 @@ def testCharFieldCreate(self):
                 decrypted_val = crypt.Decrypt(db_val[len(EncryptedCharField.prefix):])
                 self.assertEqual(test_val, decrypted_val)
 
-    @run_if_active
-    def testCharFieldRead(self):
+    def test_char_field_read(self):
         """
         Uses a private key to encrypt data on model creation.
         Verifies the data is decrypted when reading the value back from the
@@ -160,8 +159,7 @@ def testCharFieldRead(self):
                 retrieved_secret = model.objects.get(id=secret.id)
                 self.assertEqual(test_val, retrieved_secret.name)
 
-    @run_if_active
-    def testTextFieldCreate(self):
+    def test_text_field_create(self):
         """
         Uses a private key to encrypt data on model creation.
         Verifies the data is encrypted in the database and can be decrypted.
@@ -177,8 +175,7 @@ def testTextFieldCreate(self):
                 decrypted_val = crypt.Decrypt(db_val[len(EncryptedCharField.prefix):])
                 self.assertEqual(test_val, decrypted_val)
 
-    @run_if_active
-    def testTextFieldRead(self):
+    def test_text_field_read(self):
         """
         Uses a private key to encrypt data on model creation.
         Verifies the data is decrypted when reading the value back from the
@@ -191,8 +188,7 @@ def testTextFieldRead(self):
                 retrieved_secret = model.objects.get(id=secret.id)
                 self.assertEqual(test_val, retrieved_secret.text)
 
-    @run_if_active
-    def testCannotDecrypt(self):
+    def test_cannot_decrypt(self):
         """
         Uses a public key to encrypt data on model creation.
         Verifies that the data cannot be decrypted using the same key.
@@ -205,8 +201,7 @@ def testCannotDecrypt(self):
                 self.assertNotEqual(test_val, retrieved_secret.name)
                 self.assertTrue(retrieved_secret.name.startswith(EncryptedCharField.prefix))
 
-    @run_if_active
-    def testUnacceptablePurpose(self):
+    def test_unacceptable_purpose(self):
         """
         Tries to create an encrypted field with a mode mismatch.
         A purpose of "DECRYPT_AND_ENCRYPT" cannot be used with a public key,
@@ -220,8 +215,7 @@ def testUnacceptablePurpose(self):
                     # definition time, so any code in here would never get run.
                     pass
 
-    @run_if_active
-    def testDecryptionForbidden(self):
+    def test_decryption_forbidden(self):
         """
         Uses a private key to encrypt data, but decryption is not allowed.
         ENCRYPTED_FIELD_MODE is explicitly set to ENCRYPT, meaning data should
@@ -235,8 +229,7 @@ def testDecryptionForbidden(self):
                 self.assertNotEqual(test_val, retrieved_secret.name)
                 self.assertTrue(retrieved_secret.name.startswith(EncryptedCharField.prefix))
 
-    @run_if_active
-    def testEncryptPublicDecryptPrivate(self):
+    def test_encrypt_public_decrypt_private(self):
         """
         Uses a public key to encrypt, and a private key to decrypt data.
         """
diff --git a/django_extensions/tests/json_field.py b/tests/test_json_field.py
similarity index 58%
rename from django_extensions/tests/json_field.py
rename to tests/test_json_field.py
--- a/django_extensions/tests/json_field.py
+++ b/tests/test_json_field.py
@@ -1,17 +1,18 @@
-from django_extensions.tests.fields import FieldTestCase
-from django_extensions.tests.testapp.models import JSONFieldTestModel
+from django.test import TestCase
 
+from .testapp.models import JSONFieldTestModel
 
-class JsonFieldTest(FieldTestCase):
-    def testCharFieldCreate(self):
+
+class JsonFieldTest(TestCase):
+    def test_char_field_create(self):
         j = JSONFieldTestModel.objects.create(a=6, j_field=dict(foo='bar'))
         self.assertEqual(j.a, 6)
 
-    def testDefault(self):
+    def test_default(self):
         j = JSONFieldTestModel.objects.create(a=1)
         self.assertEqual(j.j_field, {})
 
-    def testEmptyList(self):
+    def test_empty_list(self):
         j = JSONFieldTestModel.objects.create(a=6, j_field=[])
         self.assertTrue(isinstance(j.j_field, list))
         self.assertEqual(j.j_field, [])
diff --git a/django_extensions/tests/management_command.py b/tests/test_management_command.py
similarity index 97%
rename from django_extensions/tests/management_command.py
rename to tests/test_management_command.py
--- a/django_extensions/tests/management_command.py
+++ b/tests/test_management_command.py
@@ -1,6 +1,10 @@
 # -*- coding: utf-8 -*-
-import sys
 import logging
+import sys
+
+from django.core.management import call_command
+from django.test import TestCase
+
 
 try:
     from cStringIO import StringIO  # NOQA
@@ -12,8 +16,6 @@
 except ImportError:
     from django.utils import importlib  # NOQA
 
-from django.core.management import call_command
-from django.test import TestCase
 
 
 class MockLoggingHandler(logging.Handler):
@@ -41,7 +43,7 @@ def test_error_logging(self):
         # Ensure command errors are properly logged and reraised
         from django_extensions.management.base import logger
         logger.addHandler(MockLoggingHandler())
-        module_path = "django_extensions.tests.management.commands.error_raising_command"
+        module_path = "tests.management.commands.error_raising_command"
         module = importlib.import_module(module_path)
         error_raising_command = module.Command()
         self.assertRaises(Exception, error_raising_command.execute)
diff --git a/django_extensions/tests/test_models.py b/tests/test_models.py
similarity index 96%
rename from django_extensions/tests/test_models.py
rename to tests/test_models.py
--- a/django_extensions/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,7 +1,7 @@
 from django.test import TestCase
-
 from django_extensions.db.models import ActivatorModel
-from django_extensions.tests.testapp.models import Post
+
+from .testapp.models import Post
 
 
 class ActivatorModelTestCase(TestCase):
diff --git a/tests/test_shortuuid_field.py b/tests/test_shortuuid_field.py
new file mode 100644
--- /dev/null
+++ b/tests/test_shortuuid_field.py
@@ -0,0 +1,32 @@
+import six
+
+from django.test import TestCase
+
+from .testapp.models import (
+    ShortUUIDTestAgregateModel,
+    ShortUUIDTestManyToManyModel,
+    ShortUUIDTestModel_field,
+    ShortUUIDTestModel_pk,
+)
+
+
+class ShortUUIDFieldTest(TestCase):
+    def test_UUID_field_create(self):
+        j = ShortUUIDTestModel_field.objects.create(a=6, uuid_field=six.u('vytxeTZskVKR7C7WgdSP3d'))
+        self.assertEqual(j.uuid_field, six.u('vytxeTZskVKR7C7WgdSP3d'))
+
+    def test_UUID_field_pk_create(self):
+        j = ShortUUIDTestModel_pk.objects.create(uuid_field=six.u('vytxeTZskVKR7C7WgdSP3d'))
+        self.assertEqual(j.uuid_field, six.u('vytxeTZskVKR7C7WgdSP3d'))
+        self.assertEqual(j.pk, six.u('vytxeTZskVKR7C7WgdSP3d'))
+
+    def test_UUID_field_pk_agregate_create(self):
+        j = ShortUUIDTestAgregateModel.objects.create(a=6)
+        self.assertEqual(j.a, 6)
+        self.assertIsInstance(j.pk, six.string_types)
+        self.assertTrue(len(j.pk) < 23)
+
+    def test_UUID_field_manytomany_create(self):
+        j = ShortUUIDTestManyToManyModel.objects.create(uuid_field=six.u('vytxeTZskVKR7C7WgdSP3e'))
+        self.assertEqual(j.uuid_field, six.u('vytxeTZskVKR7C7WgdSP3e'))
+        self.assertEqual(j.pk, six.u('vytxeTZskVKR7C7WgdSP3e'))
diff --git a/django_extensions/tests/test_templatetags.py b/tests/test_templatetags.py
similarity index 99%
rename from django_extensions/tests/test_templatetags.py
rename to tests/test_templatetags.py
--- a/django_extensions/tests/test_templatetags.py
+++ b/tests/test_templatetags.py
@@ -1,7 +1,6 @@
 import six
 
 from django.test import TestCase
-
 from django_extensions.templatetags.widont import widont, widont_html
 
 
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+import six
+
+from django.test import TestCase
+from django_extensions.utils.text import truncate_letters
+
+
+class TruncateLetterTests(TestCase):
+    def test_truncate_more_than_text_length(self):
+        self.assertEqual(six.u("hello tests"), truncate_letters("hello tests", 100))
+
+    def test_truncate_text(self):
+        self.assertEqual(six.u("hello..."), truncate_letters("hello tests", 5))
+
+    def test_truncate_with_range(self):
+        for i in range(10, -1, -1):
+            self.assertEqual(
+                six.u('hello tests'[:i]) + '...',
+                truncate_letters("hello tests", i)
+            )
+
+    def test_with_non_ascii_characters(self):
+        self.assertEqual(
+            six.u('\u5ce0 (\u3068\u3046\u3052 t\u014dg...'),
+            truncate_letters("峠 (とうげ tōge - mountain pass)", 10)
+        )
diff --git a/django_extensions/tests/uuid_field.py b/tests/test_uuid_field.py
similarity index 81%
rename from django_extensions/tests/uuid_field.py
rename to tests/test_uuid_field.py
--- a/django_extensions/tests/uuid_field.py
+++ b/tests/test_uuid_field.py
@@ -1,36 +1,41 @@
 import re
-import uuid
-
 import six
+import uuid
 
+from django.test import TestCase
 from django_extensions.db.fields import PostgreSQLUUIDField
-from django_extensions.tests.fields import FieldTestCase
-from django_extensions.tests.testapp.models import UUIDTestModel_field, UUIDTestModel_pk, UUIDTestAgregateModel, UUIDTestManyToManyModel
+
+from .testapp.models import (
+    UUIDTestAgregateModel,
+    UUIDTestManyToManyModel,
+    UUIDTestModel_field,
+    UUIDTestModel_pk,
+)
 
 
-class UUIDFieldTest(FieldTestCase):
-    def testUUIDFieldCreate(self):
+class UUIDFieldTest(TestCase):
+    def test_UUID_field_create(self):
         j = UUIDTestModel_field.objects.create(a=6, uuid_field=six.u('550e8400-e29b-41d4-a716-446655440000'))
         self.assertEqual(j.uuid_field, six.u('550e8400-e29b-41d4-a716-446655440000'))
 
-    def testUUIDField_pkCreate(self):
+    def test_UUID_field_pk_create(self):
         j = UUIDTestModel_pk.objects.create(uuid_field=six.u('550e8400-e29b-41d4-a716-446655440000'))
         self.assertEqual(j.uuid_field, six.u('550e8400-e29b-41d4-a716-446655440000'))
         self.assertEqual(j.pk, six.u('550e8400-e29b-41d4-a716-446655440000'))
 
-    def testUUIDField_pkAgregateCreate(self):
+    def test_UUID_field_pk_agregate_create(self):
         j = UUIDTestAgregateModel.objects.create(a=6, uuid_field=six.u('550e8400-e29b-41d4-a716-446655440001'))
         self.assertEqual(j.a, 6)
         self.assertIsInstance(j.pk, six.string_types)
         self.assertEqual(len(j.pk), 36)
 
-    def testUUIDFieldManyToManyCreate(self):
+    def test_UUID_field_manytomany_create(self):
         j = UUIDTestManyToManyModel.objects.create(uuid_field=six.u('550e8400-e29b-41d4-a716-446655440010'))
         self.assertEqual(j.uuid_field, six.u('550e8400-e29b-41d4-a716-446655440010'))
         self.assertEqual(j.pk, six.u('550e8400-e29b-41d4-a716-446655440010'))
 
 
-class PostgreSQLUUIDFieldTest(FieldTestCase):
+class PostgreSQLUUIDFieldTest(TestCase):
     def test_uuid_casting(self):
         # As explain by postgres documentation
         # http://www.postgresql.org/docs/9.1/static/datatype-uuid.html
diff --git a/django_extensions/tests/testapp/urls.py b/tests/testapp/__init__.py
similarity index 100%
rename from django_extensions/tests/testapp/urls.py
rename to tests/testapp/__init__.py
diff --git a/django_extensions/tests/testapp/models.py b/tests/testapp/models.py
similarity index 100%
rename from django_extensions/tests/testapp/models.py
rename to tests/testapp/models.py
diff --git a/tests/testapp/urls.py b/tests/testapp/urls.py
new file mode 100644