Skip to content

Instantly share code, notes, and snippets.

@GeeWee
Last active February 1, 2023 20:26
Show Gist options
  • Save GeeWee/54b6fd7ad87bcc876781ae02d1e0993d to your computer and use it in GitHub Desktop.
Save GeeWee/54b6fd7ad87bcc876781ae02d1e0993d to your computer and use it in GitHub Desktop.
TEST_TENANT_NAME = "test"
@pytest.fixture(scope="function")
def db(request, django_db_setup, django_db_blocker):
from django.test import TestCase, TransactionTestCase
"""
Django db fixture.
Seeing as this has the same signature, it overrides the fixture in pytest_django.fixtures
It doesn't support quite the same things, such as transactional tests or resetting of sequences.
The way our setup works here, is that due to performance reasons we want to override this, and then
create our public and test tenant, before entering into the `atomic` block that pytest and Django normally
runs tests in - this way we only have to do the heavy work of migrating our schemas once every test run, rather
than every test.
"""
logger.info("Fetching the DB fixture")
if is_django_unittest(request):
return
# Some weird django db_blocker magic
django_db_blocker.unblock()
request.addfinalizer(django_db_blocker.restore)
# Create the public and the test tenant.
# We do this right before the pre_setup so it doesn't
# get axed by the atomic block()
Tenant.objects.get_or_create(schema_name=get_public_schema_name())
_get_or_create_test_tenant()
""" Here we distinguish between a transactional test or not (corresponding to Djangos
TestCase or its TransactionTestCase. Some tests that create/drop tenants can't be run
inside an atomic block, so must be marked as transactional"""
if "transactional" in request.keywords:
test_case = TransactionTestCase(methodName="__init__")
logger.debug("Using transactional test case")
else:
# This performs the rest of the test in an atomic() block which will roll back the changes.
logger.debug("Using regular test case")
test_case = TestCase(methodName="__init__")
test_case._pre_setup()
# Post-teardown function here reverts the atomic blocks to leave the DB in a fresh state.
request.addfinalizer(test_case._post_teardown) # This rolls the atomic block back
@pytest.fixture(scope="function")
def user(run_in_tenant_context) -> User:
from django.contrib.auth import get_user_model
from model_mommy import mommy
return mommy.make(get_user_model(), tenant=_get_or_create_test_tenant())
@pytest.fixture(scope="function")
def api_client(user) -> APIClient:
""" Returns a logged in APIClient from DRF. Creates a user-model as a side-effect."""
logger.info("Fetching api_client fixture")
client = APIClient()
client.force_login(user)
# Run test
return client
@pytest.fixture(scope="function", autouse=True)
def run_in_tenant_context(db, request, caplog):
# Here we create the statics
logger.info("Running in test tenant context")
with tenant_context(_get_or_create_test_tenant()):
# Delete unknown offloading ServiceType from datamigration
ServiceType.objects.all().delete()
# Create statics if needed
if "no_statics" not in request.keywords:
# We don't want to show setup logs normally
with caplog.at_level(logging.WARNING):
logger.info("Populating static tables")
populate_static_database_tables()
yield
def _get_or_create_test_tenant() -> Tenant:
"""
Fixture that gives us a test_tenant if we need it
"""
try:
tenant = Tenant.objects.get(schema_name=TEST_TENANT_NAME)
logger.debug("Using previously created tenant")
return tenant
except Tenant.DoesNotExist:
logger.debug("Creating new test tenant")
tenant = Tenant(schema_name=TEST_TENANT_NAME)
tenant.save(verbosity=0) # This saves the tenant and creates the tenant schema.
return tenant
@GeeWee
Copy link
Author

GeeWee commented Feb 4, 2019

Usage:

def test_company_cannot_be_created_without_location(api_client: APIClient):
    company = mommy.prepare_recipe("companies.company")

    response = api_client.post(company_list_url, data={"cvr": company.cvr, "name": company.name})

    assert_status_code(response, 400)
    res = response.data
    assert res["main_location"][0].code == "required"
    assert Company.objects.count() == 0
    assert MainLocation.objects.count() == 0

@lukik
Copy link

lukik commented Apr 19, 2019

Hi,

Am trying to customize your gist to work in my setup but I keep running into what seems like a basic error but I can't figure out how to fix it.

Am able to create the public tenant. Issue is, at the point of creating an actual tenant, am getting an error: INTERNALERROR> Failed: Database access not allowed, use the "django_db" mark, or the "db" or "transactional_db" fixtures to enable it.

I've tried adding @pytest.mark.django_db to the django_db_setup but that doesn't work.

My setup is like your but in my case instead of having tenant = Tenant(name=TENANT_NAME, schema_name=TENANT_NAME, domain_url='test-tenant.test.com') I am creating the tenant via django-tenant-users which has a provision_tenant function i.e. provision_tenant("EvilCorp", "evilcorp", "[email protected]"). As such, I was not expecting that function it to be flagged for db_access given that your Create Tenant Function is not requiring the db flag?

@GeeWee
Copy link
Author

GeeWee commented Sep 6, 2019

I didn't see your comment. I've updated the gist to the current one I'm using, which is potentially more useful

@nitinnain
Copy link

Wonder why you omitted the import statements from the file? What's ServiceType?

@GeeWee
Copy link
Author

GeeWee commented Jul 28, 2021

Can't really recall why I omitted the import statements anymore.
ServiceType is just a application-specific thing for my app. You can omit that line and it should work just fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment