单元测试 — Django 文档

来自菜鸟教程
Django/docs/3.1.x/internals/contributing/writing-code/unit-tests
跳转至:导航、​搜索

单元测试

Django 带有自己的测试套件,位于代码库的 tests 目录中。 我们的政策是确保所有测试始终通过。

我们感谢对测试套件的所有贡献!

Django 测试都使用 Django 附带的测试基础设施来测试应用程序。 有关如何编写新测试的说明,请参阅 编写和运行测试

运行单元测试

快速入门

首先, 在 GitHub 上 fork Django。

其次,创建并激活虚拟环境。 如果您不熟悉如何做到这一点,请阅读我们的 贡献教程

接下来,克隆你的 fork,安装一些需求,然后运行测试:

安装这些要求可能需要一些您的计算机尚未安装的操作系统软件包。 您通常可以通过在 Web 上搜索错误消息的最后一行来确定要安装哪个包。 如果需要,请尝试将您的操作系统添加到搜索查询中。

如果您在安装要求时遇到问题,可以跳过该步骤。 有关安装可选测试依赖项的详细信息,请参阅 运行所有测试 。 如果您没有安装可选的依赖项,则需要它的测试将被跳过。

运行测试需要一个 Django 设置模块来定义要使用的数据库。 为了帮助您入门,Django 提供并使用了一个使用 SQLite 数据库的示例设置模块。 请参阅 使用另一个设置模块 以了解如何使用不同的设置模块以不同的数据库运行测试。

有问题? 有关一些常见问题,请参阅 故障排除


使用 tox 运行测试

Tox 是一个在不同的虚拟环境中运行测试的工具。 Django 包含一个基本的 tox.ini,它会自动执行我们的构建服务器对拉取请求执行的一些检查。 要运行单元测试和其他检查(例如 导入排序文档拼写检查器代码格式 ),请安装并运行 [ X173X] 命令来自 Django 源代码树中的任何位置:

默认情况下,tox 使用 SQLite、flake8isort 和文档拼写检查器的捆绑测试设置文件运行测试套件。 除了本文档中其他地方提到的系统依赖项之外,命令 python3 必须在您的路径上并链接到适当的 Python 版本。 可以看到默认环境列表如下:

测试其他 Python 版本和数据库后端

除了默认环境,tox 还支持为其他版本的 Python 和其他数据库后端运行单元测试。 由于 Django 的测试套件没有为 SQLite 以外的数据库后端捆绑设置文件,但是,您必须 创建并提供您自己的测试设置 。 例如,要使用 PostgreSQL 在 Python 3.7 上运行测试:

此命令设置 Python 3.7 虚拟环境,安装 Django 的测试套件依赖项(包括那些用于 PostgreSQL 的依赖项),并使用提供的参数(在本例中为 --settings=my_postgres_settings)调用 runtests.py

本文档的其余部分显示了在没有 tox 的情况下运行测试的命令,但是,任何传递给 runtests.py 的选项也可以通过在参数列表前加上 --,如上。

Tox 还遵守 DJANGO_SETTINGS_MODULE 环境变量(如果已设置)。 例如,以下等效于上面的命令:

$ DJANGO_SETTINGS_MODULE=my_postgres_settings tox -e py35-postgres

Windows 用户应该使用:

...\> set DJANGO_SETTINGS_MODULE=my_postgres_settings
...\> tox -e py35-postgres

运行 JavaScript 测试

Django 包含一组 JavaScript 单元测试 ,用于某些 contrib 应用程序中的函数。 默认情况下,JavaScript 测试不使用 tox 运行,因为它们需要安装 Node.js,并且对于大多数补丁来说不是必需的。 要使用 tox 运行 JavaScript 测试:

此命令运行 npm install 以确保测试要求是最新的,然后运行 npm test


使用 django-docker-box 运行测试

django-docker-box 允许你在所有支持的数据库和 python 版本中运行 Django 的测试套件。 安装和使用说明见django-docker-box项目页面。


使用另一个 settings 模块

包含的设置模块 (tests/test_sqlite.py) 允许您使用 SQLite 运行测试套件。 如果要使用不同的数据库运行测试,则需要定义自己的设置文件。 某些测试,例如针对 contrib.postgres 的测试,特定于特定的数据库后端,如果使用不同的后端运行,则会被跳过。

要使用不同的设置运行测试,请确保模块在您的 PYTHONPATH 上,并通过 --settings 的模块。

任何测试设置模块中的 :setting:`DATABASES` 设置都需要定义两个数据库:

  • default 数据库。 此数据库应使用您要用于主要测试的后端。
  • 别名为 other 的数据库。 other 数据库用于测试查询是否可以定向到不同的数据库。 此数据库应使用与 default 相同的后端,并且必须具有不同的名称。

