【问题标题】:Django Unit Testing taking a very long time to create test databaseDjango 单元测试需要很长时间才能创建测试数据库
【发布时间】:2026-02-15 23:25:01
【问题描述】:

一段时间以来,我的单元测试花费的时间比预期的要长。我尝试调试了几次,但都没有成功,因为延迟甚至在我的测试开始运行之前。这影响了我做任何接近测试驱动开发的远程操作的能力(也许我的期望太高了),所以我想看看我是否可以一劳永逸地解决这个问题。

运行测试时,从开始到实际开始测试之间有 70 到 80 秒的延迟。例如,如果我对一个小模块(使用time python manage.py test myapp)运行测试,我会得到

<... bunch of unimportant print messages I print from my settings>

Creating test database for alias 'default'...
......
----------------------------------------------------------------
Ran 6 tests in 2.161s

OK
Destroying test database for alias 'default'...

real    1m21.612s
user    1m17.170s
sys     0m1.400s

1m:21 中大约有 1m18 介于

Creating test database for alias 'default'...

.......

线。换句话说,测试用时不到 3 秒,但数据库初始化似乎用时 1:18 分钟

我有大约 30 个应用程序,大多数都有 1 到 3 个数据库模型,因此这应该可以让您了解项目规模。我使用 SQLite 进行单元测试,并实施了一些建议的改进。我无法发布我的整个设置文件,但很高兴添加所需的任何信息。

我确实使用跑步者

from django.test.runner import DiscoverRunner
from django.conf import settings

class ExcludeAppsTestSuiteRunner(DiscoverRunner):
    """Override the default django 'test' command, exclude from testing
    apps which we know will fail."""

    def run_tests(self, test_labels, extra_tests=None, **kwargs):
        if not test_labels:
            # No appnames specified on the command line, so we run all
            # tests, but remove those which we know are troublesome.
            test_labels = (
                'app1',
                'app2',
                ....
                )
            print ('Testing: ' + str(test_labels))

        return super(ExcludeAppsTestSuiteRunner, self).run_tests(
                test_labels, extra_tests, **kwargs)

在我的设置中:

TEST_RUNNER = 'config.test_runner.ExcludeAppsTestSuiteRunner'

我也尝试过使用django-nosedjango-nose-exclude

我已经阅读了很多关于如何自己加速测试的内容,但没有找到任何关于如何优化或避免数据库初始化的线索。我已经看到了关于尝试不使用数据库进行测试的建议,但我不能或不知道如何完全避免这种情况。

请告诉我

  1. 这是正常的,也是意料之中的
  2. 不出所料(希望能修复或引导该做什么)

再一次,我不需要关于如何加快测试本身的帮助,而是初始化(或开销)。我希望上面的示例花费 10 秒而不是 80 秒。

非常感谢

我使用--verbose 3 运行测试(针对单个应用程序),发现这都与迁移有关:

  Rendering model states... DONE (40.500s)
  Applying authentication.0001_initial... OK (0.005s)
  Applying account.0001_initial... OK (0.022s)
  Applying account.0002_email_max_length... OK (0.016s)
  Applying contenttypes.0001_initial... OK (0.024s)
  Applying contenttypes.0002_remove_content_type_name... OK (0.048s)
  Applying s3video.0001_initial... OK (0.021s)
  Applying s3picture.0001_initial... OK (0.052s)
  ... Many more like this

我压缩了所有迁移,但仍然很慢。

【问题讨论】:

  • 非常有帮助。尤其是verbose 选项。在我们的项目中,有数百个迁移,其中一些显然需要一秒钟才能完成。对于使用 PyCharm (Pro) 的用户,您可以将 --verbose 3(或 -v 3)添加到测试的运行配置中(在“选项:”下)。

标签: python django django-unittest django-nose


【解决方案1】:

解决我的问题的最终解决方案是强制 Django 在测试期间禁用迁移,这可以通过这样的设置来完成

TESTING = 'test' in sys.argv[1:]
if TESTING:
    print('=========================')
    print('In TEST Mode - Disableling Migrations')
    print('=========================')

    class DisableMigrations(object):

        def __contains__(self, item):
            return True

        def __getitem__(self, item):
            return None

    MIGRATION_MODULES = DisableMigrations()

或使用https://pypi.python.org/pypi/django-test-without-migrations

我的整个测试现在大约需要 1 分钟,而一个小应用程序需要 5 秒。

在我的情况下,测试不需要迁移,因为我在迁移时更新测试,并且不使用迁移来添加数据。这并不适合所有人

【讨论】:

  • 太棒了,你一定有很多迁移! :-)
  • 不像我已经压扁了我的所有东西。但是我确实有一个问题(对于另一篇文章),每次我 makemigrations 时 Django-allauth 都会不断创建一个新的迁移。但是有很多关于 Django 1.8 缓慢迁移的帖子。我相信 1.9 修复了一些问题
  • 是的,我在某处读到过这个。为了克服这个问题,我只使用pytest-django,它有一个选项--nomigrations,可以直接从模型创建数据库。显然这是 Django 在 1.6 之前的默认行为。我知道在部署之前运行一次迁移很有用,但不是每次测试运行都运行一次!
  • 不会 --keepdb 做同样的事情吗?
  • 使用 django 2.1 我不得不将 return "notmigrations" 更改为 return None,否则它会抱怨 ModuleNotFoundError: No module named 'notmigrations'
【解决方案2】:

总结

使用pytest

操作

  1. pip install pytest-django
  2. pytest --nomigrations 而不是 ./manage.py test

结果

  • ./manage.py test 花费 2 分 11.86 秒
  • pytest --nomigrations 花费 2.18 秒

