On my journey of adding multi-tenancy to my app, I have used django-tenant-schemas
and the testing utilities it provides to make my unit tests work with the tenants or main site.
However the functional tests is another kettle of fish as there is not utility to use for them.
Functional Tests
From the django docs on the topic:
Use in-browser frameworks like Selenium to test rendered HTML and the behavior of Web pages, namely JavaScript functionality. Django also provides special support for those frameworks; see the section on LiveServerTestCase for more details.
LiveServerTestCase
is the same as a TransactionTestCase
with one added feature in that it launches a live django server in the background for browser based tests and simulations with selenium.
Good to know that the TransactionTestCase
differs from a normal TestCase
in the way the db is reset after tests. TransactionTestCase will truncate the tables, whereas TestCase will just rollback the transaction.
The live server listens on
localhost
and binds to port 0 which uses a free port assigned by the operating system. The server’s URL can be accessed withself.live_server_url
during the tests.
If you are using the staticfiles
app then you need to use StaticLiveServerTestCase
to ensure the static files are loaded.
Now what are the issues that I can foresee:
- There is a public facing site (with
PUBLIC_URLS
) and tenant sites that run on different domains. I would need to test both sites with selenium. - The test tenant is not created anymore and I will have to subclass the
StaticLiveServerTestsCase
to create the test tenant (and public tenant that uses the public schema)
Other than that all is good.
The Public Schema
Functional tests should simulate reality as much as possible. So I want there to be a public schema and at least 2 tenants (Update: nope I don’t need to do this, as this should be tested by django_tenant_schemas itself). So in creating the base functional test case, these 3 things need to be created.
The first thing to do is get the setUp
of the functional test right. Now regular setup of the app needs to be linked with a tenant db and not public as the public schema will only have the SHARED_APPS
and not the TENANT_APPS
so we need to separate it out.
So I started on the public test case, I took everything to its base bones and made a simple request:
self.browser.get(f'{ self.live_server_url }')
Which gave a bad request error. Usually when you get a 400
it is because your public schema is not there. So you will need to create your TenantModel Record and ensure the domain is localhost.
class PublicFunctionalTest(BaseFunctionalTest):
'''Functional test just for the public facing site'''
def setUp(self):
'''Create the public schema'''
super().setUp()
Client.objects.create(
domain_url='localhost',
schema_name='public',
name='public'
)
This worked well and I was sent to the public website, made easier due to the default of localhost
by the TestCase. Now you can add all your public facing functional tests.
Creating the Tenant Schema
Creating the public schema in the public facing functional test was straightforward because the server in the background goes to localhost and that is the domain_url of the schema we setup.
Now to makes things as simple as possible, I just realised you can set the tenant schema to use localhost
and everything should work similarly. There are however a few other things that seem to need to be done based on the provided TenantTestCase
which I’ve pasted here:
from django.conf import settings
from django.core.management import call_command
from django.db import connection
from django.test import TestCase
from tenant_schemas.utils import get_public_schema_name, get_tenant_model
ALLOWED_TEST_DOMAIN = '.test.com'
class TenantTestCase(TestCase):
@classmethod
def add_allowed_test_domain(cls):
# ALLOWED_HOSTS is a special setting of Django setup_test_environment so we can't modify it with helpers
if ALLOWED_TEST_DOMAIN not in settings.ALLOWED_HOSTS:
settings.ALLOWED_HOSTS += [ALLOWED_TEST_DOMAIN]
@classmethod
def remove_allowed_test_domain(cls):
if ALLOWED_TEST_DOMAIN in settings.ALLOWED_HOSTS:
settings.ALLOWED_HOSTS.remove(ALLOWED_TEST_DOMAIN)
@classmethod
def setUpClass(cls):
cls.sync_shared()
cls.add_allowed_test_domain()
tenant_domain = 'tenant.test.com'
cls.tenant = get_tenant_model()(domain_url=tenant_domain, schema_name='test')
cls.tenant.save(verbosity=0) # todo: is there any way to get the verbosity from the test command here?
connection.set_tenant(cls.tenant)
@classmethod
def tearDownClass(cls):
connection.set_schema_to_public()
cls.tenant.delete()
cls.remove_allowed_test_domain()
cursor = connection.cursor()
cursor.execute('DROP SCHEMA IF EXISTS test CASCADE')
@classmethod
def sync_shared(cls):
call_command('migrate_schemas',
schema_name=get_public_schema_name(),
interactive=False,
verbosity=0)
class FastTenantTestCase(TenantTestCase):
@classmethod
def setUpClass(cls):
cls.sync_shared()
cls.add_allowed_test_domain()
tenant_domain = 'tenant.test.com'
TenantModel = get_tenant_model()
try:
cls.tenant = TenantModel.objects.get(domain_url=tenant_domain, schema_name='test')
except:
cls.tenant = TenantModel(domain_url=tenant_domain, schema_name='test')
cls.tenant.save(verbosity=0)
connection.set_tenant(cls.tenant)
@classmethod
def tearDownClass(cls):
connection.set_schema_to_public()
cls.remove_allowed_test_domain()
There are quite a few things being done here, one thing that I don’t think will work is setting the domain_url
of the tenant to tenant.test.com
which would require a modification to the host file.
We would need things like set_tenant
. So we need to decide if it is better to inherit from TenantTestCase
and add the functionality of the StaticLiveServer or the other way around. Or perhaps use Multiple Inheritance.
Wait I think I’ve found an example where a Mixin
would be the appropriate choice. So I pretty much copied the TenantTestCase
and extended from StaticLiveServerTestCase
instead of TestCase
.
class StaticLiveServerTenantTestCase(StaticLiveServerTestCase):
However I have a wrapper of the StaticLiveServerTestCase that adds functionality from harry percival’s obey the testing goat book that takes screenshots, waits for elements to appear etc.
Now I don’t want to duplicate these methods just applied to different classes because they extend from different classes.