#20739 -- Start removing django.test.testcases dep on d.c.staticfiles. #1354

wants to merge 27 commits into
Jump to file or symbol
Failed to load files and symbols.
+285 −32
Diff settings


Just for now

@@ -0,0 +1,14 @@
+from django.test import LiveServerTestCase
+from django.contrib.staticfiles.handlers import StaticFilesHandler
+class StaticLiveServerCase(LiveServerTestCase):
+ """
+ This class 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
@@ -27,7 +27,7 @@ def serve(request, path, insecure=False, **kwargs):
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:
raise Http404
@@ -6,23 +6,25 @@
from functools import wraps
import json
import os
+import posixpath
import re
import sys
-import socket
import threading
import unittest
from unittest import skipIf # Imported here for backward compatibility
from unittest.util import safe_repr
- from urllib.parse import urlsplit, urlunsplit
+ from urllib.parse import urlsplit, urlunsplit, urlparse, unquote
+ from urllib.request import url2pathname
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.contrib.staticfiles.handlers import StaticFilesHandler
from django.core import mail
from django.core.exceptions import ValidationError, ImproperlyConfigured
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.color import no_style
from django.core.management.commands import flush
@@ -933,10 +935,70 @@ def log_message(*args):
-class _MediaFilesHandler(StaticFilesHandler):
+class FSFilesHandler(WSGIHandler):
- Handler for serving the media files. This is a private class that is
- meant to be used solely as a convenience by LiveServerThread.
+ WSGI middleware that intercepts calls to a directory, as defined by one of
+ 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):
@@ -945,22 +1007,19 @@ def get_base_dir(self):
def get_base_url(self):
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):
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.port = None
self.possible_ports = possible_ports
self.is_ready = threading.Event()
self.error = None
+ self.static_handler = static_handler
self.connections_override = connections_override
super(LiveServerThread, self).__init__()
@@ -976,7 +1035,7 @@ def run(self):
connections[alias] = conn
# 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
# one that is free to use for the WSGI server.
@@ -1028,6 +1087,8 @@ class LiveServerTestCase(TransactionTestCase):
other thread can see the changes.
+ static_handler = _StaticFilesHandler
def live_server_url(self):
return 'http://%s:%s' % (
@@ -1069,8 +1130,9 @@ def setUpClass(cls):
except Exception:
msg = 'Invalid address ("%s") for live server.' % specified_address
six.reraise(ImproperlyConfigured, ImproperlyConfigured(msg), sys.exc_info()[2])
- cls.server_thread = LiveServerThread(
- host, possible_ports, connections_override)
+ cls.server_thread = LiveServerThread(host, possible_ports,
+ cls.static_handler,
+ connections_override=connections_override)
cls.server_thread.daemon = True
@@ -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.
+.. _staticfiles-testing-support:
+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`.
@@ -406,3 +406,26 @@ files in app directories.
That's because this view is **grossly inefficient** and probably
**insecure**. This is only intended for local development, and should
**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
+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`.
@@ -332,6 +332,20 @@ Miscellaneous
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.
+* 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
@@ -1041,11 +1041,25 @@ out the `full reference`_ for more details.
.. _full reference: http://selenium-python.readthedocs.org/en/latest/api.html
.. _Firefox: http://www.mozilla.com/firefox/
-.. note::
+.. versionchanged:: 1.7
- ``LiveServerTestCase`` makes use of the :doc:`staticfiles contrib app
- </howto/static-files/index>` so you'll need to have your project configured
- accordingly (in particular by setting :setting:`STATIC_URL`).
+ Before Django 1.7 ``LiveServerTestCase`` used to rely on the
+ :doc:`staticfiles contrib app </howto/static-files/index>` to get the
+ 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 old
+ behavior now. See :ref:`the relevant documentation
+ <staticfiles-testing-support>` for more details.
.. note::
@@ -82,13 +82,6 @@ def setUpClass(cls):
cls.raises_exception('localhost:8081-blah', 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.
- cls.raises_exception('localhost:8081', ImproperlyConfigured)
# Restore original environment variable
if address_predefined:
os.environ['DJANGO_LIVE_TEST_SERVER_ADDRESS'] = old_address
@@ -145,13 +138,18 @@ def test_static_files(self):
f = self.urlopen('/static/example_static_file.txt')
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
- discover app's static assets without having to collectstatic first.
+ Test that LiveServerTestCase reports a 404 status code when HTTP client
+ tries to access a static file that isn't explictly put under
- f = self.urlopen('/static/another_app/another_app_static_file.txt')
- self.assertEqual(f.read().rstrip(b'\r\n'), b'static file from another_app')
+ try:
+ 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):
Oops, something went wrong.