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!