如果您使用的后端不是 SQLite,则需要为每个数据库提供其他详细信息:

  • :setting:`USER` 选项需要为数据库指定一个现有的用户帐户。 该用户需要执行 CREATE DATABASE 的权限,以便可以创建测试数据库。
  • :setting:`PASSWORD` 选项需要提供已经指定的 :setting:`USER` 的密码。

测试数据库通过在 :setting:`DATABASES` 中定义的数据库的 :setting:`NAME` 设置的值前面加上 test_ 来获取它们的名称。 测试完成后,这些测试数据库将被删除。

您还需要确保您的数据库使用 UTF-8 作为默认字符集。 如果您的数据库服务器不使用 UTF-8 作为默认字符集,您将需要包含一个值 :设置:`字符集 ` 在适用数据库的测试设置字典中。


仅运行部分测试

Django 的整个测试套件需要一段时间才能运行,并且如果您只是向 Django 添加了一个想要快速运行而不运行其他所有测试的测试,那么运行每一个测试都可能是多余的。 您可以通过将测试模块的名称附加到命令行上的 runtests.py 来运行单元测试的子集。

例如,如果您只想为通用关系和国际化运行测试,请键入:

您如何找出各个测试的名称? 查看 tests/ — 每个目录名称都有一个测试名称。

如果您只想运行特定类别的测试,您可以指定单个测试类的路径列表。 例如,要运行 i18n 模块的 TranslationTests,请键入:

除此之外,您可以指定一个单独的测试方法,如下所示:

您可以使用 --start-at 选项从指定的顶级模块开始运行测试。 例如:

您还可以使用 --start-after 选项在指定的顶级模块之后运行测试。 例如:

请注意,--reverse 选项不会影响 --start-at--start-after 选项。 此外,这些选项不能与测试标签一起使用。


运行 Selenium 测试

某些测试需要 Selenium 和 Web 浏览器。 要运行这些测试,您必须安装 selenium 包并使用 --selenium=<BROWSERS> 选项运行测试。 例如,如果您安装了 Firefox 和 Google Chrome:

有关可用浏览器的列表,请参阅 selenium.webdriver 包。

指定 --selenium 会自动设置 --tags=selenium 以仅运行需要硒的测试。

一些浏览器(例如 Chrome 或 Firefox)支持无头测试,可以更快更稳定。 添加 --headless 选项以启用此模式。


运行所有测试

如果要运行完整的测试套件,则需要安装许多依赖项:

您可以在 Django 源代码树的 tests/requirements 目录中的 pip 需求文件 中找到这些依赖项,并像这样安装它们:

如果您在安装过程中遇到错误,您的系统可能缺少一个或多个 Python 包的依赖项。 查阅失败包的文档或使用您遇到的错误消息在 Web 上搜索。

您还可以使用 oracle.txtmysql.txtpostgres.txt 安装您选择的数据库适配器。

如果你想测试 memcached 缓存后端,你还需要定义一个指向你的 memcached 实例的 :setting:`CACHES` 设置。

要运行 GeoDjango 测试,您需要 设置空间数据库并安装地理空间库

这些依赖项中的每一个都是可选的。 如果您遗漏了其中任何一个,相关的测试将被跳过。

要运行一些自动重载测试,您需要安装 Watchman 服务。


代码覆盖率

鼓励贡献者在测试套件上运行覆盖以识别需要额外测试的区域。 覆盖率工具的安装和使用在测试代码覆盖率中有描述。

覆盖率应该在单个过程中运行以获得准确的统计数据。 要使用标准测试设置在 Django 测试套件上运行覆盖率:

运行coverage后,通过运行生成html报告:

在运行 Django 测试的覆盖率时,包含的 .coveragerc 设置文件将 coverage_html 定义为报告的输出目录,并且还排除了几个与结果无关的目录(测试代码或外部代码包含在姜戈)。


贡献应用程序

可以在 tests/ 目录中找到对 contrib 应用程序的测试,通常在 <app_name>_tests 下。 例如,contrib.auth 的测试位于 tests/auth_tests


故障排除

测试套件在 master 分支上挂起或显示失败

确保您拥有 支持的 Python 版本 的最新版本,因为早期版本中经常存在可能导致测试套件失败或挂起的错误。

macOS(High Sierra 和更新版本)上,您可能会看到此消息被记录,之后测试挂起:

objc[42074]: +[__NSPlaceholderDate initialize] may have been in progress in
another thread when fork() was called.

为了避免这种情况,请设置 OBJC_DISABLE_INITIALIZE_FORK_SAFETY 环境变量,例如:

$ OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES ./runtests.py

或者将 export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES 添加到您的 shell 的启动文件中(例如 ~/.profile)。


UnicodeEncodeError 的许多测试失败

如果未安装 locales 软件包,某些测试将失败并显示 UnicodeEncodeError

您可以在基于 Debian 的系统上解决此问题,例如,通过运行:

$ apt-get install locales
$ dpkg-reconfigure locales

您可以通过配置 shell 的语言环境为 macOS 系统解决此问题:

$ export LANG="en_US.UTF-8"
$ export LC_ALL="en_US.UTF-8"

运行 locale 命令以确认更改。 或者,将这些导出命令添加到您的 shell 的启动文件中(例如 ~/.bashrc for Bash) 以避免必须重新输入它们。


仅在组合中失败的测试

如果测试在单独运行时通过但在整个套件中失败,我们有一些工具可以帮助分析问题。

runtests.py--bisect 选项将运行失败的测试,同时在每次迭代中将与它一起运行的测试集减半,通常可以识别出可能与此相关的少量测试失败。

例如,假设单独运行的失败测试是 ModelTest.test_eq,然后使用:

将尝试确定干扰给定测试的测试。 首先,测试使用测试套件的前半部分运行。 如果发生故障,测试套件的前半部分将分为两组,然后每组运行指定的测试。 如果测试套件的前半部分没有失败,则测试套件的后半部分将使用指定的测试运行,并按前面所述进行适当拆分。 该过程重复进行,直到失败的测试集最小化。

--pair 选项与套件中的所有其他测试一起运行给定的测试,让您检查另一个测试是否有导致失败的副作用。 所以:

test_eq 与每个测试标签配对。

对于 --bisect--pair,如果您已经怀疑哪些情况可能是导致失败的原因,您可以将测试限制为由 指定进一步的测试标签 之后进行交叉分析第一个:

您还可以尝试使用 --reverse 选项反向运行任何一组测试,以验证以不同顺序执行测试不会导致任何问题:


查看测试期间运行的 SQL 查询

如果您希望检查在失败测试中运行的 SQL,您可以使用 --debug-sql 选项打开 SQL 日志记录 。 如果将其与 --verbosity=2 结合使用,将输出所有 SQL 查询:


查看测试失败的完整追溯

默认情况下,测试与每个内核一个进程并行运行。 但是,当测试并行运行时,您只会看到任何测试失败的截断回溯。 您可以使用 --parallel 选项调整此行为:

为此,您还可以使用 DJANGO_TEST_PROCESSES 环境变量。


编写测试的技巧

隔离模型注册

为避免污染全局 apps 注册表并防止不必要的表创建,测试方法中定义的模型应绑定到临时 Apps 实例:

from django.apps.registry import Apps
from django.db import models
from django.test import SimpleTestCase

class TestModelDefinition(SimpleTestCase):
    def test_model_definition(self):
        test_apps = Apps(['app_label'])

        class TestModel(models.Model):
            class Meta:
                apps = test_apps
        ...
django.test.utils.isolate_apps(*app_labels, attr_name=None, kwarg_name=None)

由于此模式涉及大量样板文件,Django 提供了 isolate_apps() 装饰器。 它是这样使用的:

from django.db import models
from django.test import SimpleTestCase
from django.test.utils import isolate_apps

class TestModelDefinition(SimpleTestCase):
    @isolate_apps('app_label')
    def test_model_definition(self):
        class TestModel(models.Model):
            pass
        ...

设置 app_label

在没有显式 app_label 的测试方法中定义的模型会自动分配其测试类所在的应用程序的标签。

为了确保在 isolate_apps() 实例的上下文中定义的模型正确安装,您应该将一组目标 app_label 作为参数传递:

测试/app_label/tests.py

from django.db import models
from django.test import SimpleTestCase
from django.test.utils import isolate_apps

class TestModelDefinition(SimpleTestCase):
    @isolate_apps('app_label', 'other_app_label')
    def test_model_definition(self):
        # This model automatically receives app_label='app_label'
        class TestModel(models.Model):
            pass

        class OtherAppModel(models.Model):
            class Meta:
                app_label = 'other_app_label'
        ...

装饰器也可以应用于类:

from django.db import models
from django.test import SimpleTestCase
from django.test.utils import isolate_apps

@isolate_apps('app_label')
class TestModelDefinition(SimpleTestCase):
    def test_model_definition(self):
        class TestModel(models.Model):
            pass
        ...

当用作类装饰器时,可以使用 attr_name 参数将用于隔离模型注册的临时 Apps 实例作为属性检索:

from django.db import models
from django.test import SimpleTestCase
from django.test.utils import isolate_apps

@isolate_apps('app_label', attr_name='apps')
class TestModelDefinition(SimpleTestCase):
    def test_model_definition(self):
        class TestModel(models.Model):
            pass
        self.assertIs(self.apps.get_model('app_label', 'TestModel'), TestModel)

或者作为测试方法的参数,当用作方法装饰器时,使用 kwarg_name 参数:

from django.db import models
from django.test import SimpleTestCase
from django.test.utils import isolate_apps

class TestModelDefinition(SimpleTestCase):
    @isolate_apps('app_label', kwarg_name='apps')
    def test_model_definition(self, apps):
        class TestModel(models.Model):
            pass
        self.assertIs(apps.get_model('app_label', 'TestModel'), TestModel)