web-dev-qa-db-ger.com

Django: Dynamisches Erstellen eines Modells nur zum Testen

Ich habe eine Django-App, die ein settings-Attribut in der Form benötigt:

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

Dann hakt ihr post_save-Signal ein, um abhängig von der definierten attributeN ein anderes festes Modell zu aktualisieren.

Ich möchte dieses Verhalten testen und die Tests sollten funktionieren, auch wenn diese App die einzige im Projekt ist (abgesehen von ihren eigenen Abhängigkeiten muss keine weitere Wrapper-App installiert werden). Wie kann ich Modellmodelle nur für die Testdatenbank erstellen und hinzufügen/registrieren/aktivieren? (oder ist es überhaupt möglich?)

Lösungen, die es mir erlauben, Testvorrichtungen zu verwenden, wären großartig.

58
muhuk

Sie können Ihre Tests in einem tests/-Unterverzeichnis der App ablegen (anstelle einer tests.py-Datei) und einen tests/models.py in die Test-Only-Modelle einschließen.

Geben Sie dann ein Testlaufskript ( example ) an, das Ihre tests/ "app" in INSTALLED_APPS enthält. (Dies funktioniert nicht, wenn App-Tests von einem realen Projekt ausgeführt werden, für das die Tests-App nicht in INSTALLED_APPS enthalten ist. Ich finde es jedoch selten nützlich, wiederverwendbare App-Tests von einem Projekt aus auszuführen, und Django 1.6+ ist nicht standardmäßig installiert .)

(NOTE: Die unten beschriebene alternative dynamische Methode funktioniert nur in Django 1.1+, wenn Ihre Testfall-Unterklassen TransactionTestCase - was Ihre Tests erheblich verlangsamt - und in Django 1.7+ nicht mehr funktionieren hier nur aus historischem Interesse, verwenden Sie es nicht.)

Zu Beginn Ihrer Tests (d. H. In einer setUp-Methode oder zu Beginn einer Gruppe von Doctests) können Sie "myapp.tests" dynamisch zur INSTALLED_APPS-Einstellung hinzufügen. Führen Sie dann Folgendes aus:

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

Am Ende Ihrer Tests sollten Sie bereinigen, indem Sie die alte Version von INSTALLED_APPS wiederherstellen und den App-Cache erneut löschen.

Diese Klasse kapselt das Muster ein, sodass der Testcode nicht so sehr verstopft wird.

51
Carl Meyer

@ paluhs Antwort erfordert das Hinzufügen von unerwünschtem Code zu einer Nicht-Testdatei. Nach meiner Erfahrung funktioniert die @ carl-Lösung nicht mit Django.test.TestCase, das zur Verwendung von Fixtures benötigt wird. Wenn Sie Django.test.TestCase verwenden möchten, müssen Sie unbedingt syncdb aufrufen, bevor die Fixtures geladen werden. Dies erfordert das Überschreiben der _pre_setup-Methode (das Einfügen des Codes in die setUp-Methode reicht nicht aus). Ich verwende meine eigene Version von TestCase, mit der ich Apps mit Testmodellen hinzufügen kann. Es ist wie folgt definiert:

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

Diese Lösung funktioniert nur für frühere Versionen von Django (vor 1.7). Sie können Ihre Version einfach überprüfen:

import Django
django.VERSION < (1, 7)

Ursprüngliche Antwort:

Es ist ziemlich seltsam, aber ich arbeite mit sehr einfachen Mustern:

  1. hinzufügen von tests.py zur App, die Sie testen möchten,
  2. in dieser Datei werden nur Testmodelle definiert.
  3. geben Sie unten Ihren Testcode ein (Doctest- oder TestCase-Definition).

