单元测试

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

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

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

运行单元测试

快速入门

首先,在 GitHub 上 Fork Django

其次,创建并激活虚拟环境。如果您不熟悉如何执行此操作,请阅读我们的 贡献教程

接下来,克隆您的 Fork,安装一些需求并运行测试

$ git clone https://github.com/YourGitHubName/django.git django-repo
$ cd django-repo/tests
$ python -m pip install -e ..
$ python -m pip install -r requirements/py3.txt
$ ./runtests.py
...\> git clone https://github.com/YourGitHubName/django.git django-repo
...\> cd django-repo\tests
...\> py -m pip install -e ..
...\> py -m pip install -r requirements\py3.txt
...\> runtests.py 

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

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

运行测试需要一个 Django settings 模块来定义要使用的数据库。为了帮助您入门,Django 提供并使用了一个示例 settings 模块,该模块使用 SQLite 数据库。有关如何使用不同的 settings 模块以不同的数据库运行测试,请参见 使用其他 settings 模块

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

使用 tox 运行测试

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

$ python -m pip install tox
$ tox
...\> py -m pip install tox
...\> tox

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

$ tox -l
py3
black
blacken-docs
flake8>=3.7.0
docs
isort>=5.1.0
...\> tox -l
py3
black
blacken-docs
flake8>=3.7.0
docs
isort>=5.1.0

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

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

$ tox -e py310-postgres -- --settings=my_postgres_settings
...\> tox -e py310-postgres -- --settings=my_postgres_settings

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

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

Tox 还会尊重 DJANGO_SETTINGS_MODULE 环境变量(如果已设置)。例如,以下命令等效于上述命令

$ DJANGO_SETTINGS_MODULE=my_postgres_settings tox -e py310-postgres

Windows 用户应使用

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

运行 JavaScript 测试

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

$ tox -e javascript
...\> tox -e javascript

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

使用 django-docker-box 运行测试

django-docker-box 允许您在所有受支持的数据库和 Python 版本上运行 Django 的测试套件。有关安装和使用说明,请参见 django-docker-box 项目页面。

使用其他 settings 模块

包含的 settings 模块 (tests/test_sqlite.py) 允许您使用 SQLite 运行测试套件。如果您想使用其他数据库运行测试,则需要定义您自己的 settings 文件。某些测试(例如 contrib.postgres 的测试)特定于特定的数据库后端,如果使用其他后端运行,则将被跳过。某些测试在特定的数据库后端上会被跳过或预期会失败(请参见每个后端的 DatabaseFeatures.django_test_skipsDatabaseFeatures.django_test_expected_failures)。

要使用不同的设置运行测试,请确保该模块位于您的 PYTHONPATH 上,并使用 --settings 传递该模块。

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

  • 一个 default 数据库。此数据库应使用您希望用于主要测试的后端。

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

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

  • USER 选项需要指定数据库中现有的用户帐户。该用户需要执行 CREATE DATABASE 的权限,以便可以创建测试数据库。

  • PASSWORD 选项需要提供已指定的 USER 的密码。

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

您还需要确保您的数据库使用 UTF-8 作为默认字符集。如果您的数据库服务器不使用 UTF-8 作为默认字符集,则需要在适用数据库的测试 settings 字典中包含 CHARSET 的值。

仅运行部分测试

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

例如,如果您只想运行泛型关系和国际化的测试,请输入

$ ./runtests.py --settings=path.to.settings generic_relations i18n
...\> runtests.py --settings=path.to.settings generic_relations i18n

如何找到各个测试的名称?查看 tests/ - 每个目录名称都是测试的名称。

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

$ ./runtests.py --settings=path.to.settings i18n.tests.TranslationTests
...\> runtests.py --settings=path.to.settings i18n.tests.TranslationTests

更进一步,您可以像这样指定单个测试方法

$ ./runtests.py --settings=path.to.settings i18n.tests.TranslationTests.test_lazy_objects
...\> runtests.py --settings=path.to.settings i18n.tests.TranslationTests.test_lazy_objects

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

$ ./runtests.py --start-at=wsgi
...\> runtests.py --start-at=wsgi

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

$ ./runtests.py --start-after=wsgi
...\> runtests.py --start-after=wsgi

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

运行 Selenium 测试

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

