web-dev-qa-db-fra.com

Django: Comment créer un modèle dynamiquement juste pour le tester

J'ai une application Django qui nécessite un attribut settings sous la forme de:

RELATED_MODELS = ('appname1.modelname1.attribute1',
                  'appname1.modelname2.attribute2', 
                  'appname2.modelname3.attribute3', ...)

Accroche ensuite son signal post_save pour mettre à jour un autre modèle fixe en fonction de la variable attributeN définie.

Je voudrais tester ce comportement et les tests devraient fonctionner même si cette application est la seule du projet (à l'exception de ses propres dépendances, aucune autre application d'emballage ne doit être installée). Comment créer et attacher/enregistrer/activer des modèles simulés uniquement pour la base de données de test? (ou est-ce possible?)

Des solutions qui me permettraient d'utiliser des appareils de test seraient formidables.

58
muhuk

Vous pouvez placer vos tests dans un sous-répertoire tests/ de l'application (plutôt que dans un fichier tests.py) et inclure un tests/models.py avec les modèles testés uniquement.

Ensuite, fournissez un script de test ( exemple ) qui inclut votre tests/ "app" dans INSTALLED_APPS. (Cela ne fonctionne pas lors de l'exécution de tests d'application à partir d'un projet réel, qui ne contiendra pas l'application de test dans INSTALLED_APPS, mais je trouve rarement utile d'exécuter des tests d'application réutilisables à partir d'un projet, et Django 1.6+ ne fonctionne pas par défaut. .)

(NOTE: la méthode dynamique alternative décrite ci-dessous ne fonctionne dans Django 1.1+ si vos sous-classes de cas de test TransactionTestCase - ce qui ralentit considérablement vos tests - et ne fonctionne plus du tout dans Django 1.7+. À gauche ici uniquement pour votre intérêt historique, ne l'utilisez pas.)

Au début de vos tests (c'est-à-dire dans une méthode setUp ou au début d'un ensemble de doctests), vous pouvez ajouter dynamiquement "myapp.tests" au paramètre INSTALLED_APPS, puis procédez comme suit:

from Django.core.management import call_command
from Django.db.models import loading
loading.cache.loaded = False
call_command('syncdb', verbosity=0)

Ensuite, à la fin de vos tests, vous devriez nettoyer en restaurant l'ancienne version de INSTALLED_APPS et en effaçant à nouveau le cache de l'application.

Cette classe encapsule le motif pour ne pas encombrer votre code de test autant.

51
Carl Meyer

La réponse de @ paluh nécessite l'ajout de code indésirable dans un fichier non-test et, d'après mon expérience, la solution de @ carl ne fonctionne pas avec Django.test.TestCase, nécessaire à l'utilisation de fixtures. Si vous souhaitez utiliser Django.test.TestCase, vous devez vous assurer d’appeler syncdb avant de charger les projecteurs. Cela nécessite de remplacer la méthode _pre_setup (l'insertion du code dans la méthode setUp n'est pas suffisante). J'utilise ma propre version de TestCase qui me permet d'ajouter des applications avec des modèles de test. Il est défini comme suit:

from Django.conf import settings
from Django.core.management import call_command
from Django.db.models import loading
from Django import test

class TestCase(test.TestCase):
    apps = ()

    def _pre_setup(self):
        # Add the models to the db.
        self._original_installed_apps = list(settings.INSTALLED_APPS)
        for app in self.apps:
            settings.INSTALLED_APPS.append(app)
        loading.cache.loaded = False
        call_command('syncdb', interactive=False, verbosity=0)
        # Call the original method that does the fixtures etc.
        super(TestCase, self)._pre_setup()

    def _post_teardown(self):
        # Call the original method.
        super(TestCase, self)._post_teardown()
        # Restore the settings.
        settings.INSTALLED_APPS = self._original_installed_apps
        loading.cache.loaded = False
18
Conley Owens

Cette solution ne fonctionne que pour les versions antérieures de Django (avant 1.7). Vous pouvez vérifier votre version facilement:

import Django
django.VERSION < (1, 7)

Réponse originale:

C'est assez étrange, mais me forme fonctionne très simple modèle:

  1. ajoutez tests.py à l'application que vous allez tester,
  2. dans ce fichier, définissez simplement les modèles de test,
  3. indiquez ci-dessous votre code de test (définition de doctest ou TestCase),

Ci-dessous, j'ai mis un code qui définit le modèle d'article qui n'est nécessaire que pour les tests (il existe dans someapp/tests.py et je peux le tester avec juste: ./manage.py test someapp ):

class Article(models.Model):
    title = models.CharField(max_length=128)
    description = models.TextField()
    document = DocumentTextField(template=lambda i: i.description)

    def __unicode__(self):
        return self.title

__test__ = {"doctest": """
#smuggling model for tests
>>> from .tests import Article

#testing data
>>> by_two = Article.objects.create(title="divisible by two", description="two four six eight")
>>> by_three = Article.objects.create(title="divisible by three", description="three six nine")
>>> by_four = Article.objects.create(title="divisible by four", description="four four eight")

>>> Article.objects.all().search(document='four')
[<Article: divisible by two>, <Article: divisible by four>]
>>> Article.objects.all().search(document='three')
[<Article: divisible by three>]
"""}

Les tests unitaires fonctionnent également avec une telle définition de modèle.

11
paluh

J'ai partagé ma solution que j'utilise dans mes projets. Peut-être que ça aide quelqu'un.

pip install Django-fake-model

Deux étapes simples pour créer un faux modèle:

1) Définir un modèle dans n’importe quel fichier (je définis habituellement un modèle dans un fichier de test à proximité d’un scénario de test)

from Django_fake_model import models as f


class MyFakeModel(f.FakeModel):

    name = models.CharField(max_length=100)

2) Ajoutez le décorateur @MyFakeModel.fake_me à votre TestCase ou à la fonction de test.

