Fixing spurious teardown test failures with Django’s LiveServerTestCase

If you are using Django’s LiveServerTestCase to do browser-based testing, you might be running into sporadic database failures in between individual test methods.

The root cause is that the server is bound to the lifetime of the test case (aka class), where as its parent TransactionTestCase class does database changes before & after each individual test method (by calling setUp, tearDown and so on).

The problems occur when you still have leftover traffic hitting the development server (stray request, browser instance that’s taking its time to exit, etc) which will conflict with the database changes being attempted.

The solution is to bind the test server’s lifetime to every test method, ensuring the server is no longer running (and no requests can be processed) before any database changes can be attempted.

Here’s an implementation I wrote to successfully resolve the issue in a past project:

from django.test import TransactionTestCase, modify_settings
from django.test.testcases import LiveServerThread
from django.test.testcases import _StaticFilesHandler as LiveServerStaticFilesHandler


class IsolatedLiveServerTestCase(TransactionTestCase):
    """
    Django LiveServerTestCase reimplementation that restarts the live server
    in between test methods. Mostly a copy/paste of LiveServerTestCase,
    but with most methods changed to be instance methods rather than class methods.

    Works around a bug/limitation of the Django implementation where the live server
    is running for the lifetime of the test *class*, while its own parent TransactionTestCase
    does database changes *in between* tests, which opens the potential for background requests
    (initiated by JS, etc) to still be in flight in between tests, conflicting with the database changes
    that happen in between tests (tables are truncated, fixtures are reloaded, etc).

    Without this, you're liable to get spurious database-related errors
    (conflicts, deadlocks, etc) in between tests leading to flakiness.

    WARNING: only PostgreSQL support is implemented. SQLite DB connection sharing overrides
    aren't implemented, as a result SQLite behavior is considered undefined.
    """

    host = "localhost"
    port = 0
    server_thread_class = LiveServerThread
    static_handler = LiveServerStaticFilesHandler

    @property
    def live_server_url(self):
        return "http://%s:%s" % (self.host, self.server_thread.port)

    @property
    def allowed_host(self):
        return self.host

    def setUp(self):
        super().setUp()

        self._live_server_modified_settings = modify_settings(
            ALLOWED_HOSTS={"append": self.allowed_host},
        )
        self._live_server_modified_settings.enable()
        self.addCleanup(self._live_server_modified_settings.disable)

        self._start_server_thread()

    def _start_server_thread(self):
        self.server_thread = self.server_thread_class(
            self.host,
            self.static_handler,
            connections_override={},
            port=self.port,
        )
        self.server_thread.daemon = True
        self.server_thread.start()
        self.addCleanup(self.server_thread.terminate)

        self.server_thread.is_ready.wait()

        if error := self.server_thread.error:
            raise error

Enjoy!


Like what you see? I may be available for Python & Django-related consulting. Reach out on LinkedIn or via e-mail if you need assistance with technical matters!

 
1
Kudos
 
1
Kudos

Now read this

StrongSwan IKEv2 iOS road-warrior server config

Note: this is an old post from 2016 and hasn’t been updated nor reviewed since. Nowadays I would recommend using Wireguard which is harder to configure insecurely, or at least use something like OpenWrt which provides a nice GUI to... Continue →