$ ./runtests.py --selenium=firefox,chrome
...\> runtests.py --selenium=firefox,chrome

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

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

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

为了测试对管理 UI 的更改,可以启用--screenshots选项来运行 selenium 测试。屏幕截图将保存到tests/screenshots/目录中。

要定义在 selenium 测试期间何时应拍摄屏幕截图,测试类必须使用@django.test.selenium.screenshot_cases装饰器,并带有一系列受支持的屏幕截图类型("desktop_size""mobile_size""small_screen_size""rtl""dark""high_contrast")。然后,它可以在所需位置调用self.take_screenshot("unique-screenshot-name")以生成屏幕截图。例如

from django.test.selenium import SeleniumTestCase, screenshot_cases
from django.urls import reverse


class SeleniumTests(SeleniumTestCase):
    @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"])
    def test_login_button_centered(self):
        self.selenium.get(self.live_server_url + reverse("admin:login"))
        self.take_screenshot("login")
        ...

这将生成登录页面的多个屏幕截图 - 一个用于桌面屏幕,一个用于移动屏幕,一个用于桌面上的从右到左语言,一个用于桌面上的暗模式,以及一个用于使用 Chrome 时桌面上的高对比度模式。

Django 5.1 中的更改

添加了--screenshots选项和@screenshot_cases装饰器。

运行所有测试

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

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

$ python -m pip install -r tests/requirements/py3.txt
...\> py -m pip install -r tests\requirements\py3.txt

如果您在安装过程中遇到错误,则您的系统可能缺少一个或多个 Python 软件包的依赖项。请查阅出现故障的软件包的文档,或使用遇到的错误消息在网络上搜索。

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

如果您想测试 memcached 或 Redis 缓存后端,您还需要定义一个CACHES设置,分别指向您的 memcached 或 Redis 实例。

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

这些依赖项都是可选的。如果您缺少任何一个,则关联的测试将被跳过。

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

代码覆盖率

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

要使用标准测试设置在 Django 测试套件上运行覆盖率

$ coverage run ./runtests.py --settings=test_sqlite
...\> coverage run runtests.py --settings=test_sqlite

运行覆盖率后,通过运行以下命令合并所有覆盖率统计信息

$ coverage combine
...\> coverage combine

之后,通过运行以下命令生成 html 报告

$ coverage html
...\> coverage html

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

Contrib 应用

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

故障排除

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

确保您拥有受支持的 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 的启动文件(例如,Bash 的~/.bashrc)中,以避免重新键入它们。

仅在组合时失败的测试

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

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

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

$ ./runtests.py --bisect basic.tests.ModelTest.test_eq
...\> runtests.py --bisect basic.tests.ModelTest.test_eq

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

使用 --pair 选项可以将给定的测试与测试套件中的其他每个测试一起运行,以便检查其他测试是否存在导致失败的副作用。因此

$ ./runtests.py --pair basic.tests.ModelTest.test_eq
...\> runtests.py --pair basic.tests.ModelTest.test_eq

将把 test_eq 与每个测试标签配对。

同时使用 --bisect--pair 时,如果您已经怀疑哪些用例可能是导致失败的原因,则可以通过在第一个测试标签后 指定其他测试标签 来限制要进行交叉分析的测试。

$ ./runtests.py --pair basic.tests.ModelTest.test_eq queries transactions
...\> runtests.py --pair basic.tests.ModelTest.test_eq queries transactions

您还可以尝试使用 --shuffle--reverse 选项以随机或反向顺序运行任何一组测试。这有助于验证以不同顺序执行测试不会导致任何问题。

$ ./runtests.py basic --shuffle
$ ./runtests.py basic --reverse
...\> runtests.py basic --shuffle
...\> runtests.py basic --reverse

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

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

$ ./runtests.py basic --debug-sql
...\> runtests.py basic --debug-sql

查看测试失败的完整回溯

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

$ ./runtests.py basic --parallel=1
...\> runtests.py basic --parallel=1

您也可以为此目的使用 DJANGO_TEST_PROCESSES 环境变量。

编写测试的技巧

隔离模型注册

为了避免污染全局 apps 注册表并防止不必要的表创建,在测试方法中定义的模型应绑定到一个临时的 Apps 实例。为此,请使用 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 集作为参数传递。

tests/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"

        ...
返回顶部