class MyTest(TestCase):

    @MyFakeModel.fake_me
    def test_create_model(self):
        MyFakeModel.objects.create(name='123')
        model = MyFakeModel.objects.get(name='123')
        self.assertEqual(model.name, '123')

Ce décorateur crée une table dans votre base de données avant chaque test et supprime la table après le test.

Aussi, vous pouvez créer / supprimer table manuellement: MyFakeModel.create_table()/MyFakeModel.delete_table()

10
Kirill Ermolov

Citant de une réponse connexe :

Si vous souhaitez que les modèles ne soient définis que pour les tests, vous devez vérifier Ticket Django n ° 7835 en particulier commentaire n ° 24 dont une partie de Est donnée ci-dessous:

Apparemment, vous pouvez simplement définir des modèles directement dans votre tests.py. Syncdb n’importe jamais tests.py, ces modèles ne seront donc pas synchronisés avec la base de données Normale, mais ils seront synchronisés avec la base de données de test, et peut être utilisé dans les tests.

9
joeharrie

J'ai trouvé un moyen de tester uniquement des modèles pour Django 1.7+.

L'idée de base est de faire de votre tests une application et d'ajouter votre tests à INSTALLED_APPS.

Voici un exemple:

$ ls common
__init__.py   admin.py      apps.py       fixtures      models.py     pagination.py tests         validators.py views.py

$ ls common/tests
__init__.py        apps.py            models.py          serializers.py     test_filter.py     test_pagination.py test_validators.py views.py

Et j’ai différents settings à des fins différentes (ref: scinder le fichier de paramètres ), à savoir:

  • settings/default.py: fichier de paramètres de base
  • settings/production.py: pour la production
  • settings/development.py: pour le développement
  • settings/testing.py: pour tester.

Et dans settings/testing.py, vous pouvez modifier INSTALLED_APPS:

settings/testing.py:

from default import *

DEBUG = True

INSTALLED_APPS += ['common', 'common.tests']

Et assurez-vous que vous avez défini une étiquette appropriée pour votre application de test, à savoir: 

common/tests/apps.py

from Django.apps import AppConfig


class CommonTestsConfig(AppConfig):
    name = 'common.tests'
    label = 'common_tests'

common/tests/__init__.py, configurez AppConfig (réf: Django Applications ).

default_app_config = 'common.tests.apps.CommonTestsConfig'

Ensuite, générez la migration de la base de données par

python manage.py makemigrations --settings=<your_project_name>.settings.testing tests

Enfin, vous pouvez exécuter votre test avec param --settings=<your_project_name>.settings.testing

Si vous utilisez py.test, vous pouvez même supprimer un fichier pytest.ini avec le manage.py de Django.

py.test

[pytest]
Django_SETTINGS_MODULE=kungfu.settings.testing
9
Xiao Hanyu

J'ai choisi une approche légèrement différente, bien que davantage couplée, de création dynamique de modèles uniquement pour les tests. 

Je conserve tous mes tests dans un sous-répertoire tests qui réside dans mon application files. Le fichier models.py du sous-répertoire tests contient mes modèles uniquement test. La partie couplée vient ici, où je dois ajouter ce qui suit à mon fichier settings.py:

# check if we are testing right now
TESTING = 'test' in sys.argv

if TESTING:
    # add test packages that have models
    INSTALLED_APPS += ['files.tests',]

J'ai également défini db_table dans mon modèle de test, car sinon Django aurait créé la table avec le nom tests_<model_name>, ce qui pourrait avoir entraîné un conflit avec d'autres modèles de test dans une autre application. Voici mon mon modèle de test:

class Recipe(models.Model):

    '''Test-only model to test out thumbnail registration.'''

    dish_image = models.ImageField(upload_to='recipes/')

    class Meta:
        db_table = 'files_tests_recipe'
9
Jashugan

Voici le modèle que j'utilise pour faire cela. 

J'ai écrit cette méthode que j'utilise sur une version sous-classée de TestCase. Cela va comme suit:

@classmethod
def create_models_from_app(cls, app_name):
    """
    Manually create Models (used only for testing) from the specified string app name.
    Models are loaded from the module "<app_name>.models"
    """
    from Django.db import connection, DatabaseError
    from Django.db.models.loading import load_app

    app = load_app(app_name)
    from Django.core.management import sql
    from Django.core.management.color import no_style
    sql = sql.sql_create(app, no_style(), connection)
    cursor = connection.cursor()
    for statement in sql:
        try:
            cursor.execute(statement)
        except DatabaseError, excn:
            logger.debug(excn.message)
            pass

Ensuite, je crée un fichier models.py spécifique au test, quelque chose comme myapp/tests/models.py, qui n’est pas inclus dans INSTALLED_APPS. 

Dans ma méthode setUp, j'appelle create_models_from_app ('myapp.tests') et crée les tables appropriées.

Le seul "obtenu" avec cette approche est que vous ne voulez pas vraiment créer les modèles à chaque fois que setUp s'exécute, c'est pourquoi j'attrape DatabaseError. Je suppose que l’appel à cette méthode pourrait aller en haut du fichier de test et que cela fonctionnerait un peu mieux. 

4
slacy

En combinant vos réponses, en particulier @ slacy, j'ai fait ceci:

class TestCase(test.TestCase):
    initiated = False

    @classmethod
    def setUpClass(cls, *args, **kwargs):
        if not TestCase.initiated:
            TestCase.create_models_from_app('myapp.tests')
            TestCase.initiated = True

        super(TestCase, cls).setUpClass(*args, **kwargs)

    @classmethod
    def create_models_from_app(cls, app_name):
        """
        Manually create Models (used only for testing) from the specified string app name.
        Models are loaded from the module "<app_name>.models"
        """
        from Django.db import connection, DatabaseError
        from Django.db.models.loading import load_app

        app = load_app(app_name)
        from Django.core.management import sql
        from Django.core.management.color import no_style
        sql = sql.sql_create(app, no_style(), connection)
        cursor = connection.cursor()
        for statement in sql:
            try:
                cursor.execute(statement)
            except DatabaseError, excn:
                logger.debug(excn.message)

Avec cela, vous n'essayez pas de créer des tables de base de données plus d'une fois, et vous n'avez pas besoin de changer votre INSTALLED_APPS.

3
zVictor

Si vous écrivez une application Django réutilisable, créez-lui une application minimale dédiée au test !

$ Django-admin.py startproject test_myapp_project
$ Django-admin.py startapp test_myapp

ajoutez à la fois myapp et test_myapp au INSTALLED_APPS, créez vos modèles ici et c'est bon!

J'ai passé en revue toutes ces réponses ainsi que le billet Django 7835 , et j'ai finalement opté pour une approche totalement différente. Je voulais que mon application (qui étend quelque peu queryset.values ​​()) puisse être testée de manière isolée; de plus, mon paquet inclut certains modèles et je voulais une distinction nette entre les modèles de test et ceux du paquet.

C'est alors que j'ai réalisé qu'il était plus facile d'ajouter un très petit projet Django dans le paquet! Cela permet également une séparation beaucoup plus nette du code IMHO:

Là-bas, vous pouvez définir vos modèles proprement et sans aucun piratage. Vous savez qu'ils seront créés lors de l'exécution de vos tests!

Si vous n'écrivez pas une application indépendante réutilisable, vous pouvez toujours procéder ainsi: créez une application test_myapp et ajoutez-la à votre INSTALLED_APPS uniquement dans un settings_test_myapp.py séparé!

1
Stefano

Quelqu'un a déjà mentionné le billet Django # 7835 , mais il semble y avoir une réponse plus récente qui semble beaucoup plus prometteuse pour les versions plus récentes de Django. Spécifiquement # 42 , qui propose une TestRunner différente:

from importlib.util import find_spec
import unittest

from Django.apps import apps
from Django.conf import settings
from Django.test.runner import DiscoverRunner


class TestLoader(unittest.TestLoader):
    """ Loader that reports all successful loads to a runner """
    def __init__(self, *args, runner, **kwargs):
        self.runner = runner
        super().__init__(*args, **kwargs)

    def loadTestsFromModule(self, module, pattern=None):
        suite = super().loadTestsFromModule(module, pattern)
        if suite.countTestCases():
            self.runner.register_test_module(module)
        return suite


class RunnerWithTestModels(DiscoverRunner):
    """ Test Runner that will add any test packages with a 'models' module to INSTALLED_APPS.
        Allows test only models to be defined within any package that contains tests.
        All test models should be set with app_label = 'tests'
    """
    def __init__(self, *args, **kwargs):
        self.test_packages = set()
        self.test_loader = TestLoader(runner=self)
        super().__init__(*args, **kwargs)

    def register_test_module(self, module):
        self.test_packages.add(module.__package__)

    def setup_databases(self, **kwargs):
        # Look for test models
        test_apps = set()
        for package in self.test_packages:
            if find_spec('.models', package):
                test_apps.add(package)
        # Add test apps with models to INSTALLED_APPS that aren't already there
        new_installed = settings.INSTALLED_APPS + Tuple(ta for ta in test_apps if ta not in settings.INSTALLED_APPS)
        apps.set_installed_apps(new_installed)
        return super().setup_databases(**kwargs)
0
André Fratelli