Im Folgenden habe ich einen Code eingefügt, der das Article-Modell definiert, das nur für Tests benötigt wird (es existiert in someapp/tests.py, und ich kann es einfach testen mit: ./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>]
"""}

Unit-Tests arbeiten auch mit einer solchen Modelldefinition.

11
paluh

Ich habe meine Lösung geteilt, die ich in meinen Projekten verwende. Vielleicht hilft es jemandem.

pip install Django-fake-model

Zwei einfache Schritte zum Erstellen eines falschen Modells:

1) Modell in einer beliebigen Datei definieren (Normalerweise definiere ich das Modell in einer Testdatei in der Nähe eines Testfalls).

from Django_fake_model import models as f


class MyFakeModel(f.FakeModel):

    name = models.CharField(max_length=100)

2) Fügen Sie dem TestCase oder Dekorator @MyFakeModel.fake_me hinzu, um die Funktion zu testen.

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')

Dieser Dekorateur erstellt vor jedem Test eine Tabelle in Ihrer Datenbank und entfernt die Tabelle nach dem Test.

Sie können auch erstellen / löschen Tabelle manuell: MyFakeModel.create_table()/MyFakeModel.delete_table()

10
Kirill Ermolov

Zitieren aus einer verwandten Antwort :

Wenn Sie möchten, dass Modelle nur zum Testen definiert werden, sollten Sie Django-Ticket # 7835 insbesondere Kommentar # 24 einen Teil davon anzeigen. Ist unten angegeben:

Anscheinend können Sie Modelle einfach direkt in tests.py definieren. Syncdb importiert tests.py nie, sodass diese Modelle nicht mit der normalen - Datenbank synchronisiert werden, sondern mit Testdatenbank und kann in Tests verwendet werden.

9
joeharrie

Ich habe einen Weg für Testmodelle für Django 1.7+ gefunden.

Die Grundidee ist, machen Sie aus Ihrer tests eine App und fügen Sie Ihre tests zu INSTALLED_APPS hinzu.

Hier ist ein Beispiel:

$ 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

Und ich habe verschiedene settings für verschiedene Zwecke (ref: Aufteilen der Einstellungsdatei ), nämlich:

  • settings/default.py: Basiseinstellungsdatei
  • settings/production.py: für die Produktion
  • settings/development.py: für die Entwicklung
  • settings/testing.py: zum testen.

In settings/testing.py können Sie INSTALLED_APPS ändern:

settings/testing.py:

from default import *

DEBUG = True

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

Und vergewissern Sie sich, dass Sie eine korrekte Bezeichnung für Ihre Test-App festgelegt haben, nämlich 

common/tests/apps.py

from Django.apps import AppConfig


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

common/tests/__init__.py, richtige AppConfig einrichten (ref: Django Applications ).

default_app_config = 'common.tests.apps.CommonTestsConfig'

Generieren Sie dann die Db-Migration durch

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

Zum Schluss können Sie Ihren Test mit param --settings=<your_project_name>.settings.testing ausführen. 

Wenn Sie py.test verwenden, können Sie sogar eine pytest.ini-Datei zusammen mit Djangos manage.py ablegen.

py.test

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

Ich entschied mich für einen etwas anderen, wenn auch eher gekoppelten Ansatz, Modelle nur zum Testen dynamisch zu erstellen. 

Ich bewahre alle Tests in einem tests-Unterverzeichnis auf, das in meiner files-App gespeichert ist. Die models.py-Datei im tests-Unterverzeichnis enthält meine Testmodelle. Der gekoppelte Teil kommt hier herein, wo ich meiner settings.py-Datei Folgendes hinzufügen muss:

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

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

Ich habe auch db_table in meinem Testmodell gesetzt, da Django sonst die Tabelle mit dem Namen tests_<model_name> erstellt hätte, was möglicherweise zu einem Konflikt mit anderen Testmodellen in einer anderen App geführt hat. Hier ist mein Testmodell:

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

Hier ist das Muster, das ich dazu benutze. 

Ich habe diese Methode geschrieben, die ich in einer untergeordneten Version von TestCase verwende. Es geht wie folgt:

@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

Dann erstelle ich eine spezielle testspezifische models.py-Datei in etwas wie myapp/tests/models.py, die nicht in INSTALLED_APPS enthalten ist. 

In meiner setUp-Methode rufe ich create_models_from_app ('myapp.tests') auf, und es werden die richtigen Tabellen erstellt.

Das einzige "Gotcha" bei diesem Ansatz ist, dass Sie die Modelle nicht wirklich erstellen möchten, wenn setUp ausgeführt wird, weshalb ich DatabaseError erwische. Ich denke, der Aufruf dieser Methode könnte ganz oben in der Testdatei stehen und würde etwas besser funktionieren. 

4
slacy

Ich kombinierte deine Antworten, besonders bei @ slacy, und habe folgendes getan:

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)

Dabei versuchen Sie nicht, mehr als einmal db -Tabellen zu erstellen, und Sie müssen Ihren INSTALLED_APPS nicht ändern.

3
zVictor

Wenn Sie eine wiederverwendbare Django-App schreiben, erstellen Sie erstellen Sie eine minimale Test-App dafür!

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

fügen Sie sowohl myapp als auch test_myapp zum INSTALLED_APPS hinzu, erstellen Sie Ihre Modelle dort und es ist gut zu gehen!

Ich habe alle diese Antworten sowie das Django-Ticket 7835 durchgelesen und schließlich einen völlig anderen Ansatz gewählt. Ich wollte, dass meine App (irgendwie erweitern queryset.values ​​()) isoliert getestet werden kann. Außerdem enthält mein Paket einige Modelle, und ich wollte eine klare Unterscheidung zwischen Testmodellen und Paketmodellen.

Da wurde mir klar, dass es einfacher war, ein sehr kleines Django-Projekt in das Paket aufzunehmen! Dies ermöglicht auch eine wesentlich sauberere Trennung von Code IMHO:

Dort können Sie Ihre Modelle sauber und ohne Hack definieren, und Sie wissen, dass sie erstellt werden, wenn Sie Ihre Tests von dort aus ausführen!

Wenn Sie keine unabhängige, wiederverwendbare App schreiben, können Sie immer noch folgendermaßen vorgehen: Erstellen Sie eine test_myapp-App und fügen Sie sie nur in einem separaten settings_test_myapp.py zu Ihrem INSTALLED_APPS hinzu!

1
Stefano

Jemand erwähnte bereits Django-Ticket # 7835 , aber es scheint eine neuere Antwort zu geben, die viel versprechender für neuere Versionen von Django aussieht. Speziell # 42 , das eine andere TestRunner vorschlägt:

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