Chapter 22 · The test framework
22.15 A server in a thread: LiveServerTestCase
LiveServerTestCase runs the project behind a real HTTP server on a thread of the test process, one server for each test class, at LiveServerTestCase. The server is a ThreadedWSGIServer run by a LiveServerThread, behind handlers that serve static and media files, and a SQLite test database in memory is shared with it through the test’s own connection.
The test client has no socket a browser could reach (The test client: Client and ClientHandler). The class’s docstring gives its purpose: a live server in a separate thread, so that tests may use another framework, Selenium for one, in place of the test client. It also says why the class is a TransactionTestCase: threads do not share transactions, unless they use SQLite’s database in memory, and each must commit for the other to see its changes (Emptying the tables after each test: TransactionTestCase; LiveServerTestCase).
Starting the server: setUpClass
LiveServerTestCase is "localhost" and LiveServerTestCase is 0; LiveServerTestCase and LiveServerTestCase name the thread’s class and the outermost handler. LiveServerTestCase and LiveServerTestCase are class properties (classproperty); the URL is made from host and the running thread’s port.
LiveServerTestCase runs its parents’ setUpClass, which enters the class’s settings overrides (Settings for one test: override_settings and TestContextDecorator), so the server’s handlers read the overridden settings. It appends allowed_host to ALLOWED_HOSTS with modify_settings, entered as a context of the class, and starts the server with LiveServerTestCase:
LiveServerTestCasechooses the connections to share, and._make_connections_override BaseDatabaseWrapperlets another thread use each (Opening and closing a connection)..inc_thread_sharing LiveServerTestCasemakes the thread from._create_server_thread server_thread_class, with the host, the port,static_handlerand those connections; it is made a daemon and started.LiveServerTestCaseis registered as a class cleanup.._terminate_thread - The test’s thread waits on
LiveServerThread, a.is_ready threading.Event, and raises the error the thread kept, if any.
unittest runs a class’s cleanups even when setUpClass raises, so a thread whose server failed to start is still joined and its connections’ sharing undone. _terminate_thread calls LiveServerThread, then BaseDatabaseWrapper on each shared connection.
The thread: LiveServerThread
LiveServerThread puts the shared connections in connections under their aliases, in place of this thread’s own, and builds the application: a WSGIHandler inside a _MediaFilesHandler inside the class’s static_handler. It makes a ThreadedWSGIServer on the host and port, with allow_reuse_address=False and QuietWSGIRequestHandler, which logs no request so as not, its docstring says, to clutter the test output. For port 0 the system gives a free port, which it reads back from the bound socket. Only then does it set is_ready and serve. An exception on the way is kept as the thread’s error and is_ready set, to end the wait; in a finally the thread closes its connections.
The server is built on the development server’s WSGIServer and WSGIRequestHandler; ThreadedWSGIServer adds a thread for each client’s connection, and is not the threaded class runserver makes (The development server). ThreadedWSGIServer puts the shared connections in place on each such thread, and ThreadedWSGIServer closes the thread’s connections when the client’s connection ends. LiveServerThread, if is_ready is set, shuts down and closes the server, if one was made, and joins the thread; otherwise it does nothing.
One connection for two threads
A SQLite test database is in memory unless its "TEST" "NAME" names a file (Test databases: BaseDatabaseCreation). _make_connections_override takes those of the test thread’s connections that are SQLite’s to a database in memory. The server’s threads use those very objects, so they see what the test wrote, committed or not, and their closing does not destroy the database: SQLite’s DatabaseWrapper ignores a close of a database in memory. The documentation warns against the two threads querying through it at once: a test should wait for a page to load before going on.
Any other database is not shared: each thread of the server opens its own connection to the test database, and sees what the test has committed.
Static and media files: FSFilesHandler
FSFilesHandler is a WSGIHandler around another WSGI application that answers the paths under a URL from a directory. FSFilesHandler hands the application inside every request whose path is not under the URL’s path, and all of them when the URL names a host. A path it keeps goes through WSGIHandler to FSFilesHandler, and FSFilesHandler gives what follows the URL’s path, normalised, to django, with the handler’s directory as the document root, whatever settings.DEBUG is (The static file view). A missing file, or a directory, raises Http404, and the request goes on to BaseHandler: the file handler’s own chain, not the application inside, for as a WSGIHandler it built the middleware chain when it was made.
_StaticFilesHandler serves STATIC_ROOT under STATIC_URL, and _MediaFilesHandler serves MEDIA_ROOT under MEDIA_URL, so static files are served only once collected. The documentation gives that as the reason for StaticLiveServerTestCase, whose static_handler is the static files application’s StaticFilesHandler, serving from the finders (Serving static files: StaticFilesHandler and the development server; docs/).
The harbour office over HTTP
A LiveServerTestCase in the harbour office’s tests, with the settings the handlers need, and a notice beside it, notices/tides.txt:
# harbour/office/tests/test_live.py
from pathlib import Path
from urllib.request import urlopen
from django.test import LiveServerTestCase, override_settings
from harbour.fleet.models import Port, Ship
NOTICES = Path(__file__).resolve().parent / "notices"
@override_settings(STATIC_URL="/static/", MEDIA_URL="/media/", MEDIA_ROOT=NOTICES)
class ShipLiveTests(LiveServerTestCase):
def test_list_over_http(self):
valletta = Port.objects.create(name="Valletta", country="Malta")
Ship.objects.create(name="Beagle", tonnage=235, home=valletta)
print(self.live_server_url)
with urlopen(self.live_server_url + "/ships/") as response:
self.assertIn("Beagle", response.read().decode())
def test_notice(self):
with urlopen(self.live_server_url + "/media/tides.txt") as response:
print(response.status, response.headers["Content-Type"], response.read())
$ python manage.py test harbour.office.tests.test_live
Found 2 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
http://localhost:46104
.200 text/plain b'High water 06:12\n'
.
----------------------------------------------------------------------
Ran 2 tests in 4.650s
OK
Destroying test database for alias 'default'...
The port is this run’s. The test made Beagle on its own thread, and ship_list, on a thread of the server, read it through the same connection; the notice came from MEDIA_ROOT.
The testserver command is another way to serve a test database: the development server over one loaded with fixtures (The development server).