提示

  • 您可以在项目根目录中创建一个名为 pytest.ini 的文件,并在此处指定 default command line options 和/或 Django settings

    # content of pytest.ini
    [pytest]
    addopts = --nomigrations
    DJANGO_SETTINGS_MODULE = yourproject.settings
    

    现在您可以使用 pytest 简单地运行测试,并节省您输入的时间。

  • 您可以通过将--reuse-db 添加到默认命令行选项来进一步加快后续测试。

    [pytest]
    addopts = --nomigrations --reuse-db
    

    但是,一旦您的数据库模型发生更改,您必须运行一次pytest --create-dbforce re-creation of the test database

  • 如果你需要在测试过程中启用gevent monkey patching,你可以在你的项目根目录下创建一个名为pytest的文件,内容如下,将执行位转换为它(chmod +x pytest)并运行./pytest for测试而不是pytest:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    # content of pytest
    from gevent import monkey
    
    monkey.patch_all()
    
    import os
    
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "yourproject.settings")
    
    from django.db import connection
    
    connection.allow_thread_sharing = True
    
    import re
    import sys
    
    from pytest import main
    
    if __name__ == '__main__':
        sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
        sys.exit(main())
    

    您可以创建一个test_gevent.py文件来测试gevent猴子补丁是否成功:

    # -*- coding: utf-8 -*-
    # content of test_gevent.py
    import time
    from django.test import TestCase
    from django.db import connection
    import gevent
    
    
    def f(n):
        cur = connection.cursor()
        cur.execute("SELECT SLEEP(%s)", (n,))
        cur.execute("SELECT %s", (n,))
        cur.fetchall()
        connection.close()
    
    
    class GeventTestCase(TestCase):
        longMessage = True
    
        def test_gevent_spawn(self):
            timer = time.time()
            d1, d2, d3 = 1, 2, 3
            t1 = gevent.spawn(f, d1)
            t2 = gevent.spawn(f, d2)
            t3 = gevent.spawn(f, d3)
            gevent.joinall([t1, t2, t3])
            cost = time.time() - timer
            self.assertAlmostEqual(cost, max(d1, d2, d3), delta=1.0,
                                   msg='gevent spawn not working as expected')
    

参考文献

【讨论】:

  • 另外,如果你厌倦了整天看着小灰点,pip install pytest-sugar,突然测试很漂亮!
  • 我尝试了这种方法,但我收到了关于数据库权限的错误。我想我必须用一个装饰器来包装我所有的测试用例才能工作。 pytest-django.readthedocs.io/en/latest/…。但这似乎太麻烦了。
【解决方案3】:

当迁移文件没有变化时使用./manage.py test --keepdb

【讨论】:

  • 这种方法有什么潜在的缺陷吗?
  • 我唯一担心的是:每次测试用例运行后数据库会被清除吗?但除此之外,您在每次迁移时都可以节省大量运行 DDL SQL 查询的时间。当您没有 SSD 时,速度特别慢。如果您有 100 次迁移,它也应该加快速度。这是一个临时修复程序,用于在开发过程中一个接一个地快速运行测试,而无需在每个测试之间等待 5 到 10 分钟。
【解决方案4】:

数据库初始化确实耗时太长...

我有一个项目,它的模型/表数量大致相同(大约 77 个),大约有 350 个测试,总共需要 1 分钟来运行所有内容。在分配了 2 个 CPU 和 2GB 内存的 vagrant 机器上进行开发。我还使用 py.test 和 pytest-xdist 插件来并行运行多个测试。

您可以做的另一件事是告诉 django 重用测试数据库,并且仅在架构更改时重新创建它。您也可以使用 SQLite,以便测试使用内存数据库。两种方法都在这里解释: https://docs.djangoproject.com/en/dev/topics/testing/overview/#the-test-database

编辑:如果上述选项都不起作用,另一种选择是让您的单元测试从 django SimpleTestCase 继承或使用不创建数据库的自定义测试运行器,如中所述这个答案在这里:django unit tests without a db

然后,您可以使用像这样的库(我承认这是我写的)模拟 django 对数据库的调用:https://github.com/stphivos/django-mock-queries

通过这种方式,您可以在本地快速运行单元测试,让 CI 服务器担心运行需要数据库的集成测试,然后再将您的代码合并到某个稳定的 dev/master 分支而不是生产分支。

【讨论】:

  • 感谢您的回答。我刚刚更正了 OP,表明我已经在内存中使用 SQLite。我刚刚尝试了 --keepdb 但似乎没有任何效果,可能是因为我在内存中使用 SQLite。有没有办法使用 SQLite 但使用数据库文件,以便可以保留它?我的整体测试(174 次测试)在 1:18 初始化加上 1min = 2:18min 运行,所以这是可以接受的。在您的情况下,您的初始化显然要小得多。我不认为数据库初始化可以并行运行,所以看起来你的开销为零。是这样吗?
  • 我会说一般来说,数据库初始化会更加 I/O 密集型,并且运行测试更多的内存和 cpu 密集型 - 特别是对于分布式测试。但是您使用的是 SQLite 内存数据库,因此不确定数据库初始化涉及多少 I/O。从来没有在一个项目中拥有过这么多应用程序,不确定每个项目的模型发现开销有多大。你的规格?另外,您是否在运行测试之前创建了大量测试数据?
  • 我们的型号好像差不多,give or take,你的测试比我多,所以我希望能得到你的结果。我不知道您所说的“规格”是什么意思,但不,我没有任何固定装置。所有数据都是作为测试本身的一部分创建的,或者在 setUp() 期间创建
  • 抱歉,我想问你的开发机器是否有限制规格。用解决方案更新了我的答案。
  • 不,我没有尝试过,包括压缩所有尝试过的迁移。我需要尝试模拟库,但无法做到