Django 单元测试需要很长时间才能创建测试数据库

Django Unit Testing taking a very long time to create test database

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

当 运行 测试时,在测试开始和实际开始之间有 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:18min

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

我用 运行ner

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

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

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

我有一个项目 models/tables(大约 77),大约有 350 个测试,总计 运行 需要 1 分钟。在分配了 2 个 CPU 和 2GB 内存的流浪机器中进行开发。此外,我将 py.test 与 pytest-xdist 插件一起用于 运行 并行进行多个测试。

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

编辑:如果none以上选项有效,另一种选择是让您的单元测试继承自django SimpleTestCase或使用自定义测试运行ner 不会创建数据库,如此处的答案所述:django unit tests without a db.

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

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

解决我的问题的最终解决方案是强制 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 秒。

就我而言,测试不需要迁移,因为我在迁移时更新测试,并且不使用迁移来添加数据。这并不适用于所有人

总结

使用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 and/or Django settings

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

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

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

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

    但是,一旦你的数据库模型改变了,你必须运行pytest --create-db一次到force re-creation of the test database

  • 如果在测试时需要启用gevent monkey patching,可以在项目根目录下创建一个名为pytest的文件,内容如下,将执行位投给它(chmod +x pytest) 和 运行 ./pytest 用于测试而不是 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 monkey补丁是否成功:

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

参考资料

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