【问题标题】:Django: How to create a model dynamically just for testingDjango:如何动态创建模型仅用于测试
【发布时间】:2010-10-04 21:54:39
【问题描述】:

我有一个 Django 应用程序,它需要一个 settings 属性,格式为:

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

然后根据定义的attributeN 挂钩他们的 post_save 信号以更新一些其他固定模型。

我想测试这种行为,即使这个应用程序是项目中唯一的一个,测试也应该可以工作(除了它自己的依赖项,不需要安装其他包装应用程序)。如何仅为测试数据库创建和附加/注册/激活模拟模型? (或者有可能吗?)

允许我使用测试夹具的解决方案会很棒。

【问题讨论】:

    标签: python django unit-testing django-models mocking


    【解决方案1】:

    您可以将测试放在应用程序的 tests/ 子目录中(而不是 tests.py 文件),并在仅测试模型中包含 tests/models.py

    然后提供一个测试运行脚本 (example),其中在 INSTALLED_APPS 中包含您的 tests/“应用程序”。 (这在从真实项目运行应用程序测试时不起作用,INSTALLED_APPS 中没有测试应用程序,但我很少发现从项目运行可重用应用程序测试很有用,而 Django 1.6+ 没有默认情况下。)

    (注意:下面描述的替代动态方法仅适用于 Django 1.1+,如果您的测试用例子类 TransactionTestCase - 这会显着减慢您的测试速度 - 并且在 Django 1.7 中不再适用+. 仅出于历史兴趣而留在这里;不要使用它。)

    在测试开始时(即在 setUp 方法中,或在一组 doctest 开始时),您可以将 "myapp.tests" 动态添加到 INSTALLED_APPS 设置中,然后执行以下操作:

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

    然后在测试结束时,您应该通过恢复旧版本的 INSTALLED_APPS 并再次清除应用缓存来进行清理。

    This class 封装了该模式,因此它不会使您的测试代码非常混乱。

    【讨论】:

    • 这是一个干净而强大的snipplet(我猜它是你的)。起初创建一个完整的应用程序对于模拟模型来说似乎太多了。但现在我认为从单元测试的角度来看,它最能代表现实世界的使用情况。谢谢。
    • 是的,我不知道什么是最好的,但这对我有用。当您意识到“创建一个完整的应用程序”的真正含义是“创建一个 models.py 文件”时,它似乎就不再那么重要了。
    • 卡尔,感谢您的 sn-p。当我发现这个页面和链接时,我正要写这个。好东西。
    • 这很棒。在我找到这个答案之前,我正要问这个问题。感谢您的帖子
    • 示例测试运行脚本的链接已失效;这是updated link
    【解决方案2】:

    @paluh 的回答需要将不需要的代码添加到非测试文件中,根据我的经验,@carl 的解决方案不适用于使用夹具所需的django.test.TestCase。如果你想使用django.test.TestCase,你需要确保在加载夹具之前调用syncdb。这需要覆盖_pre_setup 方法(将代码放在setUp 方法中是不够的)。我使用我自己的TestCase 版本,它可以让我添加带有测试模型的应用程序。定义如下:

    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
    

    【讨论】:

    • 为了让它与 South 一起工作,我必须将 migrate=False 传递给 call_command。
    • 如果您已将 settings.INSTALLED_APPS 定义为元组(如 django 文档中建议的那样),您首先必须将其转换为列表。否则它工作正常。
    【解决方案3】:

    我分享了我在项目中使用的solution。也许它可以帮助某人。

    pip install django-fake-model

    创建假模型的两个简单步骤:

    1) 在任何文件中定义模型(我通常在测试用例附近的测试文件中定义模型)

    from django_fake_model import models as f
    
    
    class MyFakeModel(f.FakeModel):
    
        name = models.CharField(max_length=100)
    

    2) 将装饰器 @MyFakeModel.fake_me 添加到您的 TestCase 或测试函数中。

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

    此装饰器在每次测试前在您的数据库中创建表,并在测试后删除该表。

    您也可以手动创建/删除表:MyFakeModel.create_table() / MyFakeModel.delete_table()

    【讨论】:

      【解决方案4】:

      此解决方案仅适用于早期版本的 django1.7 之前)。您可以轻松检查您的版本:

      import django
      django.VERSION < (1, 7)
      

      原始回复:

      这很奇怪,但我的工作方式非常简单:

      1. 将 tests.py 添加到您要测试的应用中,
      2. 在这个文件中只定义测试模型,
      3. 在下面放置您的测试代码(doctest 或 TestCase 定义),

      下面我放了一些代码,它定义了仅用于测试的 Article 模型(它存在于 someapp/tests.py 中,我可以使用以下命令对其进行测试:./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>]
      """}
      

      单元测试也适用于此类模型定义。

      【讨论】:

      • 这很棒 - 工作正常(我使用的是 django 1.2.1),这对我来说就像是“正确”的方式。测试模型应作为此应用程序测试的一部分存在。
      • 更新 - 这不适用于灯具,但您可以通过覆盖 _pre_setup 手动调用 syndb(通过 call_command),如 Conley 对此问题的回答中所述
      【解决方案5】:

      我已经找到了一种用于 django 1.7+ 的仅测试模型的方法。

      基本思路是,将您的tests 制作成一个应用,并将您的tests 添加到INSTALLED_APPS

      这是一个例子:

      $ 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
      

      而且我有不同的settings用于不同的目的(参考:splitting up the settings file),即:

      • settings/default.py: 基本设置文件
      • settings/production.py:用于生产
      • settings/development.py: 用于开发
      • settings/testing.py:用于测试。

      而在settings/testing.py,你可以修改INSTALLED_APPS

      settings/testing.py:

      from default import *
      
      DEBUG = True
      
      INSTALLED_APPS += ['common', 'common.tests']
      

      并确保您为测试应用设置了正确的标签,即,

      common/tests/apps.py

      from django.apps import AppConfig
      
      
      class CommonTestsConfig(AppConfig):
          name = 'common.tests'
          label = 'common_tests'
      

      common/tests/__init__.py,设置正确的AppConfig(参考:Django Applications)。

      default_app_config = 'common.tests.apps.CommonTestsConfig'
      

      然后,通过

      生成db迁移
      python manage.py makemigrations --settings=<your_project_name>.settings.testing tests
      

      最后,您可以使用参数--settings=&lt;your_project_name&gt;.settings.testing 运行测试。

      如果您使用 py.test,您甚至可以将 pytest.ini 文件与 django 的 manage.py 一起删除。

      py.test

      [pytest]
      DJANGO_SETTINGS_MODULE=kungfu.settings.testing
      

      【讨论】:

      • 不错的方法,似乎是最近 Django 版本中唯一可行的解​​决方案。但值得一提的是 DEBUG=False 用于测试,无论您的 settings.py
      • 你为什么喜欢DEBUG=False in testing.py
      • 不确定我说得对。这不是我的愿望,这是默认的behavior of Django
      • 目前唯一适用于 Django 1.10 的解决方案,谢谢!
      【解决方案6】:

      我选择了一种稍微不同但更耦合的方法来动态创建模型,仅用于测试。

      我将所有测试保存在 tests 子目录中,该子目录位于我的 files 应用程序中。 tests 子目录中的 models.py 文件包含我的仅测试模型。耦合部分在这里,我需要将以下内容添加到我的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',]
      

      我还在我的测试模型中设置了 db_table,因为否则 Django 会创建名为 tests_&lt;model_name&gt; 的表,这可能会导致与另一个应用程序中的其他测试模型发生冲突。这是我的测试模型:

      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'
      

      【讨论】:

      • 这适用于项目,但可能不适用于应用程序。干净的方法。
      • 确实如此。我在想,如果 Django 提供了在应用程序中设置文件的能力,那么这将工作而无需进行项目级别的修改。
      • 嗯,很多应用程序都会考虑项目设置文件。还有这样的选项:github.com/jaredly/django-appsettings
      • 你用的是哪个版本的Django?
      【解决方案7】:

      引用a related answer:

      如果您只想为测试定义模型,那么您应该查看 Django ticket #7835 特别是 comment #24 其中一部分 如下:

      显然,您可以直接在您的 tests.py 中简单地定义模型。 Syncdb 从不导入 tests.py,因此这些模型不会同步到 正常的数据库,但它们会同步到测试数据库,并且可以 用于测试。

      【讨论】:

      • 这似乎在 Django 1.7+ 中变得不那么可靠了,大概是因为处理迁移的方式。
      • @Sarah:你能详细说明一下吗?
      • Django 1.7+ 没有“syncdb”。我调查已经至少一年了,但是如果我没记错的话, AppConfig.ready() 仅在数据库构建完成并且所有迁移运行后才被调用,并且测试模块甚至在 AppConfig.ready( )。您可能可以使用自定义测试运行程序、settings.py 或 AppConfig 破解某些东西,但我无法让 put-the-models-in-the-tests 的明显变体工作。如果有人有这个工作的 Django 1.7+ 示例,我很高兴看到它。
      【解决方案8】:

      这是我用来执行此操作的模式。

      我已经编写了这个方法,用于 TestCase 的子类版本。如下:

      @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
      

      然后,我在类似myapp/tests/models.py 的文件中创建了一个特殊的特定于测试的models.py 文件,该文件不包含在INSTALLED_APPS 中。

      在我的 setUp 方法中,我调用 create_models_from_app('myapp.tests') 并创建适当的表。

      使用这种方法的唯一“问题”是您真的不想在 setUp 运行时创建模型,这就是我捕获 DatabaseError 的原因。我想这个方法的调用可以放在测试文件的顶部,这样会更好一些。

      【讨论】:

      • 这个记录器是从哪里导入的?我遇到了这个问题:NameError: global name 'logger' is not defined
      • import logging; logger = logging.getLogger(__name__)
      【解决方案9】:

      结合你的答案,特别是@slacy's,我这样做了:

      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)
      

      有了这个,你不会尝试多次创建数据库表,也不需要更改你的 INSTALLED_APPS。

      【讨论】:

        【解决方案10】:

        如果你正在编写一个可重用的 django-app,为它创建一个最小的测试专用应用

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

        myapptest_myapp 添加到INSTALLED_APPS,在那里创建您的模型,一切顺利!

        我已经阅读了所有这些答案以及 django 票 7835,最后我选择了一种完全不同的方法。 我希望我的应用程序(以某种方式扩展 queryset.values() )能够被单独测试;另外,我的包确实包含一些模型,我希望在测试模型和包模型之间有一个清晰的区别。

        那时我意识到在包中添加一个非常小的 django 项目更容易! 这也允许更清晰地分离代码恕我直言:

        在那里你可以干净利落地定义你的模型,并且你知道当你从那里运行测试时它们会被创建!

        如果您不编写独立的、可重用的应用程序,您仍然可以这样:创建一个test_myapp 应用程序,并将其添加到您的 INSTALLED_APPS 中,仅在单独的 settings_test_myapp.py 中!

        【讨论】:

          【解决方案11】:

          有人已经提到了Django ticket #7835,但似乎有一个更新的回复,对于更新的 Django 版本看起来更有希望。具体#42,它提出了不同的TestRunner

          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)
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2020-03-18
            • 1970-01-01
            • 1970-01-01
            • 2012-12-31
            • 2012-07-28
            • 2015-01-22
            • 2021-09-24
            • 1970-01-01
            相关资源
            最近更新 更多