Made django.test.testcases not depend on staticfiles contrib app.

Do this by introducing a django.contrib.staticfiles.testing.StaticLiveServerCase
unittest TestCase subclass.

Fixes #20739.
This commit is contained in:
Ramiro Morales 2013-06-01 14:24:46 -03:00
parent e0643cb676
commit e909ceae9b
9 changed files with 285 additions and 32 deletions

View File

@ -0,0 +1,14 @@
from django.test import LiveServerTestCase
from django.contrib.staticfiles.handlers import StaticFilesHandler
class StaticLiveServerCase(LiveServerTestCase):
"""
Extends django.test.LiveServerTestCase to transparently overlay at test
execution-time the assets provided by the staticfiles app finders. This
means you don't need to run collectstatic before or as a part of your tests
setup.
"""
static_handler = StaticFilesHandler

View File

@ -27,7 +27,7 @@ def serve(request, path, insecure=False, **kwargs):
in your URLconf. in your URLconf.
It uses the django.views.static view to serve the found files. It uses the django.views.static.serve() view to serve the found files.
""" """
if not settings.DEBUG and not insecure: if not settings.DEBUG and not insecure:
raise Http404 raise Http404

View File

@ -6,23 +6,25 @@ import errno
from functools import wraps from functools import wraps
import json import json
import os import os
import posixpath
import re import re
import sys import sys
import socket
import threading import threading
import unittest import unittest
from unittest import skipIf # Imported here for backward compatibility from unittest import skipIf # Imported here for backward compatibility
from unittest.util import safe_repr from unittest.util import safe_repr
try: try:
from urllib.parse import urlsplit, urlunsplit from urllib.parse import urlsplit, urlunsplit, urlparse, unquote
from urllib.request import url2pathname
except ImportError: # Python 2 except ImportError: # Python 2
from urlparse import urlsplit, urlunsplit from urlparse import urlsplit, urlunsplit, urlparse
from urllib import url2pathname, unquote
from django.conf import settings from django.conf import settings
from django.contrib.staticfiles.handlers import StaticFilesHandler
from django.core import mail from django.core import mail
from django.core.exceptions import ValidationError, ImproperlyConfigured from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.core.handlers.wsgi import WSGIHandler from django.core.handlers.wsgi import WSGIHandler
from django.core.handlers.base import get_path_info
from django.core.management import call_command from django.core.management import call_command
from django.core.management.color import no_style from django.core.management.color import no_style
from django.core.management.commands import flush from django.core.management.commands import flush
@ -933,10 +935,70 @@ class QuietWSGIRequestHandler(WSGIRequestHandler):
pass pass
class _MediaFilesHandler(StaticFilesHandler): class FSFilesHandler(WSGIHandler):
""" """
Handler for serving the media files. This is a private class that is WSGI middleware that intercepts calls to a directory, as defined by one of
meant to be used solely as a convenience by LiveServerThread. the *_ROOT settings, and serves those files, publishing them under *_URL.
"""
def __init__(self, application):
self.application = application
self.base_url = urlparse(self.get_base_url())
super(FSFilesHandler, self).__init__()
def _should_handle(self, path):
"""
Checks if the path should be handled. Ignores the path if:
* the host is provided as part of the base_url
* the request's path isn't under the media path (or equal)
"""
return path.startswith(self.base_url[2]) and not self.base_url[1]
def file_path(self, url):
"""
Returns the relative path to the file on disk for the given URL.
"""
relative_url = url[len(self.base_url[2]):]
return url2pathname(relative_url)
def get_response(self, request):
from django.http import Http404
if self._should_handle(request.path):
try:
return self.serve(request)
except Http404:
pass
return super(FSFilesHandler, self).get_response(request)
def serve(self, request):
os_rel_path = self.file_path(request.path)
final_rel_path = posixpath.normpath(unquote(os_rel_path)).lstrip('/')
return serve(request, final_rel_path, document_root=self.get_base_dir())
def __call__(self, environ, start_response):
if not self._should_handle(get_path_info(environ)):
return self.application(environ, start_response)
return super(FSFilesHandler, self).__call__(environ, start_response)
class _StaticFilesHandler(FSFilesHandler):
"""
Handler for serving static files. A private class that is meant to be used
solely as a convenience by LiveServerThread.
"""
def get_base_dir(self):
return settings.STATIC_ROOT
def get_base_url(self):
return settings.STATIC_URL
class _MediaFilesHandler(FSFilesHandler):
"""
Handler for serving the media files. A private class that is meant to be
used solely as a convenience by LiveServerThread.
""" """
def get_base_dir(self): def get_base_dir(self):
@ -945,22 +1007,19 @@ class _MediaFilesHandler(StaticFilesHandler):
def get_base_url(self): def get_base_url(self):
return settings.MEDIA_URL return settings.MEDIA_URL
def serve(self, request):
relative_url = request.path[len(self.base_url[2]):]
return serve(request, relative_url, document_root=self.get_base_dir())
class LiveServerThread(threading.Thread): class LiveServerThread(threading.Thread):
""" """
Thread for running a live http server while the tests are running. Thread for running a live http server while the tests are running.
""" """
def __init__(self, host, possible_ports, connections_override=None): def __init__(self, host, possible_ports, static_handler, connections_override=None):
self.host = host self.host = host
self.port = None self.port = None
self.possible_ports = possible_ports self.possible_ports = possible_ports
self.is_ready = threading.Event() self.is_ready = threading.Event()
self.error = None self.error = None
self.static_handler = static_handler
self.connections_override = connections_override self.connections_override = connections_override
super(LiveServerThread, self).__init__() super(LiveServerThread, self).__init__()
@ -976,7 +1035,7 @@ class LiveServerThread(threading.Thread):
connections[alias] = conn connections[alias] = conn
try: try:
# Create the handler for serving static and media files # Create the handler for serving static and media files
handler = StaticFilesHandler(_MediaFilesHandler(WSGIHandler())) handler = self.static_handler(_MediaFilesHandler(WSGIHandler()))
# Go through the list of possible ports, hoping that we can find # Go through the list of possible ports, hoping that we can find
# one that is free to use for the WSGI server. # one that is free to use for the WSGI server.
@ -1028,6 +1087,8 @@ class LiveServerTestCase(TransactionTestCase):
other thread can see the changes. other thread can see the changes.
""" """
static_handler = _StaticFilesHandler
@property @property
def live_server_url(self): def live_server_url(self):
return 'http://%s:%s' % ( return 'http://%s:%s' % (
@ -1069,8 +1130,9 @@ class LiveServerTestCase(TransactionTestCase):
except Exception: except Exception:
msg = 'Invalid address ("%s") for live server.' % specified_address msg = 'Invalid address ("%s") for live server.' % specified_address
six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg), sys.exc_info()[2]) six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg), sys.exc_info()[2])
cls.server_thread = LiveServerThread( cls.server_thread = LiveServerThread(host, possible_ports,
host, possible_ports, connections_override) cls.static_handler,
connections_override=connections_override)
cls.server_thread.daemon = True cls.server_thread.daemon = True
cls.server_thread.start() cls.server_thread.start()

View File

@ -100,6 +100,33 @@ this by adding the following snippet to your urls.py::
the given prefix is local (e.g. ``/static/``) and not a URL (e.g. the given prefix is local (e.g. ``/static/``) and not a URL (e.g.
``http://static.example.com/``). ``http://static.example.com/``).
.. _staticfiles-testing-support:
Testing
=======
When running tests that use actual HTTP requests instead of the built-in
testing client (i.e. when using the built-in :class:`LiveServerTestCase
<django.test.LiveServerTestCase>`) the static assets need to be served along
the rest of the content so the test environment reproduces the real one as
faithfully as possible, but ``LiveServerTestCase`` has only very basic static
file-serving functionality: It doesn't know about the finders feature of the
``staticfiles`` application and assumes the static content has already been
collected under :setting:`STATIC_ROOT`.
Because of this, ``staticfiles`` ships its own
:class:`django.contrib.staticfiles.testing.StaticLiveServerCase`, a subclass
of the built-in one that has the ability to transparently serve all the assets
during execution of these tests in a way very similar to what we get at
development time with ``DEBUG = True``, i.e. without having to collect them
using :djadmin:`collectstatic` first.
.. versionadded:: 1.7
:class:`django.contrib.staticfiles.testing.StaticLiveServerCase` is new in
Django 1.7. Previously its functionality was provided by
:class:`django.test.LiveServerTestCase`.
Deployment Deployment
========== ==========

View File

@ -406,3 +406,26 @@ files in app directories.
That's because this view is **grossly inefficient** and probably That's because this view is **grossly inefficient** and probably
**insecure**. This is only intended for local development, and should **insecure**. This is only intended for local development, and should
**never be used in production**. **never be used in production**.
Specialized test case to support 'live testing'
-----------------------------------------------
.. class:: testing.StaticLiveServerCase
This unittest TestCase subclass extends :class:`django.test.LiveServerTestCase`.
Just like its parent, you can use it to write tests that involve running the
code under test and consuming it with testing tools through HTTP (e.g. Selenium,
PhantomJS, etc.), because of which it's needed that the static assets are also
published.
But given the fact that it makes use of the
:func:`django.contrib.staticfiles.views.serve` view described above, it can
transparently overlay at test execution-time the assets provided by the
``staticfiles`` finders. This means you don't need to run
:djadmin:`collectstatic` before or as a part of your tests setup.
.. versionadded:: 1.7
``StaticLiveServerCase`` is new in Django 1.7. Previously its functionality
was provided by :class:`django.test.LiveServerTestCase`.

View File

@ -332,6 +332,20 @@ Miscellaneous
Define a ``get_absolute_url()`` method on your own custom user object or use Define a ``get_absolute_url()`` method on your own custom user object or use
:setting:`ABSOLUTE_URL_OVERRIDES` if you want a URL for your user. :setting:`ABSOLUTE_URL_OVERRIDES` if you want a URL for your user.
* The static asset-serving functionality of the
:class:`django.test.LiveServerTestCase` class has been simplified: Now it's
only able to serve content already present in :setting:`STATIC_ROOT` when
tests are run. The ability to transparently serve all the static assets
(similarly to what one gets with :setting:`DEBUG = True <DEBUG>` at
development-time) has been moved to a new class that lives in the
``staticfiles`` application (the one actually in charge of such feature):
:class:`django.contrib.staticfiles.testing.StaticLiveServerCase`. In other
words, ``LiveServerTestCase`` itself is less powerful but at the same time
has less magic.
Rationale behind this is removal of dependency of non-contrib code on
contrib applications.
Features deprecated in 1.7 Features deprecated in 1.7
========================== ==========================

View File

@ -1041,11 +1041,25 @@ out the `full reference`_ for more details.
.. _full reference: http://selenium-python.readthedocs.org/en/latest/api.html .. _full reference: http://selenium-python.readthedocs.org/en/latest/api.html
.. _Firefox: http://www.mozilla.com/firefox/ .. _Firefox: http://www.mozilla.com/firefox/
.. note:: .. versionchanged:: 1.7
``LiveServerTestCase`` makes use of the :doc:`staticfiles contrib app Before Django 1.7 ``LiveServerTestCase`` used to rely on the
</howto/static-files/index>` so you'll need to have your project configured :doc:`staticfiles contrib app </howto/static-files/index>` to get the
accordingly (in particular by setting :setting:`STATIC_URL`). static assets of the application(s) under test transparently served at their
expected locations during the execution of these tests.
In Django 1.7 this dependency of core functionality on a ``contrib``
appplication has been removed, because of which ``LiveServerTestCase``
ability in this respect has been retrofitted to simply publish the contents
of the file system under :setting:`STATIC_ROOT` at the :setting:`STATIC_URL`
URL.
If you use the ``staticfiles`` app in your project and need to perform live
testing then you might want to consider using the
:class:`~django.contrib.staticfiles.testing.StaticLiveServerCase` subclass
shipped with it instead because it's the one that implements the original
behavior now. See :ref:`the relevant documentation
<staticfiles-testing-support>` for more details.
.. note:: .. note::

View File

@ -82,13 +82,6 @@ class LiveServerAddress(LiveServerBase):
cls.raises_exception('localhost:8081-blah', ImproperlyConfigured) cls.raises_exception('localhost:8081-blah', ImproperlyConfigured)
cls.raises_exception('localhost:8081-8082-8083', ImproperlyConfigured) cls.raises_exception('localhost:8081-8082-8083', ImproperlyConfigured)
# 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
cls.raises_exception('localhost:8081', ImproperlyConfigured)
TEST_SETTINGS['STATIC_URL'] = old_STATIC_URL
# Restore original environment variable # Restore original environment variable
if address_predefined: if address_predefined:
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = old_address os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = old_address
@ -145,13 +138,18 @@ class LiveServerViews(LiveServerBase):
f = self.urlopen('/static/example_static_file.txt') f = self.urlopen('/static/example_static_file.txt')
self.assertEqual(f.read().rstrip(b'\r\n'), b'example static file') self.assertEqual(f.read().rstrip(b'\r\n'), b'example static file')
def test_collectstatic_emulation(self): def test_no_collectstatic_emulation(self):
""" """
Test LiveServerTestCase use of staticfiles' serve() allows it to Test that LiveServerTestCase reports a 404 status code when HTTP client
discover app's static assets without having to collectstatic first. tries to access a static file that isn't explictly put under
STATIC_ROOT.
""" """
f = self.urlopen('/static/another_app/another_app_static_file.txt') try:
self.assertEqual(f.read().rstrip(b'\r\n'), b'static file from another_app') self.urlopen('/static/another_app/another_app_static_file.txt')
except HTTPError as err:
self.assertEqual(err.code, 404, 'Expected 404 response')
else:
self.fail('Expected 404 response (got %d)' % err.code)
def test_media_files(self): def test_media_files(self):
""" """

View File

@ -0,0 +1,101 @@
"""
A subset of the tests in tests/servers/tests exercicing
django.contrib.staticfiles.testing.StaticLiveServerCase instead of
django.test.LiveServerTestCase.
"""
import os
try:
from urllib.request import urlopen
except ImportError: # Python 2
from urllib2 import urlopen
from django.core.exceptions import ImproperlyConfigured
from django.test.utils import override_settings
from django.utils._os import upath
from django.contrib.staticfiles.testing import StaticLiveServerCase
TEST_ROOT = os.path.dirname(upath(__file__))
TEST_SETTINGS = {
'MEDIA_URL': '/media/',
'STATIC_URL': '/static/',
'MEDIA_ROOT': os.path.join(TEST_ROOT, 'project', 'site_media', 'media'),
'STATIC_ROOT': os.path.join(TEST_ROOT, 'project', 'site_media', 'static'),
}
class LiveServerBase(StaticLiveServerCase):
available_apps = []
@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()
class StaticLiveServerChecks(LiveServerBase):
@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')
# 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
cls.raises_exception('localhost:8081', ImproperlyConfigured)
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']
@classmethod
def tearDownClass(cls):
# skip it, as setUpClass doesn't call its parent either
pass
@classmethod
def raises_exception(cls, address, exception):
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = address
try:
super(StaticLiveServerChecks, cls).setUpClass()
raise Exception("The line above should have raised an exception")
except exception:
pass
finally:
super(StaticLiveServerChecks, cls).tearDownClass()
def test_test_test(self):
# Intentionally empty method so that the test is picked up by the
# test runner and the overridden setUpClass() method is executed.
pass
class StaticLiveServerView(LiveServerBase):
def urlopen(self, url):
return urlopen(self.live_server_url + url)
def test_collectstatic_emulation(self):
"""
Test that StaticLiveServerCase use of staticfiles' serve() allows it to
discover app's static assets without having to collectstatic first.
"""
f = self.urlopen('/static/test/file.txt')
self.assertEqual(f.read().rstrip(b'\r\n'), b'In app media directory.')