Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #2879 -- Added support for the integration with Selenium and ot…

…her in-browser testing frameworks. Also added the first Selenium tests for `contrib.admin`. Many thanks to everyone for their contributions and feedback: Mikeal Rogers, Dirk Datzert, mir, Simon G., Almad, Russell Keith-Magee, Denis Golomazov, devin, robertrv, andrewbadr, Idan Gazit, voidspace, Tom Christie, hjwp2, Adam Nelson, Jannis Leidel, Anssi Kääriäinen, Preston Holmes, Bruno Renié and Jacob Kaplan-Moss.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@17241 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 2f02a05ffb45be68b4164b4785ff1826833150a3 1 parent 45e3dff
Julien Phalip authored December 22, 2011
52  django/contrib/admin/tests.py
... ...
@@ -0,0 +1,52 @@
  1
+import sys
  2
+
  3
+from django.test import LiveServerTestCase
  4
+from django.utils.importlib import import_module
  5
+from django.utils.unittest import SkipTest
  6
+from django.utils.translation import ugettext as _
  7
+
  8
+class AdminSeleniumWebDriverTestCase(LiveServerTestCase):
  9
+    webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
  10
+
  11
+    @classmethod
  12
+    def setUpClass(cls):
  13
+        if sys.version_info < (2, 6):
  14
+            raise SkipTest('Selenium Webdriver does not support Python < 2.6.')
  15
+        try:
  16
+            # Import and start the WebDriver class.
  17
+            module, attr = cls.webdriver_class.rsplit('.', 1)
  18
+            mod = import_module(module)
  19
+            WebDriver = getattr(mod, attr)
  20
+            cls.selenium = WebDriver()
  21
+        except Exception:
  22
+            raise SkipTest('Selenium webdriver "%s" not installed or not '
  23
+                           'operational.' % cls.webdriver_class)
  24
+        super(AdminSeleniumWebDriverTestCase, cls).setUpClass()
  25
+
  26
+    @classmethod
  27
+    def tearDownClass(cls):
  28
+        super(AdminSeleniumWebDriverTestCase, cls).tearDownClass()
  29
+        if hasattr(cls, 'selenium'):
  30
+            cls.selenium.quit()
  31
+
  32
+    def admin_login(self, username, password, login_url='/admin/'):
  33
+        """
  34
+        Helper function to log into the admin.
  35
+        """
  36
+        self.selenium.get('%s%s' % (self.live_server_url, login_url))
  37
+        username_input = self.selenium.find_element_by_name('username')
  38
+        username_input.send_keys(username)
  39
+        password_input = self.selenium.find_element_by_name('password')
  40
+        password_input.send_keys(password)
  41
+        login_text = _('Log in')
  42
+        self.selenium.find_element_by_xpath(
  43
+            '//input[@value="%s"]' % login_text).click()
  44
+
  45
+    def get_css_value(self, selector, attribute):
  46
+        """
  47
+        Helper function that returns the value for the CSS attribute of an
  48
+        DOM element specified by the given selector. Uses the jQuery that ships
  49
+        with Django.
  50
+        """
  51
+        return self.selenium.execute_script(
  52
+            'return django.jQuery("%s").css("%s")' % (selector, attribute))
37  django/core/management/commands/test.py
... ...
@@ -1,20 +1,32 @@
  1
+import sys
  2
+import os
  3
+from optparse import make_option, OptionParser
  4
+
1 5
 from django.conf import settings
2 6
 from django.core.management.base import BaseCommand
3  
-from optparse import make_option, OptionParser
4  
-import sys
5 7
 from django.test.utils import get_runner
6 8
 
7 9
 class Command(BaseCommand):
8 10
     option_list = BaseCommand.option_list + (
9  
-        make_option('--noinput', action='store_false', dest='interactive', default=True,
  11
+        make_option('--noinput',
  12
+            action='store_false', dest='interactive', default=True,
10 13
             help='Tells Django to NOT prompt the user for input of any kind.'),
11  
-        make_option('--failfast', action='store_true', dest='failfast', default=False,
12  
-            help='Tells Django to stop running the test suite after first failed test.'),
13  
-        make_option('--testrunner', action='store', dest='testrunner',
14  
-            help='Tells Django to use specified test runner class instead of the one '+
15  
-                 'specified by the TEST_RUNNER setting.')
  14
+        make_option('--failfast',
  15
+            action='store_true', dest='failfast', default=False,
  16
+            help='Tells Django to stop running the test suite after first '
  17
+                 'failed test.'),
  18
+        make_option('--testrunner',
  19
+            action='store', dest='testrunner',
  20
+            help='Tells Django to use specified test runner class instead of '
  21
+                 'the one specified by the TEST_RUNNER setting.'),
  22
+        make_option('--liveserver',
  23
+            action='store', dest='liveserver', default=None,
  24
+            help='Overrides the default address where the live server (used '
  25
+                 'with LiveServerTestCase) is expected to run from. The '
  26
+                 'default value is localhost:8081.'),
16 27
     )
17  
-    help = 'Runs the test suite for the specified applications, or the entire site if no apps are specified.'
  28
+    help = ('Runs the test suite for the specified applications, or the '
  29
+            'entire site if no apps are specified.')
18 30
     args = '[appname ...]'
19 31
 
20 32
     requires_model_validation = False
@@ -35,7 +47,8 @@ def run_from_argv(self, argv):
35 47
 
36 48
     def create_parser(self, prog_name, subcommand):
37 49
         test_runner_class = get_runner(settings, self.test_runner)
38  
-        options = self.option_list + getattr(test_runner_class, 'option_list', ())
  50
+        options = self.option_list + getattr(
  51
+            test_runner_class, 'option_list', ())
39 52
         return OptionParser(prog=prog_name,
40 53
                             usage=self.usage(subcommand),
41 54
                             version=self.get_version(),
@@ -48,6 +61,10 @@ def handle(self, *test_labels, **options):
48 61
         TestRunner = get_runner(settings, options.get('testrunner'))
49 62
         options['verbosity'] = int(options.get('verbosity'))
50 63
 
  64
+        if options.get('liveserver') is not None:
  65
+            os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = options['liveserver']
  66
+            del options['liveserver']
  67
+
51 68
         test_runner = TestRunner(**options)
52 69
         failures = test_runner.run_tests(test_labels)
53 70
 
3  django/test/__init__.py
@@ -4,5 +4,6 @@
4 4
 
5 5
 from django.test.client import Client, RequestFactory
6 6
 from django.test.testcases import (TestCase, TransactionTestCase,
7  
-        SimpleTestCase, skipIfDBFeature, skipUnlessDBFeature)
  7
+    SimpleTestCase, LiveServerTestCase, skipIfDBFeature,
  8
+    skipUnlessDBFeature)
8 9
 from django.test.utils import Approximate
245  django/test/testcases.py
... ...
@@ -1,16 +1,23 @@
1 1
 from __future__ import with_statement
2 2
 
  3
+import os
3 4
 import re
4 5
 import sys
5 6
 from functools import wraps
6 7
 from urlparse import urlsplit, urlunsplit
7 8
 from xml.dom.minidom import parseString, Node
  9
+import select
  10
+import socket
  11
+import threading
8 12
 
9 13
 from django.conf import settings
  14
+from django.contrib.staticfiles.handlers import StaticFilesHandler
10 15
 from django.core import mail
11  
-from django.core.exceptions import ValidationError
  16
+from django.core.exceptions import ValidationError, ImproperlyConfigured
  17
+from django.core.handlers.wsgi import WSGIHandler
12 18
 from django.core.management import call_command
13 19
 from django.core.signals import request_started
  20
+from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer)
14 21
 from django.core.urlresolvers import clear_url_caches
15 22
 from django.core.validators import EMPTY_VALUES
16 23
 from django.db import (transaction, connection, connections, DEFAULT_DB_ALIAS,
@@ -23,6 +30,7 @@
23 30
     override_settings)
24 31
 from django.utils import simplejson, unittest as ut2
25 32
 from django.utils.encoding import smart_str
  33
+from django.views.static import serve
26 34
 
27 35
 __all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase',
28 36
            'SimpleTestCase', 'skipIfDBFeature', 'skipUnlessDBFeature')
@@ -68,7 +76,8 @@ def restore_transaction_methods():
68 76
 class OutputChecker(doctest.OutputChecker):
69 77
     def check_output(self, want, got, optionflags):
70 78
         """
71  
-        The entry method for doctest output checking. Defers to a sequence of child checkers
  79
+        The entry method for doctest output checking. Defers to a sequence of
  80
+        child checkers
72 81
         """
73 82
         checks = (self.check_output_default,
74 83
                   self.check_output_numeric,
@@ -219,6 +228,7 @@ def report_unexpected_exception(self, out, test, example, exc_info):
219 228
         for conn in connections:
220 229
             transaction.rollback_unless_managed(using=conn)
221 230
 
  231
+
222 232
 class _AssertNumQueriesContext(object):
223 233
     def __init__(self, test_case, num, connection):
224 234
         self.test_case = test_case
@@ -247,6 +257,7 @@ def __exit__(self, exc_type, exc_value, traceback):
247 257
             )
248 258
         )
249 259
 
  260
+
250 261
 class SimpleTestCase(ut2.TestCase):
251 262
 
252 263
     def save_warnings_state(self):
@@ -335,6 +346,7 @@ def assertFieldOutput(self, fieldclass, valid, invalid, field_args=None,
335 346
             self.assertTrue(isinstance(fieldclass(*field_args, **field_kwargs),
336 347
                                        fieldclass))
337 348
 
  349
+
338 350
 class TransactionTestCase(SimpleTestCase):
339 351
     # The class we'll use for the test client self.client.
340 352
     # Can be overridden in derived classes.
@@ -643,6 +655,7 @@ def assertNumQueries(self, num, func=None, *args, **kwargs):
643 655
         with context:
644 656
             func(*args, **kwargs)
645 657
 
  658
+
646 659
 def connections_support_transactions():
647 660
     """
648 661
     Returns True if all connections support transactions.
@@ -650,6 +663,7 @@ def connections_support_transactions():
650 663
     return all(conn.features.supports_transactions
651 664
                for conn in connections.all())
652 665
 
  666
+
653 667
 class TestCase(TransactionTestCase):
654 668
     """
655 669
     Does basically the same as TransactionTestCase, but surrounds every test
@@ -703,6 +717,7 @@ def _fixture_teardown(self):
703 717
             transaction.rollback(using=db)
704 718
             transaction.leave_transaction_management(using=db)
705 719
 
  720
+
706 721
 def _deferredSkip(condition, reason):
707 722
     def decorator(test_func):
708 723
         if not (isinstance(test_func, type) and
@@ -719,6 +734,7 @@ def skip_wrapper(*args, **kwargs):
719 734
         return test_item
720 735
     return decorator
721 736
 
  737
+
722 738
 def skipIfDBFeature(feature):
723 739
     """
724 740
     Skip a test if a database has the named feature
@@ -726,9 +742,234 @@ def skipIfDBFeature(feature):
726 742
     return _deferredSkip(lambda: getattr(connection.features, feature),
727 743
                          "Database has feature %s" % feature)
728 744
 
  745
+
729 746
 def skipUnlessDBFeature(feature):
730 747
     """
731 748
     Skip a test unless a database has the named feature
732 749
     """
733 750
     return _deferredSkip(lambda: not getattr(connection.features, feature),
734 751
                          "Database doesn't support feature %s" % feature)
  752
+
  753
+
  754
+class QuietWSGIRequestHandler(WSGIRequestHandler):
  755
+    """
  756
+    Just a regular WSGIRequestHandler except it doesn't log to the standard
  757
+    output any of the requests received, so as to not clutter the output for
  758
+    the tests' results.
  759
+    """
  760
+
  761
+    def log_message(*args):
  762
+        pass
  763
+
  764
+
  765
+class _ImprovedEvent(threading._Event):
  766
+    """
  767
+    Does the same as `threading.Event` except it overrides the wait() method
  768
+    with some code borrowed from Python 2.7 to return the set state of the
  769
+    event (see: http://hg.python.org/cpython/rev/b5aa8aa78c0f/). This allows
  770
+    to know whether the wait() method exited normally or because of the
  771
+    timeout. This class can be removed when Django supports only Python >= 2.7.
  772
+    """
  773
+
  774
+    def wait(self, timeout=None):
  775
+        self._Event__cond.acquire()
  776
+        try:
  777
+            if not self._Event__flag:
  778
+                self._Event__cond.wait(timeout)
  779
+            return self._Event__flag
  780
+        finally:
  781
+            self._Event__cond.release()
  782
+
  783
+
  784
+class StoppableWSGIServer(WSGIServer):
  785
+    """
  786
+    The code in this class is borrowed from the `SocketServer.BaseServer` class
  787
+    in Python 2.6. The important functionality here is that the server is non-
  788
+    blocking and that it can be shut down at any moment. This is made possible
  789
+    by the server regularly polling the socket and checking if it has been
  790
+    asked to stop.
  791
+    Note for the future: Once Django stops supporting Python 2.6, this class
  792
+    can be removed as `WSGIServer` will have this ability to shutdown on
  793
+    demand and will not require the use of the _ImprovedEvent class whose code
  794
+    is borrowed from Python 2.7.
  795
+    """
  796
+
  797
+    def __init__(self, *args, **kwargs):
  798
+        super(StoppableWSGIServer, self).__init__(*args, **kwargs)
  799
+        self.__is_shut_down = _ImprovedEvent()
  800
+        self.__serving = False
  801
+
  802
+    def serve_forever(self, poll_interval=0.5):
  803
+        """
  804
+        Handle one request at a time until shutdown.
  805
+
  806
+        Polls for shutdown every poll_interval seconds.
  807
+        """
  808
+        self.__serving = True
  809
+        self.__is_shut_down.clear()
  810
+        while self.__serving:
  811
+            r, w, e = select.select([self], [], [], poll_interval)
  812
+            if r:
  813
+                self._handle_request_noblock()
  814
+        self.__is_shut_down.set()
  815
+
  816
+    def shutdown(self):
  817
+        """
  818
+        Stops the serve_forever loop.
  819
+
  820
+        Blocks until the loop has finished. This must be called while
  821
+        serve_forever() is running in another thread, or it will
  822
+        deadlock.
  823
+        """
  824
+        self.__serving = False
  825
+        if not self.__is_shut_down.wait(2):
  826
+            raise RuntimeError(
  827
+                "Failed to shutdown the live test server in 2 seconds. The "
  828
+                "server might be stuck or generating a slow response.")
  829
+
  830
+    def handle_request(self):
  831
+        """Handle one request, possibly blocking.
  832
+        """
  833
+        fd_sets = select.select([self], [], [], None)
  834
+        if not fd_sets[0]:
  835
+            return
  836
+        self._handle_request_noblock()
  837
+
  838
+    def _handle_request_noblock(self):
  839
+        """
  840
+        Handle one request, without blocking.
  841
+
  842
+        I assume that select.select has returned that the socket is
  843
+        readable before this function was called, so there should be
  844
+        no risk of blocking in get_request().
  845
+        """
  846
+        try:
  847
+            request, client_address = self.get_request()
  848
+        except socket.error:
  849
+            return
  850
+        if self.verify_request(request, client_address):
  851
+            try:
  852
+                self.process_request(request, client_address)
  853
+            except Exception:
  854
+                self.handle_error(request, client_address)
  855
+                self.close_request(request)
  856
+
  857
+
  858
+class _MediaFilesHandler(StaticFilesHandler):
  859
+    """
  860
+    Handler for serving the media files. This is a private class that is
  861
+    meant to be used solely as a convenience by LiveServerThread.
  862
+    """
  863
+
  864
+    def get_base_dir(self):
  865
+        return settings.MEDIA_ROOT
  866
+
  867
+    def get_base_url(self):
  868
+        return settings.MEDIA_URL
  869
+
  870
+    def serve(self, request):
  871
+        return serve(request, self.file_path(request.path),
  872
+            document_root=self.get_base_dir())
  873
+
  874
+
  875
+class LiveServerThread(threading.Thread):
  876
+    """
  877
+    Thread for running a live http server while the tests are running.
  878
+    """
  879
+
  880
+    def __init__(self, address, port, connections_override=None):
  881
+        self.address = address
  882
+        self.port = port
  883
+        self.is_ready = threading.Event()
  884
+        self.error = None
  885
+        self.connections_override = connections_override
  886
+        super(LiveServerThread, self).__init__()
  887
+
  888
+    def run(self):
  889
+        """
  890
+        Sets up the live server and databases, and then loops over handling
  891
+        http requests.
  892
+        """
  893
+        if self.connections_override:
  894
+            from django.db import connections
  895
+            # Override this thread's database connections with the ones
  896
+            # provided by the main thread.
  897
+            for alias, conn in self.connections_override.items():
  898
+                connections[alias] = conn
  899
+        try:
  900
+            # Create the handler for serving static and media files
  901
+            handler = StaticFilesHandler(_MediaFilesHandler(WSGIHandler()))
  902
+            # Instantiate and start the WSGI server
  903
+            self.httpd = StoppableWSGIServer(
  904
+                (self.address, self.port), QuietWSGIRequestHandler)
  905
+            self.httpd.set_app(handler)
  906
+            self.is_ready.set()
  907
+            self.httpd.serve_forever()
  908
+        except Exception, e:
  909
+            self.error = e
  910
+            self.is_ready.set()
  911
+
  912
+    def join(self, timeout=None):
  913
+        if hasattr(self, 'httpd'):
  914
+            # Stop the WSGI server
  915
+            self.httpd.shutdown()
  916
+            self.httpd.server_close()
  917
+        super(LiveServerThread, self).join(timeout)
  918
+
  919
+
  920
+class LiveServerTestCase(TransactionTestCase):
  921
+    """
  922
+    Does basically the same as TransactionTestCase but also launches a live
  923
+    http server in a separate thread so that the tests may use another testing
  924
+    framework, such as Selenium for example, instead of the built-in dummy
  925
+    client.
  926
+    Note that it inherits from TransactionTestCase instead of TestCase because
  927
+    the threads do not share the same transactions (unless if using in-memory
  928
+    sqlite) and each thread needs to commit all their transactions so that the
  929
+    other thread can see the changes.
  930
+    """
  931
+
  932
+    @property
  933
+    def live_server_url(self):
  934
+        return 'http://%s' % self.__test_server_address
  935
+
  936
+    @classmethod
  937
+    def setUpClass(cls):
  938
+        connections_override = {}
  939
+        for conn in connections.all():
  940
+            # If using in-memory sqlite databases, pass the connections to
  941
+            # the server thread.
  942
+            if (conn.settings_dict['ENGINE'] == 'django.db.backends.sqlite3'
  943
+                and conn.settings_dict['NAME'] == ':memory:'):
  944
+                # Explicitly enable thread-shareability for this connection
  945
+                conn.allow_thread_sharing = True
  946
+                connections_override[conn.alias] = conn
  947
+
  948
+        # Launch the live server's thread
  949
+        cls.__test_server_address = os.environ.get(
  950
+            'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081')
  951
+        try:
  952
+            host, port = cls.__test_server_address.split(':')
  953
+        except Exception:
  954
+            raise ImproperlyConfigured('Invalid address ("%s") for live '
  955
+                'server.' % cls.__test_server_address)
  956
+        cls.server_thread = LiveServerThread(
  957
+            host, int(port), connections_override)
  958
+        cls.server_thread.daemon = True
  959
+        cls.server_thread.start()
  960
+
  961
+        # Wait for the live server to be ready
  962
+        cls.server_thread.is_ready.wait()
  963
+        if cls.server_thread.error:
  964
+            raise cls.server_thread.error
  965
+
  966
+        super(LiveServerTestCase, cls).setUpClass()
  967
+
  968
+    @classmethod
  969
+    def tearDownClass(cls):
  970
+        # There may not be a 'server_thread' attribute if setUpClass() for some
  971
+        # reasons has raised an exception.
  972
+        if hasattr(cls, 'server_thread'):
  973
+            # Terminate the live server's thread
  974
+            cls.server_thread.join()
  975
+        super(LiveServerTestCase, cls).tearDownClass()
15  docs/internals/contributing/writing-code/unit-tests.txt
@@ -122,6 +122,19 @@ Going beyond that, you can specify an individual test method like this:
122 122
 
123 123
     ./runtests.py --settings=path.to.settings i18n.TranslationTests.test_lazy_objects
124 124
 
  125
+Running the Selenium tests
  126
+~~~~~~~~~~~~~~~~~~~~~~~~~~
  127
+
  128
+Some admin tests require Selenium 2, Firefox and Python >= 2.6 to work via a
  129
+real Web browser. To allow those tests to run and not be skipped, you must
  130
+install the selenium_ package (version > 2.13) into your Python path.
  131
+
  132
+Then, run the tests normally, for example:
  133
+
  134
+.. code-block:: bash
  135
+
  136
+    ./runtests.py --settings=test_sqlite admin_inlines
  137
+
125 138
 Running all the tests
126 139
 ~~~~~~~~~~~~~~~~~~~~~
127 140
 
@@ -135,6 +148,7 @@ dependencies:
135 148
 *  setuptools_
136 149
 *  memcached_, plus a :ref:`supported Python binding <memcached>`
137 150
 *  gettext_ (:ref:`gettext_on_windows`)
  151
+*  selenium_ (if also using Python >= 2.6)
138 152
 
139 153
 If you want to test the memcached cache backend, you'll also need to define
140 154
 a :setting:`CACHES` setting that points at your memcached instance.
@@ -149,6 +163,7 @@ associated tests will be skipped.
149 163
 .. _setuptools: http://pypi.python.org/pypi/setuptools/
150 164
 .. _memcached: http://www.danga.com/memcached/
151 165
 .. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html
  166
+.. _selenium: http://pypi.python.org/pypi/selenium
152 167
 
153 168
 .. _contrib-apps:
154 169
 
17  docs/ref/django-admin.txt
@@ -976,15 +976,22 @@ information.
976 976
 .. versionadded:: 1.2
977 977
 .. django-admin-option:: --failfast
978 978
 
979  
-Use the :djadminopt:`--failfast` option to stop running tests and report the failure
980  
-immediately after a test fails.
  979
+The ``--failfast`` option can be used to stop running tests and report the
  980
+failure immediately after a test fails.
981 981
 
982 982
 .. versionadded:: 1.4
983 983
 .. django-admin-option:: --testrunner
984 984
 
985  
-The :djadminopt:`--testrunner` option can be used to control the test runner
986  
-class that is used to execute tests. If this value is provided, it overrides
987  
-the value provided by the :setting:`TEST_RUNNER` setting.
  985
+The ``--testrunner`` option can be used to control the test runner class that
  986
+is used to execute tests. If this value is provided, it overrides the value
  987
+provided by the :setting:`TEST_RUNNER` setting.
  988
+
  989
+.. versionadded:: 1.4
  990
+.. django-admin-option:: --liveserver
  991
+
  992
+The ``--liveserver`` option can be used to override the default address where
  993
+the live server (used with :class:`~django.test.LiveServerTestCase`) is
  994
+expected to run from. The default value is ``localhost:8081``.
988 995
 
989 996
 testserver <fixture fixture ...>
990 997
 --------------------------------
13  docs/releases/1.4.txt
@@ -40,6 +40,19 @@ before the release of Django 1.4.
40 40
 What's new in Django 1.4
41 41
 ========================
42 42
 
  43
+Support for in-browser testing frameworks
  44
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  45
+
  46
+Django 1.4 now supports the integration with in-browser testing frameworks such
  47
+as Selenium_ or Windmill_ thanks to the :class:`django.test.LiveServerTestCase`
  48
+base class, allowing you to test the interactions between your site's front and
  49
+back ends more comprehensively. See the
  50
+:class:`documentation<django.test.LiveServerTestCase>` for more details and
  51
+concrete examples.
  52
+
  53
+.. _Windmill: http://www.getwindmill.com/
  54
+.. _Selenium: http://seleniumhq.org/
  55
+
43 56
 ``SELECT FOR UPDATE`` support
44 57
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
45 58
 
104  docs/topics/testing.txt
@@ -581,21 +581,20 @@ Some of the things you can do with the test client are:
581 581
 * Test that a given request is rendered by a given Django template, with
582 582
   a template context that contains certain values.
583 583
 
584  
-Note that the test client is not intended to be a replacement for Twill_,
  584
+Note that the test client is not intended to be a replacement for Windmill_,
585 585
 Selenium_, or other "in-browser" frameworks. Django's test client has
586 586
 a different focus. In short:
587 587
 
588 588
 * Use Django's test client to establish that the correct view is being
589 589
   called and that the view is collecting the correct context data.
590 590
 
591  
-* Use in-browser frameworks such as Twill and Selenium to test *rendered*
592  
-  HTML and the *behavior* of Web pages, namely JavaScript functionality.
  591
+* Use in-browser frameworks such as Windmill_ and Selenium_ to test *rendered*
  592
+  HTML and the *behavior* of Web pages, namely JavaScript functionality. Django
  593
+  also provides special support for those frameworks; see the section on
  594
+  :class:`~django.test.LiveServerTestCase` for more details.
593 595
 
594 596
 A comprehensive test suite should use a combination of both test types.
595 597
 
596  
-.. _Twill: http://twill.idyll.org/
597  
-.. _Selenium: http://seleniumhq.org/
598  
-
599 598
 Overview and a quick example
600 599
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
601 600
 
@@ -1753,6 +1752,97 @@ under MySQL with MyISAM tables)::
1753 1752
         def test_transaction_behavior(self):
1754 1753
             # ... conditional test code
1755 1754
 
  1755
+Live test server
  1756
+----------------
  1757
+
  1758
+.. versionadded:: 1.4
  1759
+
  1760
+.. currentmodule:: django.test
  1761
+
  1762
+.. class:: LiveServerTestCase()
  1763
+
  1764
+``LiveServerTestCase`` does basically the same as
  1765
+:class:`~django.test.TransactionTestCase` with one extra feature: it launches a
  1766
+live Django server in the background on setup, and shuts it down on teardown.
  1767
+This allows the use of automated test clients other than the
  1768
+:ref:`Django dummy client <test-client>` such as, for example, the Selenium_ or
  1769
+Windmill_ clients, to execute a series of functional tests inside a browser and
  1770
+simulate a real user's actions.
  1771
+
  1772
+By default the live server's address is `'localhost:8081'` and the full URL
  1773
+can be accessed during the tests with ``self.live_server_url``. If you'd like
  1774
+to change the default address (in the case, for example, where the 8081 port is
  1775
+already taken) you may pass a different one to the :djadmin:`test` command via
  1776
+the :djadminopt:`--liveserver` option, for example:
  1777
+
  1778
+.. code-block:: bash
  1779
+
  1780
+    ./manage.py test --liveserver=localhost:8082
  1781
+
  1782
+Another way of changing the default server address is by setting the
  1783
+`DJANGO_LIVE_TEST_SERVER_ADDRESS` environment variable.
  1784
+
  1785
+To demonstrate how to use ``LiveServerTestCase``, let's write a simple Selenium
  1786
+test. First of all, you need to install the `selenium package`_ into your
  1787
+Python path:
  1788
+
  1789
+.. code-block:: bash
  1790
+
  1791
+   pip install selenium
  1792
+
  1793
+Then, add a ``LiveServerTestCase``-based test to your app's tests module
  1794
+(for example: ``myapp/tests.py``). The code for this test may look as follows:
  1795
+
  1796
+.. code-block:: python
  1797
+
  1798
+    from django.test import LiveServerTestCase
  1799
+    from selenium.webdriver.firefox.webdriver import WebDriver
  1800
+
  1801
+    class MySeleniumTests(LiveServerTestCase):
  1802
+        fixtures = ['user-data.json']
  1803
+
  1804
+        @classmethod
  1805
+        def setUpClass(cls):
  1806
+            cls.selenium = WebDriver()
  1807
+            super(MySeleniumTests, cls).setUpClass()
  1808
+
  1809
+        @classmethod
  1810
+        def tearDownClass(cls):
  1811
+            super(MySeleniumTests, cls).tearDownClass()
  1812
+            cls.selenium.quit()
  1813
+
  1814
+        def test_login(self):
  1815
+            self.selenium.get('%s%s' % (self.live_server_url, '/login/'))
  1816
+            username_input = self.selenium.find_element_by_name("username")
  1817
+            username_input.send_keys('myuser')
  1818
+            password_input = self.selenium.find_element_by_name("password")
  1819
+            password_input.send_keys('secret')
  1820
+            self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()
  1821
+
  1822
+Finally, you may run the test as follows:
  1823
+
  1824
+.. code-block:: bash
  1825
+
  1826
+    ./manage.py test myapp.MySeleniumTests.test_login
  1827
+
  1828
+This example will automatically open Firefox then go to the login page, enter
  1829
+the credentials and press the "Log in" button. Selenium offers other drivers in
  1830
+case you do not have Firefox installed or wish to use another browser. The
  1831
+example above is just a tiny fraction of what the Selenium client can do; check
  1832
+out the `full reference`_ for more details.
  1833
+
  1834
+.. _Windmill: http://www.getwindmill.com/
  1835
+.. _Selenium: http://seleniumhq.org/
  1836
+.. _selenium package: http://pypi.python.org/pypi/selenium
  1837
+.. _full reference: http://readthedocs.org/docs/selenium-python/en/latest/api.html
  1838
+.. _Firefox: http://www.mozilla.com/firefox/
  1839
+
  1840
+.. note::
  1841
+
  1842
+    ``LiveServerTestCase`` makes use of the :doc:`staticfiles contrib app
  1843
+    </howto/static-files>` so you'll need to have your project configured
  1844
+    accordingly (in particular by setting :setting:`STATIC_URL`).
  1845
+
1756 1846
 
1757 1847
 Using different testing frameworks
1758 1848
 ==================================
@@ -1833,11 +1923,9 @@ set up, execute and tear down the test suite.
1833 1923
     those options will be added to the list of command-line options that
1834 1924
     the :djadmin:`test` command can use.
1835 1925
 
1836  
-
1837 1926
 Attributes
1838 1927
 ~~~~~~~~~~
1839 1928
 
1840  
-
1841 1929
 .. attribute:: DjangoTestSuiteRunner.option_list
1842 1930
 
1843 1931
     .. versionadded:: 1.4
5  tests/regressiontests/admin_inlines/admin.py
@@ -109,6 +109,10 @@ class SottoCapoInline(admin.TabularInline):
109 109
     model = SottoCapo
110 110
 
111 111
 
  112
+class ProfileInline(admin.TabularInline):
  113
+    model = Profile
  114
+    extra = 1
  115
+
112 116
 site.register(TitleCollection, inlines=[TitleInline])
113 117
 # Test bug #12561 and #12778
114 118
 # only ModelAdmin media
@@ -124,3 +128,4 @@ class SottoCapoInline(admin.TabularInline):
124 128
 site.register(Holder4, Holder4Admin)
125 129
 site.register(Author, AuthorAdmin)
126 130
 site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline])
  131
+site.register(ProfileCollection, inlines=[ProfileInline])
10  tests/regressiontests/admin_inlines/models.py
@@ -136,3 +136,13 @@ class Consigliere(models.Model):
136 136
 class SottoCapo(models.Model):
137 137
     name = models.CharField(max_length=100)
138 138
     capo_famiglia = models.ForeignKey(CapoFamiglia, related_name='+')
  139
+
  140
+# Other models
  141
+
  142
+class ProfileCollection(models.Model):
  143
+    pass
  144
+
  145
+class Profile(models.Model):
  146
+    collection = models.ForeignKey(ProfileCollection, blank=True, null=True)
  147
+    first_name = models.CharField(max_length=100)
  148
+    last_name = models.CharField(max_length=100)
106  tests/regressiontests/admin_inlines/tests.py
... ...
@@ -1,5 +1,6 @@
1 1
 from __future__ import absolute_import
2 2
 
  3
+from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
3 4
 from django.contrib.admin.helpers import InlineAdminForm
4 5
 from django.contrib.auth.models import User, Permission
5 6
 from django.contrib.contenttypes.models import ContentType
@@ -8,7 +9,8 @@
8 9
 # local test models
9 10
 from .admin import InnerInline
10 11
 from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person,
11  
-    OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book)
  12
+    OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile,
  13
+    ProfileCollection)
12 14
 
13 15
 
14 16
 class TestInline(TestCase):
@@ -380,3 +382,105 @@ def test_inline_change_fk_all_perms(self):
380 382
         self.assertContains(response, 'value="4" id="id_inner2_set-TOTAL_FORMS"')
381 383
         self.assertContains(response, '<input type="hidden" name="inner2_set-0-id" value="%i"' % self.inner2_id)
382 384
         self.assertContains(response, 'id="id_inner2_set-0-DELETE"')
  385
+
  386
+class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
  387
+    webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
  388
+    fixtures = ['admin-views-users.xml']
  389
+    urls = "regressiontests.admin_inlines.urls"
  390
+
  391
+    def test_add_inlines(self):
  392
+        """
  393
+        Ensure that the "Add another XXX" link correctly adds items to the
  394
+        inline form.
  395
+        """
  396
+        self.admin_login(username='super', password='secret')
  397
+        self.selenium.get('%s%s' % (self.live_server_url,
  398
+            '/admin/admin_inlines/profilecollection/add/'))
  399
+
  400
+        # Check that there's only one inline to start with and that it has the
  401
+        # correct ID.
  402
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  403
+            '#profile_set-group table tr.dynamic-profile_set')), 1)
  404
+        self.failUnlessEqual(self.selenium.find_element_by_css_selector(
  405
+            '.dynamic-profile_set:nth-of-type(1)').get_attribute('id'),
  406
+            'profile_set-0')
  407
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  408
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-0 input[name=profile_set-0-first_name]')), 1)
  409
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  410
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-0 input[name=profile_set-0-last_name]')), 1)
  411
+
  412
+        # Add an inline
  413
+        self.selenium.find_element_by_link_text('Add another Profile').click()
  414
+
  415
+        # Check that the inline has been added, that it has the right id, and
  416
+        # that it contains the right fields.
  417
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  418
+            '#profile_set-group table tr.dynamic-profile_set')), 2)
  419
+        self.failUnlessEqual(self.selenium.find_element_by_css_selector(
  420
+            '.dynamic-profile_set:nth-of-type(2)').get_attribute('id'), 'profile_set-1')
  421
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  422
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 input[name=profile_set-1-first_name]')), 1)
  423
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  424
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 input[name=profile_set-1-last_name]')), 1)
  425
+
  426
+        # Let's add another one to be sure
  427
+        self.selenium.find_element_by_link_text('Add another Profile').click()
  428
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  429
+            '#profile_set-group table tr.dynamic-profile_set')), 3)
  430
+        self.failUnlessEqual(self.selenium.find_element_by_css_selector(
  431
+            '.dynamic-profile_set:nth-of-type(3)').get_attribute('id'), 'profile_set-2')
  432
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  433
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 input[name=profile_set-2-first_name]')), 1)
  434
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  435
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 input[name=profile_set-2-last_name]')), 1)
  436
+
  437
+        # Enter some data and click 'Save'
  438
+        self.selenium.find_element_by_name('profile_set-0-first_name').send_keys('0 first name 1')
  439
+        self.selenium.find_element_by_name('profile_set-0-last_name').send_keys('0 last name 2')
  440
+        self.selenium.find_element_by_name('profile_set-1-first_name').send_keys('1 first name 1')
  441
+        self.selenium.find_element_by_name('profile_set-1-last_name').send_keys('1 last name 2')
  442
+        self.selenium.find_element_by_name('profile_set-2-first_name').send_keys('2 first name 1')
  443
+        self.selenium.find_element_by_name('profile_set-2-last_name').send_keys('2 last name 2')
  444
+        self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
  445
+
  446
+        # Check that the objects have been created in the database
  447
+        self.assertEqual(ProfileCollection.objects.all().count(), 1)
  448
+        self.assertEqual(Profile.objects.all().count(), 3)
  449
+
  450
+    def test_delete_inlines(self):
  451
+        self.admin_login(username='super', password='secret')
  452
+        self.selenium.get('%s%s' % (self.live_server_url,
  453
+            '/admin/admin_inlines/profilecollection/add/'))
  454
+
  455
+        # Add a few inlines
  456
+        self.selenium.find_element_by_link_text('Add another Profile').click()
  457
+        self.selenium.find_element_by_link_text('Add another Profile').click()
  458
+        self.selenium.find_element_by_link_text('Add another Profile').click()
  459
+        self.selenium.find_element_by_link_text('Add another Profile').click()
  460
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  461
+            '#profile_set-group table tr.dynamic-profile_set')), 5)
  462
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  463
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-0')), 1)
  464
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  465
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-1')), 1)
  466
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  467
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 1)
  468
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  469
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-3')), 1)
  470
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  471
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-4')), 1)
  472
+
  473
+        # Click on a few delete buttons
  474
+        self.selenium.find_element_by_css_selector(
  475
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 td.delete a').click()
  476
+        self.selenium.find_element_by_css_selector(
  477
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 td.delete a').click()
  478
+        # Verify that they're gone and that the IDs have been re-sequenced
  479
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  480
+            '#profile_set-group table tr.dynamic-profile_set')), 3)
  481
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  482
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-0')), 1)
  483
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  484
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-1')), 1)
  485
+        self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
  486
+            'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 1)
45  tests/regressiontests/admin_scripts/tests.py
@@ -13,6 +13,7 @@
13 13
 
14 14
 from django import conf, bin, get_version
15 15
 from django.conf import settings
  16
+from django.test.simple import DjangoTestSuiteRunner
16 17
 from django.utils import unittest
17 18
 
18 19
 
@@ -1058,6 +1059,50 @@ def test_app_with_import(self):
1058 1059
         self.assertOutput(out, '0 errors found')
1059 1060
 
1060 1061
 
  1062
+class CustomTestRunner(DjangoTestSuiteRunner):
  1063
+
  1064
+    def __init__(self, *args, **kwargs):
  1065
+        assert 'liveserver' not in kwargs
  1066
+        super(CustomTestRunner, self).__init__(*args, **kwargs)
  1067
+
  1068
+    def run_tests(self, test_labels, extra_tests=None, **kwargs):
  1069
+        pass
  1070
+
  1071
+class ManageTestCommand(AdminScriptTestCase):
  1072
+    def setUp(self):
  1073
+        from django.core.management.commands.test import Command as TestCommand
  1074
+        self.cmd = TestCommand()
  1075
+
  1076
+    def test_liveserver(self):
  1077
+        """
  1078
+        Ensure that the --liveserver option sets the environment variable
  1079
+        correctly.
  1080
+        Refs #2879.
  1081
+        """
  1082
+
  1083
+        # Backup original state
  1084
+        address_predefined = 'DJANGO_LIVE_TEST_SERVER_ADDRESS' in os.environ
  1085
+        old_address = os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS')
  1086
+
  1087
+        self.cmd.handle(verbosity=0, testrunner='regressiontests.admin_scripts.tests.CustomTestRunner')
  1088
+
  1089
+        # Original state hasn't changed
  1090
+        self.assertEqual('DJANGO_LIVE_TEST_SERVER_ADDRESS' in os.environ, address_predefined)
  1091
+        self.assertEqual(os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS'), old_address)
  1092
+
  1093
+        self.cmd.handle(verbosity=0, testrunner='regressiontests.admin_scripts.tests.CustomTestRunner',
  1094
+                        liveserver='blah')
  1095
+
  1096
+        # Variable was correctly set
  1097
+        self.assertEqual(os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'], 'blah')
  1098
+
  1099
+        # Restore original state
  1100
+        if address_predefined:
  1101
+            os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = old_address
  1102
+        else:
  1103
+            del os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS']
  1104
+
  1105
+
1061 1106
 class ManageRunserver(AdminScriptTestCase):
1062 1107
     def setUp(self):
1063 1108
         from django.core.management.commands.runserver import BaseRunserverCommand
50  tests/regressiontests/admin_widgets/tests.py
@@ -7,6 +7,7 @@
7 7
 from django.conf import settings
8 8
 from django.contrib import admin
9 9
 from django.contrib.admin import widgets
  10
+from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
10 11
 from django.core.files.storage import default_storage
11 12
 from django.core.files.uploadedfile import SimpleUploadedFile
12 13
 from django.db.models import DateField
@@ -407,3 +408,52 @@ def test_no_can_add_related(self):
407 408
         # Used to fail with a name error.
408 409
         w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site)
409 410
         self.assertFalse(w.can_add_related)
  411
+
  412
+
  413
+class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
  414
+    webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
  415
+    fixtures = ['admin-widgets-users.xml']
  416
+    urls = "regressiontests.admin_widgets.urls"
  417
+
  418
+    def test_show_hide_date_time_picker_widgets(self):
  419
+        """
  420
+        Ensure that pressing the ESC key closes the date and time picker
  421
+        widgets.
  422
+        Refs #17064.
  423
+        """
  424
+        from selenium.webdriver.common.keys import Keys
  425
+
  426
+        self.admin_login(username='super', password='secret', login_url='/')
  427
+        # Open a page that has a date and time picker widgets
  428
+        self.selenium.get('%s%s' % (self.live_server_url,
  429
+            '/admin_widgets/member/add/'))
  430
+
  431
+        # First, with the date picker widget ---------------------------------
  432
+        # Check that the date picker is hidden
  433
+        self.assertEqual(
  434
+            self.get_css_value('#calendarbox0', 'display'), 'none')
  435
+        # Click the calendar icon
  436
+        self.selenium.find_element_by_id('calendarlink0').click()
  437
+        # Check that the date picker is visible
  438
+        self.assertEqual(
  439
+            self.get_css_value('#calendarbox0', 'display'), 'block')
  440
+        # Press the ESC key
  441
+        self.selenium.find_element_by_tag_name('html').send_keys([Keys.ESCAPE])
  442
+        # Check that the date picker is hidden again
  443
+        self.assertEqual(
  444
+            self.get_css_value('#calendarbox0', 'display'), 'none')
  445
+
  446
+        # Then, with the time picker widget ----------------------------------
  447
+        # Check that the time picker is hidden
  448
+        self.assertEqual(
  449
+            self.get_css_value('#clockbox0', 'display'), 'none')
  450
+        # Click the time icon
  451
+        self.selenium.find_element_by_id('clocklink0').click()
  452
+        # Check that the time picker is visible
  453
+        self.assertEqual(
  454
+            self.get_css_value('#clockbox0', 'display'), 'block')
  455
+        # Press the ESC key
  456
+        self.selenium.find_element_by_tag_name('html').send_keys([Keys.ESCAPE])
  457
+        # Check that the time picker is hidden again
  458
+        self.assertEqual(
  459
+            self.get_css_value('#clockbox0', 'display'), 'none')
16  tests/regressiontests/servers/fixtures/testdata.json
... ...
@@ -0,0 +1,16 @@
  1
+[
  2
+  {
  3
+    "pk": 1,
  4
+    "model": "servers.person",
  5
+    "fields": {
  6
+      "name": "jane"
  7
+    }
  8
+  },
  9
+  {
  10
+    "pk": 2,
  11
+    "model": "servers.person",
  12
+    "fields": {
  13
+      "name": "robert"
  14
+    }
  15
+  }
  16
+]
1  tests/regressiontests/servers/media/example_media_file.txt
... ...
@@ -0,0 +1 @@
  1
+example media file
5  tests/regressiontests/servers/models.py
... ...
@@ -0,0 +1,5 @@
  1
+from django.db import models
  2
+
  3
+
  4
+class Person(models.Model):
  5
+    name = models.CharField(max_length=256)
1  tests/regressiontests/servers/static/example_static_file.txt
... ...
@@ -0,0 +1 @@
  1
+example static file
151  tests/regressiontests/servers/tests.py
@@ -3,13 +3,17 @@
3 3
 """
4 4
 import os
5 5
 from urlparse import urljoin
  6
+import urllib2
6 7
 
7 8
 import django
8 9
 from django.conf import settings
9  
-from django.test import TestCase
  10
+from django.core.exceptions import ImproperlyConfigured
  11
+from django.test import TestCase, LiveServerTestCase
10 12
 from django.core.handlers.wsgi import WSGIHandler
11  
-from django.core.servers.basehttp import AdminMediaHandler
  13
+from django.core.servers.basehttp import AdminMediaHandler, WSGIServerException
  14
+from django.test.utils import override_settings
12 15
 
  16
+from .models import Person
13 17
 
14 18
 class AdminMediaHandlerTests(TestCase):
15 19
 
@@ -68,3 +72,146 @@ def test_media_urls(self):
68 72
                 continue
69 73
             self.fail('URL: %s should have caused a ValueError exception.'
70 74
                       % url)
  75
+
  76
+
  77
+TEST_ROOT = os.path.dirname(__file__)
  78
+TEST_SETTINGS = {
  79
+    'MEDIA_URL': '/media/',
  80
+    'MEDIA_ROOT': os.path.join(TEST_ROOT, 'media'),
  81
+    'STATIC_URL': '/static/',
  82
+    'STATIC_ROOT': os.path.join(TEST_ROOT, 'static'),
  83
+}
  84
+
  85
+
  86
+class LiveServerBase(LiveServerTestCase):
  87
+    urls = 'regressiontests.servers.urls'
  88
+    fixtures = ['testdata.json']
  89
+
  90
+    @classmethod
  91
+    def setUpClass(cls):
  92
+        # Override settings
  93
+        cls.settings_override = override_settings(**TEST_SETTINGS)
  94
+        cls.settings_override.enable()
  95
+        super(LiveServerBase, cls).setUpClass()
  96
+
  97
+    @classmethod
  98
+    def tearDownClass(cls):
  99
+        # Restore original settings
  100
+        cls.settings_override.disable()
  101
+        super(LiveServerBase, cls).tearDownClass()
  102
+
  103
+    def urlopen(self, url):
  104
+        server_address = os.environ.get(
  105
+            'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081')
  106
+        base = 'http://%s' % server_address
  107
+        return urllib2.urlopen(base + url)
  108
+
  109
+
  110
+class LiveServerAddress(LiveServerBase):
  111
+    """
  112
+    Ensure that the address set in the environment variable is valid.
  113
+    Refs #2879.
  114
+    """
  115
+
  116
+    @classmethod
  117
+    def setUpClass(cls):