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:
Julien Phalip 2011-12-22 08:33:58 +00:00
parent 45e3dff5ac
commit 2f02a05ffb
21 changed files with 921 additions and 46 deletions

View File

@ -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))

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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:

View File

@ -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 ...>
--------------------------------

View File

@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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

View File

@ -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])

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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')

View File

@ -0,0 +1,16 @@
[
{
"pk": 1,
"model": "servers.person",
"fields": {
"name": "jane"
}
},
{
"pk": 2,
"model": "servers.person",
"fields": {
"name": "robert"
}
}
]

View File

@ -0,0 +1 @@
example media file

View File

@ -0,0 +1,5 @@
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=256)

View File

@ -0,0 +1 @@
example static file

View 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'])

View File

@ -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),
)

View File

@ -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('')

View File

@ -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))