Django tests don't use test database if alias is not specified in testcases.

Hi everyone! I’ve faced a kind of bug when using django tests and want to share all the information to help to fix or overpass it.

In my case, TestRunner must use only test database, but when I run tests for specific module, testcase made changes in the original database:

python manage.py test --keepdb apps.some_app

But when I run all tests, it uses the test database:

python manage.py test --keepdb

After hours of debugging I found that TestRunner uses the original database, if it’s alias is not specified in testcases that I run. But when I run all tests, in some testcases I have parameter “databases=[‘default’, ‘replica’]”, and everything works fine.

Is it kind of bug, or ‘undeclared feature’?

See all additional information below:

settings.py

DATABASES = {
    'default': env.db('DATABASE_URL', default=''),
    'replica': env.db('DATABASE_REPLICA_URL', default=''),
...
}
DATABASES['replica']['ENGINE'] = 'apps.core.db_backends.open'
DATABASES['default']['ENGINE'] = 'apps.core.db_backends.open'

for db_name in DATABASES:
    DATABASES[db_name]['TEST'] = {
        'SERIALIZE': False,
        'DEPENDENCIES': [],
    }

test case without database declaration

class TestService(TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        cls._service_class = Service

        # NOT OKAY: IT REMOVES DATA IN ORIGINAL DATABASE!
        ObjectType.objects.all().delete()
        ObjectTypeFactory.create_batch(5)
    
    def test_service(self):
        ...

test with declared database attribute

class TesStatistic(TestCase):
    databases = ['replica', 'default']

    @classmethod
    def setUpClass(cls) -> None:

        # OKAY, IT MAKES CHANGES IN TEST DATABASE
        cls.incidents_list: List= StatisticFactory.create_batch(5)
        cls.incidents_to_delete: List = StatisticFactory.create_batch(5)

the way to overpass this bug
Create your own TestRunner and re-define setup_databases method like this:

class KeepDBTestRunner(DiscoverRunner):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.keepdb = True

    def setup_databases(self, **kwargs):
        
        # define aliases here. In original class aliases will be 'None' (from calling 
        # self.get_databases(suite)) and it causes a problem.

        aliases = {'default', 'replica'}  
        return super().setup_databases(aliases=aliases)

@OlegYariga I ran into this same issue on one of my tests, and then realized that the test was using unittest.TestCase and not django.test.TestCase. If you directly test a file using unittest.TestCase it will not do the proper setup and you’ll get that error. If you test a package, though, and one of the test classes is based off django.test.TestCase then it will work fine.

For example, with these test files:

myapp/
  tests/
    test_bad.py   # unittest.TestCase
    test_good.py  # django.test.TestCase

This will fail:

$ ./manage.py test myapp.tests.test_bad --keepdb

This will work:

$ ./manage.py test myapp.tests.test_good --keepdb

And this will also work:

$ ./manage.py test myapp.tests --keepdb