Fixed #2879 -- Added support for the integration with Selenium and other 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
This commit is contained in:
parent
45e3dff5ac
commit
2f02a05ffb
|
@ -0,0 +1,52 @@
|
|||
import sys
|
||||
|
||||
from django.test import LiveServerTestCase
|
||||
from django.utils.importlib import import_module
|
||||
from django.utils.unittest import SkipTest
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
class AdminSeleniumWebDriverTestCase(LiveServerTestCase):
|
||||
webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if sys.version_info < (2, 6):
|
||||
raise SkipTest('Selenium Webdriver does not support Python < 2.6.')
|
||||
try:
|
||||
# Import and start the WebDriver class.
|
||||
module, attr = cls.webdriver_class.rsplit('.', 1)
|
||||
mod = import_module(module)
|
||||
WebDriver = getattr(mod, attr)
|
||||
cls.selenium = WebDriver()
|
||||
except Exception:
|
||||
raise SkipTest('Selenium webdriver "%s" not installed or not '
|
||||
'operational.' % cls.webdriver_class)
|
||||
super(AdminSeleniumWebDriverTestCase, cls).setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(AdminSeleniumWebDriverTestCase, cls).tearDownClass()
|
||||
if hasattr(cls, 'selenium'):
|
||||
cls.selenium.quit()
|
||||
|
||||
def admin_login(self, username, password, login_url='/admin/'):
|
||||
"""
|
||||
Helper function to log into the admin.
|
||||
"""
|
||||
self.selenium.get('%s%s' % (self.live_server_url, login_url))
|
||||
username_input = self.selenium.find_element_by_name('username')
|
||||
username_input.send_keys(username)
|
||||
password_input = self.selenium.find_element_by_name('password')
|
||||
password_input.send_keys(password)
|
||||
login_text = _('Log in')
|
||||
self.selenium.find_element_by_xpath(
|
||||
'//input[@value="%s"]' % login_text).click()
|
||||
|
||||
def get_css_value(self, selector, attribute):
|
||||
"""
|
||||
Helper function that returns the value for the CSS attribute of an
|
||||
DOM element specified by the given selector. Uses the jQuery that ships
|
||||
with Django.
|
||||
"""
|
||||
return self.selenium.execute_script(
|
||||
'return django.jQuery("%s").css("%s")' % (selector, attribute))
|
|
@ -1,20 +1,32 @@
|
|||
import sys
|
||||
import os
|
||||
from optparse import make_option, OptionParser
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from optparse import make_option, OptionParser
|
||||
import sys
|
||||
from django.test.utils import get_runner
|
||||
|
||||
class Command(BaseCommand):
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('--noinput', action='store_false', dest='interactive', default=True,
|
||||
make_option('--noinput',
|
||||
action='store_false', dest='interactive', default=True,
|
||||
help='Tells Django to NOT prompt the user for input of any kind.'),
|
||||
make_option('--failfast', action='store_true', dest='failfast', default=False,
|
||||
help='Tells Django to stop running the test suite after first failed test.'),
|
||||
make_option('--testrunner', action='store', dest='testrunner',
|
||||
help='Tells Django to use specified test runner class instead of the one '+
|
||||
'specified by the TEST_RUNNER setting.')
|
||||
make_option('--failfast',
|
||||
action='store_true', dest='failfast', default=False,
|
||||
help='Tells Django to stop running the test suite after first '
|
||||
'failed test.'),
|
||||
make_option('--testrunner',
|
||||
action='store', dest='testrunner',
|
||||
help='Tells Django to use specified test runner class instead of '
|
||||
'the one specified by the TEST_RUNNER setting.'),
|
||||
make_option('--liveserver',
|
||||
action='store', dest='liveserver', default=None,
|
||||
help='Overrides the default address where the live server (used '
|
||||
'with LiveServerTestCase) is expected to run from. The '
|
||||
'default value is localhost:8081.'),
|
||||
)
|
||||
help = 'Runs the test suite for the specified applications, or the entire site if no apps are specified.'
|
||||
help = ('Runs the test suite for the specified applications, or the '
|
||||
'entire site if no apps are specified.')
|
||||
args = '[appname ...]'
|
||||
|
||||
requires_model_validation = False
|
||||
|
@ -35,7 +47,8 @@ class Command(BaseCommand):
|
|||
|
||||
def create_parser(self, prog_name, subcommand):
|
||||
test_runner_class = get_runner(settings, self.test_runner)
|
||||
options = self.option_list + getattr(test_runner_class, 'option_list', ())
|
||||
options = self.option_list + getattr(
|
||||
test_runner_class, 'option_list', ())
|
||||
return OptionParser(prog=prog_name,
|
||||
usage=self.usage(subcommand),
|
||||
version=self.get_version(),
|
||||
|
@ -48,6 +61,10 @@ class Command(BaseCommand):
|
|||
TestRunner = get_runner(settings, options.get('testrunner'))
|
||||
options['verbosity'] = int(options.get('verbosity'))
|
||||
|
||||
if options.get('liveserver') is not None:
|
||||
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = options['liveserver']
|
||||
del options['liveserver']
|
||||
|
||||
test_runner = TestRunner(**options)
|
||||
failures = test_runner.run_tests(test_labels)
|
||||
|
||||
|
|
|
@ -4,5 +4,6 @@ Django Unit Test and Doctest framework.
|
|||
|
||||
from django.test.client import Client, RequestFactory
|
||||
from django.test.testcases import (TestCase, TransactionTestCase,
|
||||
SimpleTestCase, skipIfDBFeature, skipUnlessDBFeature)
|
||||
SimpleTestCase, LiveServerTestCase, skipIfDBFeature,
|
||||
skipUnlessDBFeature)
|
||||
from django.test.utils import Approximate
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from functools import wraps
|
||||
from urlparse import urlsplit, urlunsplit
|
||||
from xml.dom.minidom import parseString, Node
|
||||
import select
|
||||
import socket
|
||||
import threading
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles.handlers import StaticFilesHandler
|
||||
from django.core import mail
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
||||
from django.core.handlers.wsgi import WSGIHandler
|
||||
from django.core.management import call_command
|
||||
from django.core.signals import request_started
|
||||
from django.core.servers.basehttp import (WSGIRequestHandler, WSGIServer)
|
||||
from django.core.urlresolvers import clear_url_caches
|
||||
from django.core.validators import EMPTY_VALUES
|
||||
from django.db import (transaction, connection, connections, DEFAULT_DB_ALIAS,
|
||||
|
@ -23,6 +30,7 @@ from django.test.utils import (get_warnings_state, restore_warnings_state,
|
|||
override_settings)
|
||||
from django.utils import simplejson, unittest as ut2
|
||||
from django.utils.encoding import smart_str
|
||||
from django.views.static import serve
|
||||
|
||||
__all__ = ('DocTestRunner', 'OutputChecker', 'TestCase', 'TransactionTestCase',
|
||||
'SimpleTestCase', 'skipIfDBFeature', 'skipUnlessDBFeature')
|
||||
|
@ -68,7 +76,8 @@ def restore_transaction_methods():
|
|||
class OutputChecker(doctest.OutputChecker):
|
||||
def check_output(self, want, got, optionflags):
|
||||
"""
|
||||
The entry method for doctest output checking. Defers to a sequence of child checkers
|
||||
The entry method for doctest output checking. Defers to a sequence of
|
||||
child checkers
|
||||
"""
|
||||
checks = (self.check_output_default,
|
||||
self.check_output_numeric,
|
||||
|
@ -219,6 +228,7 @@ class DocTestRunner(doctest.DocTestRunner):
|
|||
for conn in connections:
|
||||
transaction.rollback_unless_managed(using=conn)
|
||||
|
||||
|
||||
class _AssertNumQueriesContext(object):
|
||||
def __init__(self, test_case, num, connection):
|
||||
self.test_case = test_case
|
||||
|
@ -247,6 +257,7 @@ class _AssertNumQueriesContext(object):
|
|||
)
|
||||
)
|
||||
|
||||
|
||||
class SimpleTestCase(ut2.TestCase):
|
||||
|
||||
def save_warnings_state(self):
|
||||
|
@ -335,6 +346,7 @@ class SimpleTestCase(ut2.TestCase):
|
|||
self.assertTrue(isinstance(fieldclass(*field_args, **field_kwargs),
|
||||
fieldclass))
|
||||
|
||||
|
||||
class TransactionTestCase(SimpleTestCase):
|
||||
# The class we'll use for the test client self.client.
|
||||
# Can be overridden in derived classes.
|
||||
|
@ -643,6 +655,7 @@ class TransactionTestCase(SimpleTestCase):
|
|||
with context:
|
||||
func(*args, **kwargs)
|
||||
|
||||
|
||||
def connections_support_transactions():
|
||||
"""
|
||||
Returns True if all connections support transactions.
|
||||
|
@ -650,6 +663,7 @@ def connections_support_transactions():
|
|||
return all(conn.features.supports_transactions
|
||||
for conn in connections.all())
|
||||
|
||||
|
||||
class TestCase(TransactionTestCase):
|
||||
"""
|
||||
Does basically the same as TransactionTestCase, but surrounds every test
|
||||
|
@ -703,6 +717,7 @@ class TestCase(TransactionTestCase):
|
|||
transaction.rollback(using=db)
|
||||
transaction.leave_transaction_management(using=db)
|
||||
|
||||
|
||||
def _deferredSkip(condition, reason):
|
||||
def decorator(test_func):
|
||||
if not (isinstance(test_func, type) and
|
||||
|
@ -719,6 +734,7 @@ def _deferredSkip(condition, reason):
|
|||
return test_item
|
||||
return decorator
|
||||
|
||||
|
||||
def skipIfDBFeature(feature):
|
||||
"""
|
||||
Skip a test if a database has the named feature
|
||||
|
@ -726,9 +742,234 @@ def skipIfDBFeature(feature):
|
|||
return _deferredSkip(lambda: getattr(connection.features, feature),
|
||||
"Database has feature %s" % feature)
|
||||
|
||||
|
||||
def skipUnlessDBFeature(feature):
|
||||
"""
|
||||
Skip a test unless a database has the named feature
|
||||
"""
|
||||
return _deferredSkip(lambda: not getattr(connection.features, feature),
|
||||
"Database doesn't support feature %s" % feature)
|
||||
|
||||
|
||||
class QuietWSGIRequestHandler(WSGIRequestHandler):
|
||||
"""
|
||||
Just a regular WSGIRequestHandler except it doesn't log to the standard
|
||||
output any of the requests received, so as to not clutter the output for
|
||||
the tests' results.
|
||||
"""
|
||||
|
||||
def log_message(*args):
|
||||
pass
|
||||
|
||||
|
||||
class _ImprovedEvent(threading._Event):
|
||||
"""
|
||||
Does the same as `threading.Event` except it overrides the wait() method
|
||||
with some code borrowed from Python 2.7 to return the set state of the
|
||||
event (see: http://hg.python.org/cpython/rev/b5aa8aa78c0f/). This allows
|
||||
to know whether the wait() method exited normally or because of the
|
||||
timeout. This class can be removed when Django supports only Python >= 2.7.
|
||||
"""
|
||||
|
||||
def wait(self, timeout=None):
|
||||
self._Event__cond.acquire()
|
||||
try:
|
||||
if not self._Event__flag:
|
||||
self._Event__cond.wait(timeout)
|
||||
return self._Event__flag
|
||||
finally:
|
||||
self._Event__cond.release()
|
||||
|
||||
|
||||
class StoppableWSGIServer(WSGIServer):
|
||||
"""
|
||||
The code in this class is borrowed from the `SocketServer.BaseServer` class
|
||||
in Python 2.6. The important functionality here is that the server is non-
|
||||
blocking and that it can be shut down at any moment. This is made possible
|
||||
by the server regularly polling the socket and checking if it has been
|
||||
asked to stop.
|
||||
Note for the future: Once Django stops supporting Python 2.6, this class
|
||||
can be removed as `WSGIServer` will have this ability to shutdown on
|
||||
demand and will not require the use of the _ImprovedEvent class whose code
|
||||
is borrowed from Python 2.7.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(StoppableWSGIServer, self).__init__(*args, **kwargs)
|
||||
self.__is_shut_down = _ImprovedEvent()
|
||||
self.__serving = False
|
||||
|
||||
def serve_forever(self, poll_interval=0.5):
|
||||
"""
|
||||
Handle one request at a time until shutdown.
|
||||
|
||||
Polls for shutdown every poll_interval seconds.
|
||||
"""
|
||||
self.__serving = True
|
||||
self.__is_shut_down.clear()
|
||||
while self.__serving:
|
||||
r, w, e = select.select([self], [], [], poll_interval)
|
||||
if r:
|
||||
self._handle_request_noblock()
|
||||
self.__is_shut_down.set()
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
Stops the serve_forever loop.
|
||||
|
||||
Blocks until the loop has finished. This must be called while
|
||||
serve_forever() is running in another thread, or it will
|
||||
deadlock.
|
||||
"""
|
||||
self.__serving = False
|
||||
if not self.__is_shut_down.wait(2):
|
||||
raise RuntimeError(
|
||||
"Failed to shutdown the live test server in 2 seconds. The "
|
||||
"server might be stuck or generating a slow response.")
|
||||
|
||||
def handle_request(self):
|
||||
"""Handle one request, possibly blocking.
|
||||
"""
|
||||
fd_sets = select.select([self], [], [], None)
|
||||
if not fd_sets[0]:
|
||||
return
|
||||
self._handle_request_noblock()
|
||||
|
||||
def _handle_request_noblock(self):
|
||||
"""
|
||||
Handle one request, without blocking.
|
||||
|
||||
I assume that select.select has returned that the socket is
|
||||
readable before this function was called, so there should be
|
||||
no risk of blocking in get_request().
|
||||
"""
|
||||
try:
|
||||
request, client_address = self.get_request()
|
||||
except socket.error:
|
||||
return
|
||||
if self.verify_request(request, client_address):
|
||||
try:
|
||||
self.process_request(request, client_address)
|
||||
except Exception:
|
||||
self.handle_error(request, client_address)
|
||||
self.close_request(request)
|
||||
|
||||
|
||||
class _MediaFilesHandler(StaticFilesHandler):
|
||||
"""
|
||||
Handler for serving the media files. This is a private class that is
|
||||
meant to be used solely as a convenience by LiveServerThread.
|
||||
"""
|
||||
|
||||
def get_base_dir(self):
|
||||
return settings.MEDIA_ROOT
|
||||
|
||||
def get_base_url(self):
|
||||
return settings.MEDIA_URL
|
||||
|
||||
def serve(self, request):
|
||||
return serve(request, self.file_path(request.path),
|
||||
document_root=self.get_base_dir())
|
||||
|
||||
|
||||
class LiveServerThread(threading.Thread):
|
||||
"""
|
||||
Thread for running a live http server while the tests are running.
|
||||
"""
|
||||
|
||||
def __init__(self, address, port, connections_override=None):
|
||||
self.address = address
|
||||
self.port = port
|
||||
self.is_ready = threading.Event()
|
||||
self.error = None
|
||||
self.connections_override = connections_override
|
||||
super(LiveServerThread, self).__init__()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Sets up the live server and databases, and then loops over handling
|
||||
http requests.
|
||||
"""
|
||||
if self.connections_override:
|
||||
from django.db import connections
|
||||
# Override this thread's database connections with the ones
|
||||
# provided by the main thread.
|
||||
for alias, conn in self.connections_override.items():
|
||||
connections[alias] = conn
|
||||
try:
|
||||
# Create the handler for serving static and media files
|
||||
handler = StaticFilesHandler(_MediaFilesHandler(WSGIHandler()))
|
||||
# Instantiate and start the WSGI server
|
||||
self.httpd = StoppableWSGIServer(
|
||||
(self.address, self.port), QuietWSGIRequestHandler)
|
||||
self.httpd.set_app(handler)
|
||||
self.is_ready.set()
|
||||
self.httpd.serve_forever()
|
||||
except Exception, e:
|
||||
self.error = e
|
||||
self.is_ready.set()
|
||||
|
||||
def join(self, timeout=None):
|
||||
if hasattr(self, 'httpd'):
|
||||
# Stop the WSGI server
|
||||
self.httpd.shutdown()
|
||||
self.httpd.server_close()
|
||||
super(LiveServerThread, self).join(timeout)
|
||||
|
||||
|
||||
class LiveServerTestCase(TransactionTestCase):
|
||||
"""
|
||||
Does basically the same as TransactionTestCase but also launches a live
|
||||
http server in a separate thread so that the tests may use another testing
|
||||
framework, such as Selenium for example, instead of the built-in dummy
|
||||
client.
|
||||
Note that it inherits from TransactionTestCase instead of TestCase because
|
||||
the threads do not share the same transactions (unless if using in-memory
|
||||
sqlite) and each thread needs to commit all their transactions so that the
|
||||
other thread can see the changes.
|
||||
"""
|
||||
|
||||
@property
|
||||
def live_server_url(self):
|
||||
return 'http://%s' % self.__test_server_address
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
connections_override = {}
|
||||
for conn in connections.all():
|
||||
# If using in-memory sqlite databases, pass the connections to
|
||||
# the server thread.
|
||||
if (conn.settings_dict['ENGINE'] == 'django.db.backends.sqlite3'
|
||||
and conn.settings_dict['NAME'] == ':memory:'):
|
||||
# Explicitly enable thread-shareability for this connection
|
||||
conn.allow_thread_sharing = True
|
||||
connections_override[conn.alias] = conn
|
||||
|
||||
# Launch the live server's thread
|
||||
cls.__test_server_address = os.environ.get(
|
||||
'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081')
|
||||
try:
|
||||
host, port = cls.__test_server_address.split(':')
|
||||
except Exception:
|
||||
raise ImproperlyConfigured('Invalid address ("%s") for live '
|
||||
'server.' % cls.__test_server_address)
|
||||
cls.server_thread = LiveServerThread(
|
||||
host, int(port), connections_override)
|
||||
cls.server_thread.daemon = True
|
||||
cls.server_thread.start()
|
||||
|
||||
# Wait for the live server to be ready
|
||||
cls.server_thread.is_ready.wait()
|
||||
if cls.server_thread.error:
|
||||
raise cls.server_thread.error
|
||||
|
||||
super(LiveServerTestCase, cls).setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# There may not be a 'server_thread' attribute if setUpClass() for some
|
||||
# reasons has raised an exception.
|
||||
if hasattr(cls, 'server_thread'):
|
||||
# Terminate the live server's thread
|
||||
cls.server_thread.join()
|
||||
super(LiveServerTestCase, cls).tearDownClass()
|
||||
|
|
|
@ -122,6 +122,19 @@ Going beyond that, you can specify an individual test method like this:
|
|||
|
||||
./runtests.py --settings=path.to.settings i18n.TranslationTests.test_lazy_objects
|
||||
|
||||
Running the Selenium tests
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Some admin tests require Selenium 2, Firefox and Python >= 2.6 to work via a
|
||||
real Web browser. To allow those tests to run and not be skipped, you must
|
||||
install the selenium_ package (version > 2.13) into your Python path.
|
||||
|
||||
Then, run the tests normally, for example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./runtests.py --settings=test_sqlite admin_inlines
|
||||
|
||||
Running all the tests
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -135,6 +148,7 @@ dependencies:
|
|||
* setuptools_
|
||||
* memcached_, plus a :ref:`supported Python binding <memcached>`
|
||||
* gettext_ (:ref:`gettext_on_windows`)
|
||||
* selenium_ (if also using Python >= 2.6)
|
||||
|
||||
If you want to test the memcached cache backend, you'll also need to define
|
||||
a :setting:`CACHES` setting that points at your memcached instance.
|
||||
|
@ -149,6 +163,7 @@ associated tests will be skipped.
|
|||
.. _setuptools: http://pypi.python.org/pypi/setuptools/
|
||||
.. _memcached: http://www.danga.com/memcached/
|
||||
.. _gettext: http://www.gnu.org/software/gettext/manual/gettext.html
|
||||
.. _selenium: http://pypi.python.org/pypi/selenium
|
||||
|
||||
.. _contrib-apps:
|
||||
|
||||
|
|
|
@ -976,15 +976,22 @@ information.
|
|||
.. versionadded:: 1.2
|
||||
.. django-admin-option:: --failfast
|
||||
|
||||
Use the :djadminopt:`--failfast` option to stop running tests and report the failure
|
||||
immediately after a test fails.
|
||||
The ``--failfast`` option can be used to stop running tests and report the
|
||||
failure immediately after a test fails.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
.. django-admin-option:: --testrunner
|
||||
|
||||
The :djadminopt:`--testrunner` option can be used to control the test runner
|
||||
class that is used to execute tests. If this value is provided, it overrides
|
||||
the value provided by the :setting:`TEST_RUNNER` setting.
|
||||
The ``--testrunner`` option can be used to control the test runner class that
|
||||
is used to execute tests. If this value is provided, it overrides the value
|
||||
provided by the :setting:`TEST_RUNNER` setting.
|
||||
|
||||
.. versionadded:: 1.4
|
||||
.. django-admin-option:: --liveserver
|
||||
|
||||
The ``--liveserver`` option can be used to override the default address where
|
||||
the live server (used with :class:`~django.test.LiveServerTestCase`) is
|
||||
expected to run from. The default value is ``localhost:8081``.
|
||||
|
||||
testserver <fixture fixture ...>
|
||||
--------------------------------
|
||||
|
|
|
@ -40,6 +40,19 @@ before the release of Django 1.4.
|
|||
What's new in Django 1.4
|
||||
========================
|
||||
|
||||
Support for in-browser testing frameworks
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Django 1.4 now supports the integration with in-browser testing frameworks such
|
||||
as Selenium_ or Windmill_ thanks to the :class:`django.test.LiveServerTestCase`
|
||||
base class, allowing you to test the interactions between your site's front and
|
||||
back ends more comprehensively. See the
|
||||
:class:`documentation<django.test.LiveServerTestCase>` for more details and
|
||||
concrete examples.
|
||||
|
||||
.. _Windmill: http://www.getwindmill.com/
|
||||
.. _Selenium: http://seleniumhq.org/
|
||||
|
||||
``SELECT FOR UPDATE`` support
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
@ -581,21 +581,20 @@ Some of the things you can do with the test client are:
|
|||
* Test that a given request is rendered by a given Django template, with
|
||||
a template context that contains certain values.
|
||||
|
||||
Note that the test client is not intended to be a replacement for Twill_,
|
||||
Note that the test client is not intended to be a replacement for Windmill_,
|
||||
Selenium_, or other "in-browser" frameworks. Django's test client has
|
||||
a different focus. In short:
|
||||
|
||||
* Use Django's test client to establish that the correct view is being
|
||||
called and that the view is collecting the correct context data.
|
||||
|
||||
* Use in-browser frameworks such as Twill and Selenium to test *rendered*
|
||||
HTML and the *behavior* of Web pages, namely JavaScript functionality.
|
||||
* Use in-browser frameworks such as Windmill_ and Selenium_ to test *rendered*
|
||||
HTML and the *behavior* of Web pages, namely JavaScript functionality. Django
|
||||
also provides special support for those frameworks; see the section on
|
||||
:class:`~django.test.LiveServerTestCase` for more details.
|
||||
|
||||
A comprehensive test suite should use a combination of both test types.
|
||||
|
||||
.. _Twill: http://twill.idyll.org/
|
||||
.. _Selenium: http://seleniumhq.org/
|
||||
|
||||
Overview and a quick example
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -1753,6 +1752,97 @@ under MySQL with MyISAM tables)::
|
|||
def test_transaction_behavior(self):
|
||||
# ... conditional test code
|
||||
|
||||
Live test server
|
||||
----------------
|
||||
|
||||
.. versionadded:: 1.4
|
||||
|
||||
.. currentmodule:: django.test
|
||||
|
||||
.. class:: LiveServerTestCase()
|
||||
|
||||
``LiveServerTestCase`` does basically the same as
|
||||
:class:`~django.test.TransactionTestCase` with one extra feature: it launches a
|
||||
live Django server in the background on setup, and shuts it down on teardown.
|
||||
This allows the use of automated test clients other than the
|
||||
:ref:`Django dummy client <test-client>` such as, for example, the Selenium_ or
|
||||
Windmill_ clients, to execute a series of functional tests inside a browser and
|
||||
simulate a real user's actions.
|
||||
|
||||
By default the live server's address is `'localhost:8081'` and the full URL
|
||||
can be accessed during the tests with ``self.live_server_url``. If you'd like
|
||||
to change the default address (in the case, for example, where the 8081 port is
|
||||
already taken) you may pass a different one to the :djadmin:`test` command via
|
||||
the :djadminopt:`--liveserver` option, for example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./manage.py test --liveserver=localhost:8082
|
||||
|
||||
Another way of changing the default server address is by setting the
|
||||
`DJANGO_LIVE_TEST_SERVER_ADDRESS` environment variable.
|
||||
|
||||
To demonstrate how to use ``LiveServerTestCase``, let's write a simple Selenium
|
||||
test. First of all, you need to install the `selenium package`_ into your
|
||||
Python path:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install selenium
|
||||
|
||||
Then, add a ``LiveServerTestCase``-based test to your app's tests module
|
||||
(for example: ``myapp/tests.py``). The code for this test may look as follows:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from django.test import LiveServerTestCase
|
||||
from selenium.webdriver.firefox.webdriver import WebDriver
|
||||
|
||||
class MySeleniumTests(LiveServerTestCase):
|
||||
fixtures = ['user-data.json']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.selenium = WebDriver()
|
||||
super(MySeleniumTests, cls).setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super(MySeleniumTests, cls).tearDownClass()
|
||||
cls.selenium.quit()
|
||||
|
||||
def test_login(self):
|
||||
self.selenium.get('%s%s' % (self.live_server_url, '/login/'))
|
||||
username_input = self.selenium.find_element_by_name("username")
|
||||
username_input.send_keys('myuser')
|
||||
password_input = self.selenium.find_element_by_name("password")
|
||||
password_input.send_keys('secret')
|
||||
self.selenium.find_element_by_xpath('//input[@value="Log in"]').click()
|
||||
|
||||
Finally, you may run the test as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./manage.py test myapp.MySeleniumTests.test_login
|
||||
|
||||
This example will automatically open Firefox then go to the login page, enter
|
||||
the credentials and press the "Log in" button. Selenium offers other drivers in
|
||||
case you do not have Firefox installed or wish to use another browser. The
|
||||
example above is just a tiny fraction of what the Selenium client can do; check
|
||||
out the `full reference`_ for more details.
|
||||
|
||||
.. _Windmill: http://www.getwindmill.com/
|
||||
.. _Selenium: http://seleniumhq.org/
|
||||
.. _selenium package: http://pypi.python.org/pypi/selenium
|
||||
.. _full reference: http://readthedocs.org/docs/selenium-python/en/latest/api.html
|
||||
.. _Firefox: http://www.mozilla.com/firefox/
|
||||
|
||||
.. note::
|
||||
|
||||
``LiveServerTestCase`` makes use of the :doc:`staticfiles contrib app
|
||||
</howto/static-files>` so you'll need to have your project configured
|
||||
accordingly (in particular by setting :setting:`STATIC_URL`).
|
||||
|
||||
|
||||
Using different testing frameworks
|
||||
==================================
|
||||
|
@ -1833,11 +1923,9 @@ set up, execute and tear down the test suite.
|
|||
those options will be added to the list of command-line options that
|
||||
the :djadmin:`test` command can use.
|
||||
|
||||
|
||||
Attributes
|
||||
~~~~~~~~~~
|
||||
|
||||
|
||||
.. attribute:: DjangoTestSuiteRunner.option_list
|
||||
|
||||
.. versionadded:: 1.4
|
||||
|
|
|
@ -109,6 +109,10 @@ class SottoCapoInline(admin.TabularInline):
|
|||
model = SottoCapo
|
||||
|
||||
|
||||
class ProfileInline(admin.TabularInline):
|
||||
model = Profile
|
||||
extra = 1
|
||||
|
||||
site.register(TitleCollection, inlines=[TitleInline])
|
||||
# Test bug #12561 and #12778
|
||||
# only ModelAdmin media
|
||||
|
@ -124,3 +128,4 @@ site.register(Fashionista, inlines=[InlineWeakness])
|
|||
site.register(Holder4, Holder4Admin)
|
||||
site.register(Author, AuthorAdmin)
|
||||
site.register(CapoFamiglia, inlines=[ConsigliereInline, SottoCapoInline])
|
||||
site.register(ProfileCollection, inlines=[ProfileInline])
|
|
@ -136,3 +136,13 @@ class Consigliere(models.Model):
|
|||
class SottoCapo(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
capo_famiglia = models.ForeignKey(CapoFamiglia, related_name='+')
|
||||
|
||||
# Other models
|
||||
|
||||
class ProfileCollection(models.Model):
|
||||
pass
|
||||
|
||||
class Profile(models.Model):
|
||||
collection = models.ForeignKey(ProfileCollection, blank=True, null=True)
|
||||
first_name = models.CharField(max_length=100)
|
||||
last_name = models.CharField(max_length=100)
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
|
||||
from django.contrib.admin.helpers import InlineAdminForm
|
||||
from django.contrib.auth.models import User, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
@ -8,7 +9,8 @@ from django.test import TestCase
|
|||
# local test models
|
||||
from .admin import InnerInline
|
||||
from .models import (Holder, Inner, Holder2, Inner2, Holder3, Inner3, Person,
|
||||
OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book)
|
||||
OutfitItem, Fashionista, Teacher, Parent, Child, Author, Book, Profile,
|
||||
ProfileCollection)
|
||||
|
||||
|
||||
class TestInline(TestCase):
|
||||
|
@ -380,3 +382,105 @@ class TestInlinePermissions(TestCase):
|
|||
self.assertContains(response, 'value="4" id="id_inner2_set-TOTAL_FORMS"')
|
||||
self.assertContains(response, '<input type="hidden" name="inner2_set-0-id" value="%i"' % self.inner2_id)
|
||||
self.assertContains(response, 'id="id_inner2_set-0-DELETE"')
|
||||
|
||||
class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
|
||||
webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
|
||||
fixtures = ['admin-views-users.xml']
|
||||
urls = "regressiontests.admin_inlines.urls"
|
||||
|
||||
def test_add_inlines(self):
|
||||
"""
|
||||
Ensure that the "Add another XXX" link correctly adds items to the
|
||||
inline form.
|
||||
"""
|
||||
self.admin_login(username='super', password='secret')
|
||||
self.selenium.get('%s%s' % (self.live_server_url,
|
||||
'/admin/admin_inlines/profilecollection/add/'))
|
||||
|
||||
# Check that there's only one inline to start with and that it has the
|
||||
# correct ID.
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'#profile_set-group table tr.dynamic-profile_set')), 1)
|
||||
self.failUnlessEqual(self.selenium.find_element_by_css_selector(
|
||||
'.dynamic-profile_set:nth-of-type(1)').get_attribute('id'),
|
||||
'profile_set-0')
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-0 input[name=profile_set-0-first_name]')), 1)
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-0 input[name=profile_set-0-last_name]')), 1)
|
||||
|
||||
# Add an inline
|
||||
self.selenium.find_element_by_link_text('Add another Profile').click()
|
||||
|
||||
# Check that the inline has been added, that it has the right id, and
|
||||
# that it contains the right fields.
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'#profile_set-group table tr.dynamic-profile_set')), 2)
|
||||
self.failUnlessEqual(self.selenium.find_element_by_css_selector(
|
||||
'.dynamic-profile_set:nth-of-type(2)').get_attribute('id'), 'profile_set-1')
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 input[name=profile_set-1-first_name]')), 1)
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 input[name=profile_set-1-last_name]')), 1)
|
||||
|
||||
# Let's add another one to be sure
|
||||
self.selenium.find_element_by_link_text('Add another Profile').click()
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'#profile_set-group table tr.dynamic-profile_set')), 3)
|
||||
self.failUnlessEqual(self.selenium.find_element_by_css_selector(
|
||||
'.dynamic-profile_set:nth-of-type(3)').get_attribute('id'), 'profile_set-2')
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 input[name=profile_set-2-first_name]')), 1)
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 input[name=profile_set-2-last_name]')), 1)
|
||||
|
||||
# Enter some data and click 'Save'
|
||||
self.selenium.find_element_by_name('profile_set-0-first_name').send_keys('0 first name 1')
|
||||
self.selenium.find_element_by_name('profile_set-0-last_name').send_keys('0 last name 2')
|
||||
self.selenium.find_element_by_name('profile_set-1-first_name').send_keys('1 first name 1')
|
||||
self.selenium.find_element_by_name('profile_set-1-last_name').send_keys('1 last name 2')
|
||||
self.selenium.find_element_by_name('profile_set-2-first_name').send_keys('2 first name 1')
|
||||
self.selenium.find_element_by_name('profile_set-2-last_name').send_keys('2 last name 2')
|
||||
self.selenium.find_element_by_xpath('//input[@value="Save"]').click()
|
||||
|
||||
# Check that the objects have been created in the database
|
||||
self.assertEqual(ProfileCollection.objects.all().count(), 1)
|
||||
self.assertEqual(Profile.objects.all().count(), 3)
|
||||
|
||||
def test_delete_inlines(self):
|
||||
self.admin_login(username='super', password='secret')
|
||||
self.selenium.get('%s%s' % (self.live_server_url,
|
||||
'/admin/admin_inlines/profilecollection/add/'))
|
||||
|
||||
# Add a few inlines
|
||||
self.selenium.find_element_by_link_text('Add another Profile').click()
|
||||
self.selenium.find_element_by_link_text('Add another Profile').click()
|
||||
self.selenium.find_element_by_link_text('Add another Profile').click()
|
||||
self.selenium.find_element_by_link_text('Add another Profile').click()
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'#profile_set-group table tr.dynamic-profile_set')), 5)
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-0')), 1)
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-1')), 1)
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 1)
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-3')), 1)
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-4')), 1)
|
||||
|
||||
# Click on a few delete buttons
|
||||
self.selenium.find_element_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-1 td.delete a').click()
|
||||
self.selenium.find_element_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-2 td.delete a').click()
|
||||
# Verify that they're gone and that the IDs have been re-sequenced
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'#profile_set-group table tr.dynamic-profile_set')), 3)
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-0')), 1)
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-1')), 1)
|
||||
self.failUnlessEqual(len(self.selenium.find_elements_by_css_selector(
|
||||
'form#profilecollection_form tr.dynamic-profile_set#profile_set-2')), 1)
|
||||
|
|
|
@ -13,6 +13,7 @@ import re
|
|||
|
||||
from django import conf, bin, get_version
|
||||
from django.conf import settings
|
||||
from django.test.simple import DjangoTestSuiteRunner
|
||||
from django.utils import unittest
|
||||
|
||||
|
||||
|
@ -1058,6 +1059,50 @@ class ManageValidate(AdminScriptTestCase):
|
|||
self.assertOutput(out, '0 errors found')
|
||||
|
||||
|
||||
class CustomTestRunner(DjangoTestSuiteRunner):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
assert 'liveserver' not in kwargs
|
||||
super(CustomTestRunner, self).__init__(*args, **kwargs)
|
||||
|
||||
def run_tests(self, test_labels, extra_tests=None, **kwargs):
|
||||
pass
|
||||
|
||||
class ManageTestCommand(AdminScriptTestCase):
|
||||
def setUp(self):
|
||||
from django.core.management.commands.test import Command as TestCommand
|
||||
self.cmd = TestCommand()
|
||||
|
||||
def test_liveserver(self):
|
||||
"""
|
||||
Ensure that the --liveserver option sets the environment variable
|
||||
correctly.
|
||||
Refs #2879.
|
||||
"""
|
||||
|
||||
# Backup original state
|
||||
address_predefined = 'DJANGO_LIVE_TEST_SERVER_ADDRESS' in os.environ
|
||||
old_address = os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS')
|
||||
|
||||
self.cmd.handle(verbosity=0, testrunner='regressiontests.admin_scripts.tests.CustomTestRunner')
|
||||
|
||||
# Original state hasn't changed
|
||||
self.assertEqual('DJANGO_LIVE_TEST_SERVER_ADDRESS' in os.environ, address_predefined)
|
||||
self.assertEqual(os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS'), old_address)
|
||||
|
||||
self.cmd.handle(verbosity=0, testrunner='regressiontests.admin_scripts.tests.CustomTestRunner',
|
||||
liveserver='blah')
|
||||
|
||||
# Variable was correctly set
|
||||
self.assertEqual(os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'], 'blah')
|
||||
|
||||
# Restore original state
|
||||
if address_predefined:
|
||||
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = old_address
|
||||
else:
|
||||
del os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS']
|
||||
|
||||
|
||||
class ManageRunserver(AdminScriptTestCase):
|
||||
def setUp(self):
|
||||
from django.core.management.commands.runserver import BaseRunserverCommand
|
||||
|
|
|
@ -7,6 +7,7 @@ from django import forms
|
|||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin import widgets
|
||||
from django.contrib.admin.tests import AdminSeleniumWebDriverTestCase
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.db.models import DateField
|
||||
|
@ -407,3 +408,52 @@ class RelatedFieldWidgetWrapperTests(DjangoTestCase):
|
|||
# Used to fail with a name error.
|
||||
w = widgets.RelatedFieldWidgetWrapper(w, rel, widget_admin_site)
|
||||
self.assertFalse(w.can_add_related)
|
||||
|
||||
|
||||
class SeleniumFirefoxTests(AdminSeleniumWebDriverTestCase):
|
||||
webdriver_class = 'selenium.webdriver.firefox.webdriver.WebDriver'
|
||||
fixtures = ['admin-widgets-users.xml']
|
||||
urls = "regressiontests.admin_widgets.urls"
|
||||
|
||||
def test_show_hide_date_time_picker_widgets(self):
|
||||
"""
|
||||
Ensure that pressing the ESC key closes the date and time picker
|
||||
widgets.
|
||||
Refs #17064.
|
||||
"""
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
self.admin_login(username='super', password='secret', login_url='/')
|
||||
# Open a page that has a date and time picker widgets
|
||||
self.selenium.get('%s%s' % (self.live_server_url,
|
||||
'/admin_widgets/member/add/'))
|
||||
|
||||
# First, with the date picker widget ---------------------------------
|
||||
# Check that the date picker is hidden
|
||||
self.assertEqual(
|
||||
self.get_css_value('#calendarbox0', 'display'), 'none')
|
||||
# Click the calendar icon
|
||||
self.selenium.find_element_by_id('calendarlink0').click()
|
||||
# Check that the date picker is visible
|
||||
self.assertEqual(
|
||||
self.get_css_value('#calendarbox0', 'display'), 'block')
|
||||
# Press the ESC key
|
||||
self.selenium.find_element_by_tag_name('html').send_keys([Keys.ESCAPE])
|
||||
# Check that the date picker is hidden again
|
||||
self.assertEqual(
|
||||
self.get_css_value('#calendarbox0', 'display'), 'none')
|
||||
|
||||
# Then, with the time picker widget ----------------------------------
|
||||
# Check that the time picker is hidden
|
||||
self.assertEqual(
|
||||
self.get_css_value('#clockbox0', 'display'), 'none')
|
||||
# Click the time icon
|
||||
self.selenium.find_element_by_id('clocklink0').click()
|
||||
# Check that the time picker is visible
|
||||
self.assertEqual(
|
||||
self.get_css_value('#clockbox0', 'display'), 'block')
|
||||
# Press the ESC key
|
||||
self.selenium.find_element_by_tag_name('html').send_keys([Keys.ESCAPE])
|
||||
# Check that the time picker is hidden again
|
||||
self.assertEqual(
|
||||
self.get_css_value('#clockbox0', 'display'), 'none')
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
[
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "servers.person",
|
||||
"fields": {
|
||||
"name": "jane"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 2,
|
||||
"model": "servers.person",
|
||||
"fields": {
|
||||
"name": "robert"
|
||||
}
|
||||
}
|
||||
]
|
|
@ -0,0 +1 @@
|
|||
example media file
|
|
@ -0,0 +1,5 @@
|
|||
from django.db import models
|
||||
|
||||
|
||||
class Person(models.Model):
|
||||
name = models.CharField(max_length=256)
|
|
@ -0,0 +1 @@
|
|||
example static file
|
|
@ -3,13 +3,17 @@ Tests for django.core.servers.
|
|||
"""
|
||||
import os
|
||||
from urlparse import urljoin
|
||||
import urllib2
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import TestCase, LiveServerTestCase
|
||||
from django.core.handlers.wsgi import WSGIHandler
|
||||
from django.core.servers.basehttp import AdminMediaHandler
|
||||
from django.core.servers.basehttp import AdminMediaHandler, WSGIServerException
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from .models import Person
|
||||
|
||||
class AdminMediaHandlerTests(TestCase):
|
||||
|
||||
|
@ -68,3 +72,146 @@ class AdminMediaHandlerTests(TestCase):
|
|||
continue
|
||||
self.fail('URL: %s should have caused a ValueError exception.'
|
||||
% url)
|
||||
|
||||
|
||||
TEST_ROOT = os.path.dirname(__file__)
|
||||
TEST_SETTINGS = {
|
||||
'MEDIA_URL': '/media/',
|
||||
'MEDIA_ROOT': os.path.join(TEST_ROOT, 'media'),
|
||||
'STATIC_URL': '/static/',
|
||||
'STATIC_ROOT': os.path.join(TEST_ROOT, 'static'),
|
||||
}
|
||||
|
||||
|
||||
class LiveServerBase(LiveServerTestCase):
|
||||
urls = 'regressiontests.servers.urls'
|
||||
fixtures = ['testdata.json']
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Override settings
|
||||
cls.settings_override = override_settings(**TEST_SETTINGS)
|
||||
cls.settings_override.enable()
|
||||
super(LiveServerBase, cls).setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# Restore original settings
|
||||
cls.settings_override.disable()
|
||||
super(LiveServerBase, cls).tearDownClass()
|
||||
|
||||
def urlopen(self, url):
|
||||
server_address = os.environ.get(
|
||||
'DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081')
|
||||
base = 'http://%s' % server_address
|
||||
return urllib2.urlopen(base + url)
|
||||
|
||||
|
||||
class LiveServerAddress(LiveServerBase):
|
||||
"""
|
||||
Ensure that the address set in the environment variable is valid.
|
||||
Refs #2879.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Backup original environment variable
|
||||
address_predefined = 'DJANGO_LIVE_TEST_SERVER_ADDRESS' in os.environ
|
||||
old_address = os.environ.get('DJANGO_LIVE_TEST_SERVER_ADDRESS')
|
||||
|
||||
# Just the host is not accepted
|
||||
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'localhost'
|
||||
try:
|
||||
super(LiveServerAddress, cls).setUpClass()
|
||||
raise Exception("The line above should have raised an exception")
|
||||
except ImproperlyConfigured:
|
||||
pass
|
||||
|
||||
# The host must be valid
|
||||
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'blahblahblah:8081'
|
||||
try:
|
||||
super(LiveServerAddress, cls).setUpClass()
|
||||
raise Exception("The line above should have raised an exception")
|
||||
except WSGIServerException:
|
||||
pass
|
||||
|
||||
# If contrib.staticfiles isn't configured properly, the exception
|
||||
# should bubble up to the main thread.
|
||||
old_STATIC_URL = TEST_SETTINGS['STATIC_URL']
|
||||
TEST_SETTINGS['STATIC_URL'] = None
|
||||
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = 'localhost:8081'
|
||||
try:
|
||||
super(LiveServerAddress, cls).setUpClass()
|
||||
raise Exception("The line above should have raised an exception")
|
||||
except ImproperlyConfigured:
|
||||
pass
|
||||
TEST_SETTINGS['STATIC_URL'] = old_STATIC_URL
|
||||
|
||||
# Restore original environment variable
|
||||
if address_predefined:
|
||||
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = old_address
|
||||
else:
|
||||
del os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS']
|
||||
|
||||
def test_test_test(self):
|
||||
# Intentionally empty method so that the test is picked up by the
|
||||
# test runner and the overriden setUpClass() method is executed.
|
||||
pass
|
||||
|
||||
class LiveServerViews(LiveServerBase):
|
||||
def test_404(self):
|
||||
"""
|
||||
Ensure that the LiveServerTestCase serves 404s.
|
||||
Refs #2879.
|
||||
"""
|
||||
try:
|
||||
self.urlopen('/')
|
||||
except urllib2.HTTPError, err:
|
||||
self.assertEquals(err.code, 404, 'Expected 404 response')
|
||||
else:
|
||||
self.fail('Expected 404 response')
|
||||
|
||||
def test_view(self):
|
||||
"""
|
||||
Ensure that the LiveServerTestCase serves views.
|
||||
Refs #2879.
|
||||
"""
|
||||
f = self.urlopen('/example_view/')
|
||||
self.assertEquals(f.read(), 'example view')
|
||||
|
||||
def test_static_files(self):
|
||||
"""
|
||||
Ensure that the LiveServerTestCase serves static files.
|
||||
Refs #2879.
|
||||
"""
|
||||
f = self.urlopen('/static/example_static_file.txt')
|
||||
self.assertEquals(f.read(), 'example static file\n')
|
||||
|
||||
def test_media_files(self):
|
||||
"""
|
||||
Ensure that the LiveServerTestCase serves media files.
|
||||
Refs #2879.
|
||||
"""
|
||||
f = self.urlopen('/media/example_media_file.txt')
|
||||
self.assertEquals(f.read(), 'example media file\n')
|
||||
|
||||
|
||||
class LiveServerDatabase(LiveServerBase):
|
||||
|
||||
def test_fixtures_loaded(self):
|
||||
"""
|
||||
Ensure that fixtures are properly loaded and visible to the
|
||||
live server thread.
|
||||
Refs #2879.
|
||||
"""
|
||||
f = self.urlopen('/model_view/')
|
||||
self.assertEquals(f.read().splitlines(), ['jane', 'robert'])
|
||||
|
||||
def test_database_writes(self):
|
||||
"""
|
||||
Ensure that data written to the database by a view can be read.
|
||||
Refs #2879.
|
||||
"""
|
||||
self.urlopen('/create_model_instance/')
|
||||
names = [person.name for person in Person.objects.all()]
|
||||
self.assertEquals(names, ['jane', 'robert', 'emily'])
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^example_view/$', views.example_view),
|
||||
url(r'^model_view/$', views.model_view),
|
||||
url(r'^create_model_instance/$', views.create_model_instance),
|
||||
)
|
|
@ -0,0 +1,17 @@
|
|||
from django.http import HttpResponse
|
||||
from .models import Person
|
||||
|
||||
|
||||
def example_view(request):
|
||||
return HttpResponse('example view')
|
||||
|
||||
|
||||
def model_view(request):
|
||||
people = Person.objects.all()
|
||||
return HttpResponse('\n'.join([person.name for person in people]))
|
||||
|
||||
|
||||
def create_model_instance(request):
|
||||
person = Person(name='emily')
|
||||
person.save()
|
||||
return HttpResponse('')
|
|
@ -49,7 +49,10 @@ def geodjango(settings):
|
|||
|
||||
def get_test_modules():
|
||||
modules = []
|
||||
for loc, dirpath in (MODEL_TESTS_DIR_NAME, MODEL_TEST_DIR), (REGRESSION_TESTS_DIR_NAME, REGRESSION_TEST_DIR), (CONTRIB_DIR_NAME, CONTRIB_DIR):
|
||||
for loc, dirpath in (
|
||||
(MODEL_TESTS_DIR_NAME, MODEL_TEST_DIR),
|
||||
(REGRESSION_TESTS_DIR_NAME, REGRESSION_TEST_DIR),
|
||||
(CONTRIB_DIR_NAME, CONTRIB_DIR)):
|
||||
for f in os.listdir(dirpath):
|
||||
if (f.startswith('__init__') or
|
||||
f.startswith('.') or
|
||||
|
@ -150,7 +153,8 @@ def django_tests(verbosity, interactive, failfast, test_labels):
|
|||
settings.TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner'
|
||||
TestRunner = get_runner(settings)
|
||||
|
||||
test_runner = TestRunner(verbosity=verbosity, interactive=interactive, failfast=failfast)
|
||||
test_runner = TestRunner(verbosity=verbosity, interactive=interactive,
|
||||
failfast=failfast)
|
||||
failures = test_runner.run_tests(test_labels, extra_tests=extra_tests)
|
||||
|
||||
teardown(state)
|
||||
|
@ -175,7 +179,8 @@ def bisect_tests(bisection_label, options, test_labels):
|
|||
except ValueError:
|
||||
pass
|
||||
|
||||
subprocess_args = [sys.executable, __file__, '--settings=%s' % options.settings]
|
||||
subprocess_args = [
|
||||
sys.executable, __file__, '--settings=%s' % options.settings]
|
||||
if options.failfast:
|
||||
subprocess_args.append('--failfast')
|
||||
if options.verbosity:
|
||||
|
@ -235,7 +240,8 @@ def paired_tests(paired_test, options, test_labels):
|
|||
except ValueError:
|
||||
pass
|
||||
|
||||
subprocess_args = [sys.executable, __file__, '--settings=%s' % options.settings]
|
||||
subprocess_args = [
|
||||
sys.executable, __file__, '--settings=%s' % options.settings]
|
||||
if options.failfast:
|
||||
subprocess_args.append('--failfast')
|
||||
if options.verbosity:
|
||||
|
@ -244,7 +250,8 @@ def paired_tests(paired_test, options, test_labels):
|
|||
subprocess_args.append('--noinput')
|
||||
|
||||
for i, label in enumerate(test_labels):
|
||||
print '***** %d of %d: Check test pairing with %s' % (i+1, len(test_labels), label)
|
||||
print '***** %d of %d: Check test pairing with %s' % (
|
||||
i+1, len(test_labels), label)
|
||||
failures = subprocess.call(subprocess_args + [label, paired_test])
|
||||
if failures:
|
||||
print '***** Found problem pair with',label
|
||||
|
@ -257,19 +264,36 @@ if __name__ == "__main__":
|
|||
from optparse import OptionParser
|
||||
usage = "%prog [options] [module module module ...]"
|
||||
parser = OptionParser(usage=usage)
|
||||
parser.add_option('-v','--verbosity', action='store', dest='verbosity', default='1',
|
||||
parser.add_option(
|
||||
'-v','--verbosity', action='store', dest='verbosity', default='1',
|
||||
type='choice', choices=['0', '1', '2', '3'],
|
||||
help='Verbosity level; 0=minimal output, 1=normal output, 2=all output')
|
||||
parser.add_option('--noinput', action='store_false', dest='interactive', default=True,
|
||||
help='Verbosity level; 0=minimal output, 1=normal output, 2=all '
|
||||
'output')
|
||||
parser.add_option(
|
||||
'--noinput', action='store_false', dest='interactive', default=True,
|
||||
help='Tells Django to NOT prompt the user for input of any kind.')
|
||||
parser.add_option('--failfast', action='store_true', dest='failfast', default=False,
|
||||
help='Tells Django to stop running the test suite after first failed test.')
|
||||
parser.add_option('--settings',
|
||||
help='Python path to settings module, e.g. "myproject.settings". If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be used.')
|
||||
parser.add_option('--bisect', action='store', dest='bisect', default=None,
|
||||
help="Bisect the test suite to discover a test that causes a test failure when combined with the named test.")
|
||||
parser.add_option('--pair', action='store', dest='pair', default=None,
|
||||
help="Run the test suite in pairs with the named test to find problem pairs.")
|
||||
parser.add_option(
|
||||
'--failfast', action='store_true', dest='failfast', default=False,
|
||||
help='Tells Django to stop running the test suite after first failed '
|
||||
'test.')
|
||||
parser.add_option(
|
||||
'--settings',
|
||||
help='Python path to settings module, e.g. "myproject.settings". If '
|
||||
'this isn\'t provided, the DJANGO_SETTINGS_MODULE environment '
|
||||
'variable will be used.')
|
||||
parser.add_option(
|
||||
'--bisect', action='store', dest='bisect', default=None,
|
||||
help='Bisect the test suite to discover a test that causes a test '
|
||||
'failure when combined with the named test.')
|
||||
parser.add_option(
|
||||
'--pair', action='store', dest='pair', default=None,
|
||||
help='Run the test suite in pairs with the named test to find problem '
|
||||
'pairs.')
|
||||
parser.add_option(
|
||||
'--liveserver', action='store', dest='liveserver', default=None,
|
||||
help='Overrides the default address where the live server (used with '
|
||||
'LiveServerTestCase) is expected to run from. The default value '
|
||||
'is localhost:8081.'),
|
||||
options, args = parser.parse_args()
|
||||
if options.settings:
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = options.settings
|
||||
|
@ -279,11 +303,15 @@ if __name__ == "__main__":
|
|||
else:
|
||||
options.settings = os.environ['DJANGO_SETTINGS_MODULE']
|
||||
|
||||
if options.liveserver is not None:
|
||||
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = options.liveserver
|
||||
|
||||
if options.bisect:
|
||||
bisect_tests(options.bisect, options, args)
|
||||
elif options.pair:
|
||||
paired_tests(options.pair, options, args)
|
||||
else:
|
||||
failures = django_tests(int(options.verbosity), options.interactive, options.failfast, args)
|
||||
failures = django_tests(int(options.verbosity), options.interactive,
|
||||
options.failfast, args)
|
||||
if failures:
|
||||
sys.exit(bool(failures))
|
||||
|
|
Loading…
Reference in New Issue