7

Under my Django project there are a few apps and all of them have unit tests. One of them that I'm working right now is supposed to be included only in dev/stage environments, so I'm enabling it using a environment variable.

When this variable is present it is added to INSTALLED_APPS and it is working just fine, the problem is that Django is executing the tests for this app even when it is not in INSTALLED_APPS, and it fails with the following message:

ImportError: Failed to import test module: debug.tests.unit.test_services`

...(traceback information)...

RuntimeError: Model class debug.models.Email doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS.

When I define the app_label in the class Meta of models in this app the error is different, it says it can't find a table, I assume that this is because the app is not in INSTALLED_APPS, so it's migrations are not executed.

OperationalError: no such table: debug_email

I'm not sure why Django executes the tests for all apps, but not it's migrations.

Am I missing something from Django configuration for tests?

Community
  • 1
  • 1
Gabriel
  • 71
  • 1
  • 2
  • How do you run the tests? What command do you issue to run the tests? Do you do something like: `python manage.py test app_label` or maybe `python manage.py test path/to/tests`? – cezar Mar 06 '18 at 08:23
  • I'm using `python manage.py test` – Gabriel Mar 06 '18 at 11:03

3 Answers3

5

https://docs.python.org/3/library/unittest.html#unittest.TestLoader.discover says:

If load_tests exists then discovery does not recurse into the package, load_tests is responsible for loading all tests in the package.

So in the lowest __init__.py in your app which you don't always want run:

from django.apps import apps

def load_tests(loader, tests, pattern):
    from django.conf import settings

    if apps.is_installed("your_dev_app"):
        # Actually load the tests - thanks to @barney-szabolcs
        return loader.discover(start_dir=dirname(abspath(__file__)), pattern=pattern)
Dave Lawrence
  • 1,226
  • 9
  • 8
  • 1
    This is the correct answer to OP's original request. Though the contents of the function can be a lot simpler: just a simple `pass` is enough to say "Do not load any tests from this app". – coredumperror Jun 23 '20 at 21:18
  • Actually this makes the tests of the target app to never run. @barney-szabolcs answer handles the case where we want to run the tests if the app is installed. – fabiocapsouza Feb 25 '22 at 19:50
  • Hi yes I made it not run (as he asked), but left out the code to make it run in the other case - have copied Barney's code into my example – Dave Lawrence Mar 17 '22 at 00:51
5

You need to return the discovered tests in load_tests.

So, adding to @DaveLawrence's answer, the complete code is:

# your_dev_app/__init__.py

from django.apps import apps
from os.path import dirname, abspath


def load_tests(loader, tests, pattern):
    """
    loads tests for your_dev_app if it is installed.
    """
    from django.conf import settings
    if apps.is_installed("your_dev_app"):
        return loader.discover(start_dir=dirname(abspath(__file__)), pattern=pattern)
Barney Szabolcs
  • 11,846
  • 12
  • 66
  • 91
2

When you run:

python manage.py test

the command will look per default recursive for all files with the pattern test*.py in the working directory. It isn't affected by INSTALLED_APPS in settings.py.

You can specify a certain app to test it:

python manage.py test app_label

or specify a path:

python manage.py test myapp/tests

If you want to exclude some tests you can tag them and use the option --exclude-tag.

Run python manage.py test --help to get information on all options.

The official documentation gives a lot of information on the different possibilities how to run the tests.

EDIT:

If you have apps that are required only in the development environment, but not in the production, you could split your settings.py. One possible solution would be to outsource all development settings into a file local_settings.py and exclude it from versioning or from the production branch, i.e. don't push it in the production environment.

local_settings.py

DEBUG = True 
INSTALLED_APPS += (
    # Django Debug Toolbar would be for example
    # used only in development
    'debug_toolbar',
    'your dev app',
)

settings.py

try:
    from .local_settings import *
except ImportError:
    pass
cezar
  • 11,616
  • 6
  • 48
  • 84
  • Thanks, that is what I thought. I just don't understand why Django is looking for all those files if it doesn't execute their migrations, it would be good to have a configuration to define what I want to test. – Gabriel Mar 06 '18 at 13:35
  • My solution for now is to add the app to the INSTALLED_APPS and then hide the feature in it's code (urls.py and so on) – Gabriel Mar 06 '18 at 13:36
  • @Gabriel Django doesn't execute any migrations automatically. You have to do it explicitly with `python manage.py migrate`. Again, `INSTALLED_APPS` has nothing to do with tests. However the tests rely on the database settings and on the migrations. This means that if you want to test the app, you have to create migrations for it, and to create migrations you have to put in the `INSTALLED_APPS`. Your approach with `urls.py` is good, you can remove/comment the entry including the app's `urls.py` in the project's `urls.py`. – cezar Mar 06 '18 at 13:40
  • If you have eventually split your settings into production and development, for example using `local_settings.py`, you could do something like `if DEBUG: INSTALLED_APPS += ('your dev/stage app',)`. This is in my opinion a cleaner approach. – cezar Mar 06 '18 at 13:52
  • For me it is weird that Django will try to tests for all projects but not it's migrations. However I followed the approach of hiding the routes on `urls.py` when I don't want them, instead of not including the app. Thanks a lot for your comments :) – Gabriel Mar 14 '18 at 22:52