高级测试主题

请求工厂

class RequestFactory[source]

RequestFactory 的 API 与测试客户端的 API 相同。但是,它不像浏览器那样工作,RequestFactory 提供了一种生成请求实例的方法,该实例可以用作任何视图的第一个参数。这意味着您可以像测试任何其他函数一样测试视图函数——作为一个黑盒,具有精确已知的输入,测试特定的输出。

RequestFactory 的 API 是测试客户端 API 的一个略微受限的子集。

  • 它只能访问 HTTP 方法 get()post()put()delete()head()options()trace()

  • 这些方法接受所有相同的参数,除了follow。由于这只是一个用于生成请求的工厂,因此由您来处理响应。

  • 它不支持中间件。如果视图正常运行需要会话和身份验证属性,则必须由测试本身提供。

Django 5.1 中的更改

添加了query_params参数。

示例

以下是使用请求工厂的单元测试

from django.contrib.auth.models import AnonymousUser, User
from django.test import RequestFactory, TestCase

from .views import MyView, my_view


class SimpleTest(TestCase):
    def setUp(self):
        # Every test needs access to the request factory.
        self.factory = RequestFactory()
        self.user = User.objects.create_user(
            username="jacob", email="jacob@…", password="top_secret"
        )

    def test_details(self):
        # Create an instance of a GET request.
        request = self.factory.get("/customer/details")

        # Recall that middleware are not supported. You can simulate a
        # logged-in user by setting request.user manually.
        request.user = self.user

        # Or you can simulate an anonymous user by setting request.user to
        # an AnonymousUser instance.
        request.user = AnonymousUser()

        # Test my_view() as if it were deployed at /customer/details
        response = my_view(request)
        # Use this syntax for class-based views.
        response = MyView.as_view()(request)
        self.assertEqual(response.status_code, 200)

AsyncRequestFactory

class AsyncRequestFactory[source]

RequestFactory 创建类似 WSGI 的请求。如果您想创建类似 ASGI 的请求,包括拥有正确的 ASGI scope,您可以改用django.test.AsyncRequestFactory

此类与RequestFactory 直接兼容,唯一的区别是它返回ASGIRequest 实例而不是WSGIRequest 实例。它的所有方法仍然是同步的可调用对象。

defaults 中的任意关键字参数将直接添加到 ASGI scope 中。

Django 5.1 中的更改

添加了query_params参数。

测试基于类的视图

为了在请求/响应周期之外测试基于类的视图,您必须确保它们已正确配置,方法是在实例化后调用setup()

例如,假设以下基于类的视图

views.py
from django.views.generic import TemplateView


class HomeView(TemplateView):
    template_name = "myapp/home.html"

    def get_context_data(self, **kwargs):
        kwargs["environment"] = "Production"
        return super().get_context_data(**kwargs)

您可以通过首先实例化视图,然后将request 传递给setup(),然后继续执行测试代码来直接测试get_context_data() 方法。

tests.py
from django.test import RequestFactory, TestCase
from .views import HomeView


class HomePageTest(TestCase):
    def test_environment_set_in_context(self):
        request = RequestFactory().get("/")
        view = HomeView()
        view.setup(request)

        context = view.get_context_data()
        self.assertIn("environment", context)

测试和多个主机名

运行测试时会验证ALLOWED_HOSTS 设置。这允许测试客户端区分内部和外部 URL。

支持多租户或根据请求的主机更改业务逻辑并在测试中使用自定义主机名的项目必须在其ALLOWED_HOSTS 中包含这些主机。

第一个选择是将主机添加到您的设置文件。例如,docs.djangoproject.com 的测试套件包含以下内容:

from django.test import TestCase


class SearchFormTestCase(TestCase):
    def test_empty_get(self):
        response = self.client.get(
            "/en/dev/search/",
            headers={"host": "docs.djangoproject.dev:8000"},
        )
        self.assertEqual(response.status_code, 200)

并且设置文件包含项目支持的域列表。

ALLOWED_HOSTS = ["www.djangoproject.dev", "docs.djangoproject.dev", ...]

另一个选择是使用override_settings()modify_settings() 将所需的主机添加到ALLOWED_HOSTS。对于无法打包自己的设置文件的独立应用程序,或者对于域列表不是静态的项目(例如,多租户的子域),此选项可能更好。例如,您可以为域http://otherserver/编写如下测试:

from django.test import TestCase, override_settings


class MultiDomainTestCase(TestCase):
    @override_settings(ALLOWED_HOSTS=["otherserver"])
    def test_other_domain(self):
        response = self.client.get("http://otherserver/foo/bar/")

在运行测试时禁用ALLOWED_HOSTS 检查(ALLOWED_HOSTS = ['*'])将阻止测试客户端在您重定向到外部 URL 时引发有用的错误消息。

测试和多个数据库

测试主/从配置

如果您正在测试具有主/从(某些数据库称为主/从)复制功能的多数据库配置,则创建测试数据库的此策略会带来问题。创建测试数据库时,不会有任何复制,因此在主数据库上创建的数据将不会在从数据库上看到。

为了弥补这一点,Django 允许您定义数据库是测试镜像。考虑以下(简化)示例数据库配置:

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": "myproject",
        "HOST": "dbprimary",
        # ... plus some other settings
    },
    "replica": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": "myproject",
        "HOST": "dbreplica",
        "TEST": {
            "MIRROR": "default",
        },
        # ... plus some other settings
    },
}

在此设置中,我们有两个数据库服务器:dbprimary(由数据库别名default描述)和dbreplica(由别名replica描述)。正如您可能预期的那样,数据库管理员已将dbreplica配置为dbprimary 的只读副本,因此在正常活动中,对default的任何写入都将出现在replica上。

如果 Django 创建了两个独立的测试数据库,这将破坏任何期望发生复制的测试。但是,replica 数据库已配置为测试镜像(使用MIRROR 测试设置),这表示在测试下,replica 应被视为default的镜像。

配置测试环境时,将不会创建replica的测试版本。相反,replica 的连接将被重定向到指向default。结果,对default的写入将出现在replica上——但因为它们实际上是同一个数据库,而不是因为这两个数据库之间存在数据复制。由于这取决于事务,因此测试必须使用TransactionTestCase 而不是TestCase

控制测试数据库的创建顺序

默认情况下,Django 将假定所有数据库都依赖于default 数据库,因此始终首先创建default 数据库。但是,对于测试设置中任何其他数据库的创建顺序,没有任何保证。

如果您的数据库配置需要特定的创建顺序,您可以使用 DEPENDENCIES 测试设置指定存在的依赖关系。考虑以下(简化)示例数据库配置

DATABASES = {
    "default": {
        # ... db settings
        "TEST": {
            "DEPENDENCIES": ["diamonds"],
        },
    },
    "diamonds": {
        # ... db settings
        "TEST": {
            "DEPENDENCIES": [],
        },
    },
    "clubs": {
        # ... db settings
        "TEST": {
            "DEPENDENCIES": ["diamonds"],
        },
    },
    "spades": {
        # ... db settings
        "TEST": {
            "DEPENDENCIES": ["diamonds", "hearts"],
        },
    },
    "hearts": {
        # ... db settings
        "TEST": {
            "DEPENDENCIES": ["diamonds", "clubs"],
        },
    },
}

在此配置下,diamonds 数据库将首先创建,因为它是不依赖于其他数据库别名的唯一数据库别名。 defaultclubs 别名将接下来创建(尽管这对别名的创建顺序不保证),然后是 hearts,最后是 spades

如果 DEPENDENCIES 定义中存在任何循环依赖关系,则将引发 ImproperlyConfigured 异常。

TransactionTestCase 的高级功能

TransactionTestCase.available_apps

警告

此属性是一个私有 API。将来可能会在不经过弃用期的情况下更改或删除它,例如为了适应应用程序加载的更改。

它用于优化 Django 自身的测试套件,该套件包含数百个模型,但在不同应用程序中的模型之间没有关系。

默认情况下,available_apps 设置为 None。每次测试后,Django 都会调用 flush 来重置数据库状态。这会清空所有表并发出 post_migrate 信号,该信号会为每个模型重新创建一个内容类型和四个权限。此操作的成本会随着模型数量的增加而线性增长。

available_apps 设置为应用程序列表会指示 Django 仅将这些应用程序中的模型视为可用。 TransactionTestCase 的行为将如下更改

  • post_migrate 在每次测试之前都会被触发,以创建可用应用程序中每个模型的内容类型和权限(如果缺失)。

  • 每次测试后,Django 只会清空与可用应用程序中的模型对应的表。但是,在数据库级别,截断可能会级联到不可用应用程序中的相关模型。此外,post_migrate 不会被触发;它将在下一个 TransactionTestCase 选择正确的应用程序集后被触发。

由于数据库没有完全刷新,如果测试创建了未包含在 available_apps 中的模型实例,它们将泄漏,并可能导致不相关的测试失败。小心使用会话的测试;默认会话引擎将它们存储在数据库中。

由于在刷新数据库后不会发出 post_migrate,因此 TransactionTestCase 后的状态与 TestCase 后的状态不同:缺少由 post_migrate 的监听器创建的行。考虑到 测试执行顺序,如果给定测试套件中的所有 TransactionTestCase 都声明了 available_apps,或者都没有声明,则这不是问题。

available_apps 在 Django 自身的测试套件中是必需的。

TransactionTestCase.reset_sequences

TransactionTestCase 上设置 reset_sequences = True 将确保在测试运行之前始终重置序列。

class TestsThatDependsOnPrimaryKeySequences(TransactionTestCase):
    reset_sequences = True

    def test_animal_pk(self):
        lion = Animal.objects.create(name="lion", sound="roar")
        # lion.pk is guaranteed to always be 1
        self.assertEqual(lion.pk, 1)

除非您明确测试主键序列号,否则建议您不要在测试中硬编码主键值。

使用 reset_sequences = True 会降低测试速度,因为主键重置是一个相对昂贵的数据库操作。

强制按顺序运行测试类

如果您有无法并行运行的测试类(例如,因为它们共享一个公共资源),您可以使用 django.test.testcases.SerializeMixin 按顺序运行它们。此 mixin 使用文件系统 lockfile

例如,您可以使用 __file__ 来确定继承自 SerializeMixin 的同一文件中的所有测试类将按顺序运行。

import os

from django.test import TestCase
from django.test.testcases import SerializeMixin


class ImageTestCaseMixin(SerializeMixin):
    lockfile = __file__

    def setUp(self):
        self.filename = os.path.join(temp_storage_dir, "my_file.png")
        self.file = create_file(self.filename)


class RemoveImageTests(ImageTestCaseMixin, TestCase):
    def test_remove_image(self):
        os.remove(self.filename)
        self.assertFalse(os.path.exists(self.filename))


class ResizeImageTests(ImageTestCaseMixin, TestCase):
    def test_resize_image(self):
        resize_image(self.file, (48, 48))
        self.assertEqual(get_image_size(self.file), (48, 48))

使用 Django 测试运行器测试可重用应用程序

如果您正在编写一个 可重用应用程序,您可能希望使用 Django 测试运行器来运行您自己的测试套件,从而受益于 Django 测试基础设施。

一个常见的做法是在应用程序代码旁边放一个名为 tests 的目录,其结构如下:

runtests.py
polls/
    __init__.py
    models.py
    ...
tests/
    __init__.py
    models.py
    test_settings.py
    tests.py

让我们来看看其中几个文件的内容:

runtests.py
#!/usr/bin/env python
import os
import sys

import django
from django.conf import settings
from django.test.utils import get_runner

if __name__ == "__main__":
    os.environ["DJANGO_SETTINGS_MODULE"] = "tests.test_settings"
    django.setup()
    TestRunner = get_runner(settings)
    test_runner = TestRunner()
    failures = test_runner.run_tests(["tests"])
    sys.exit(bool(failures))

这是您用来运行测试套件的脚本。它设置 Django 环境,创建测试数据库并运行测试。

为了清晰起见,此示例仅包含使用 Django 测试运行器所需的最低限度内容。您可能希望添加命令行选项来控制详细程度,传递要运行的特定测试标签等。

tests/test_settings.py
SECRET_KEY = "fake-key"
INSTALLED_APPS = [
    "tests",
]

此文件包含运行应用程序测试所需的 Django 设置

同样,这是一个最小示例;您的测试可能需要其他设置才能运行。

由于在运行测试时 tests 包包含在 INSTALLED_APPS 中,因此您可以在其 models.py 文件中定义仅供测试使用的模型。

使用不同的测试框架

显然,unittest 不是唯一的 Python 测试框架。虽然 Django 没有为替代框架提供明确的支持,但它确实提供了一种方法,可以将为替代框架构建的测试调用为普通的 Django 测试。

当您运行 ./manage.py test 时,Django 会查看 TEST_RUNNER 设置以确定要执行的操作。默认情况下,TEST_RUNNER 指向 'django.test.runner.DiscoverRunner'。此类定义了默认的 Django 测试行为。此行为包括:

  1. 执行全局预测试设置。

  2. 在当前目录下查找名称与模式 test*.py 匹配的任何文件中的测试。

  3. 创建测试数据库。

  4. 运行 migrate 以将模型和初始数据安装到测试数据库中。

  5. 运行 系统检查

  6. 运行找到的测试。

  7. 销毁测试数据库。

  8. 执行全局后测试拆卸。

如果您定义了自己的测试运行器类并将 TEST_RUNNER 指向该类,则无论何时运行 ./manage.py test,Django 都会执行您的测试运行器。通过这种方式,可以使用任何可以从 Python 代码执行的测试框架,或者修改 Django 测试执行过程以满足您可能拥有的任何测试要求。

定义测试运行器

测试运行器是一个定义了run_tests()方法的类。Django自带一个DiscoverRunner类,它定义了Django测试的默认行为。这个类定义了run_tests()入口点,以及run_tests()用来设置、执行和拆卸测试套件的其他方法。

class DiscoverRunner(pattern='test*.py', top_level=None, verbosity=1, interactive=True, failfast=False, keepdb=False, reverse=False, debug_mode=False, debug_sql=False, parallel=0, tags=None, exclude_tags=None, test_name_patterns=None, pdb=False, buffer=False, enable_faulthandler=True, timing=True, shuffle=False, logger=None, durations=None, **kwargs)[source]

DiscoverRunner 将搜索任何与 pattern 匹配的文件中的测试。

top_level 可用于指定包含顶级 Python 模块的目录。通常 Django 可以自动找出这个目录,所以不需要指定此选项。如果指定,它通常应该是包含 manage.py 文件的目录。

verbosity 决定了将打印到控制台的通知和调试信息量;0 表示无输出,1 表示正常输出,2 表示详细输出。

如果 interactiveTrue,则测试套件在执行时有权向用户请求指示。此行为的一个示例是请求许可以删除现有的测试数据库。如果 interactiveFalse,则测试套件必须能够在没有任何人工干预的情况下运行。

如果 failfastTrue,则测试套件将在检测到第一个测试失败后停止运行。

如果 keepdbTrue,则测试套件将使用现有数据库,或者在必要时创建一个数据库。如果为 False,则将创建一个新数据库,如果存在现有数据库,则会提示用户将其删除。

如果 reverseTrue,则测试用例将以相反的顺序执行。这对于调试未正确隔离并具有副作用的测试可能很有用。按测试类分组 在使用此选项时会保留。此选项可以与 --shuffle 结合使用,以反转特定随机种子顺序。

debug_mode 指定在运行测试之前应将DEBUG 设置设置为多少。

parallel 指定进程数。如果 parallel 大于 1,则测试套件将在 parallel 个进程中运行。如果测试用例类少于配置的进程数,Django 将相应地减少进程数。每个进程都有自己的数据库。此选项需要第三方 tblib 包才能正确显示回溯。

tags 可用于指定一组用于筛选测试的标签。可以与 exclude_tags 结合使用。

exclude_tags 可用于指定一组用于排除测试的标签。可以与 tags 结合使用。

如果 debug_sqlTrue,则失败的测试用例将输出记录到django.db.backends 日志记录器 的 SQL 查询以及回溯。如果 verbosity2,则将输出所有测试中的查询。

test_name_patterns 可用于指定一组模式,用于根据其名称筛选测试方法和类。

如果 pdbTrue,则将在每个测试错误或失败时生成调试器(pdbipdb)。

如果 bufferTrue,则将丢弃来自通过测试的输出。

如果 enable_faulthandlerTrue,则将启用faulthandler

如果 timingTrue,则将显示测试计时,包括数据库设置和总运行时间。

如果 shuffle 是一个整数,则测试用例将在执行前以随机顺序进行混洗,使用该整数作为随机种子。如果 shuffleNone,则将随机生成种子。在这两种情况下,种子都将被记录并设置为 self.shuffle_seed,然后再运行测试。此选项可用于帮助检测未正确隔离的测试。按测试类分组 在使用此选项时会保留。

logger 可用于传递 Python Logger 对象。如果提供,则将使用日志记录器记录消息,而不是打印到控制台。日志记录器对象将尊重其日志记录级别,而不是 verbosity

durations 将显示 N 个最慢的测试用例的列表。将此选项设置为 0 将导致显示所有测试的持续时间。需要 Python 3.12+。

Django 可能会不时通过添加新参数来扩展测试运行器的功能。**kwargs 声明允许这种扩展。如果您是 DiscoverRunner 的子类或编写您自己的测试运行器,请确保它接受 **kwargs

您的测试运行器也可以定义额外的命令行选项。创建一个或覆盖一个add_arguments(cls, parser)类方法,并通过在方法内部调用parser.add_argument()来添加自定义参数,以便test命令能够使用这些参数。

Django 5.0 新特性

添加了durations参数。

属性

DiscoverRunner.test_suite

用于构建测试套件的类。默认情况下,它设置为unittest.TestSuite。如果您希望实现不同的测试收集逻辑,可以覆盖它。

DiscoverRunner.test_runner

这是用于执行单个测试并格式化结果的底层测试运行器的类。默认情况下,它设置为unittest.TextTestRunner。尽管命名约定不幸地相似,但这与DiscoverRunner的类型不同,后者涵盖更广泛的职责。您可以覆盖此属性来修改测试的运行和报告方式。

DiscoverRunner.test_loader

这是加载测试的类,无论是来自TestCase、模块还是其他地方,并将它们捆绑到测试套件中供运行器执行。默认情况下,它设置为unittest.defaultTestLoader。如果您的测试将以不同寻常的方式加载,您可以覆盖此属性。

方法

DiscoverRunner.run_tests(test_labels, **kwargs)[source]

运行测试套件。

test_labels允许您指定要运行的测试,并支持多种格式(有关支持的格式列表,请参见DiscoverRunner.build_suite())。

此方法应返回失败的测试数量。

classmethod DiscoverRunner.add_arguments(parser)[source]

覆盖此类方法以添加test管理命令接受的自定义参数。有关向解析器添加参数的详细信息,请参见argparse.ArgumentParser.add_argument()

DiscoverRunner.setup_test_environment(**kwargs)[source]

通过调用setup_test_environment()并设置DEBUGself.debug_mode(默认为False)来设置测试环境。

DiscoverRunner.build_suite(test_labels=None, **kwargs)[source]

构造一个与提供的测试标签匹配的测试套件。

test_labels是一个字符串列表,描述要运行的测试。测试标签可以采用四种形式之一

  • path.to.test_module.TestCase.test_method – 运行测试用例类中的单个测试方法。

  • path.to.test_module.TestCase – 运行测试用例中的所有测试方法。

  • path.to.module – 搜索并运行命名 Python 包或模块中的所有测试。

  • path/to/directory – 搜索并运行命名目录下所有的测试。

如果test_labels的值为None,则测试运行器将在当前目录下所有名称与其pattern匹配的文件中搜索测试(见上文)。

返回一个准备运行的TestSuite实例。

DiscoverRunner.setup_databases(**kwargs)[source]

通过调用setup_databases()创建测试数据库。

DiscoverRunner.run_checks(databases)[source]

在测试databases上运行系统检查

DiscoverRunner.run_suite(suite, **kwargs)[source]

运行测试套件。

返回运行测试套件产生的结果。

DiscoverRunner.get_test_runner_kwargs()[source]

返回用DiscoverRunner.test_runner实例化所需的关键字参数。

DiscoverRunner.teardown_databases(old_config, **kwargs)[source]

通过调用teardown_databases()销毁测试数据库,恢复测试前的状态。

DiscoverRunner.teardown_test_environment(**kwargs)[source]

恢复测试前的环境。

DiscoverRunner.suite_result(suite, result, **kwargs)[source]

根据测试套件及其结果计算并返回一个返回代码。

DiscoverRunner.log(msg, level=None)[source]

如果设置了logger,则以给定的整数日志级别(例如logging.DEBUGlogging.INFOlogging.WARNING)记录消息。否则,将根据当前verbosity将消息打印到控制台。例如,如果verbosity为0,则不会打印任何消息;如果verbosity至少为1,则将打印INFO及以上级别的消息;如果verbosity至少为2,则将打印DEBUG级别的消息。level默认为logging.INFO

测试工具

django.test.utils

为了帮助创建您自己的测试运行器,Django 在django.test.utils模块中提供了一些实用程序方法。

setup_test_environment(debug=None)[source]

执行全局预测试设置,例如安装模板渲染系统的检测工具并设置虚拟邮件收件箱。

如果debug不是None,则DEBUG设置将更新为其值。

teardown_test_environment()[source]

执行全局后测试拆卸,例如从模板系统中删除检测工具并恢复正常的邮件服务。

setup_databases(verbosity, interactive, *, time_keeper=None, keepdb=False, debug_sql=False, parallel=0, aliases=None, serialized_aliases=None, **kwargs)[source]

创建测试数据库。

返回一个数据结构,该结构提供了足够的细节来撤销所做的更改。测试结束后,这些数据将提供给teardown_databases()函数。

aliases参数确定应为哪些DATABASES别名设置测试数据库。如果未提供,则默认为所有DATABASES别名。

serialized_aliases参数确定aliases的哪个子集测试数据库应将其状态序列化以允许使用serialized_rollback功能。如果未提供,则默认为aliases

teardown_databases(old_config, parallel=0, keepdb=False)[source]

销毁测试数据库,恢复测试前的状态。

old_config是一个数据结构,定义了需要反转的数据库配置更改。它是setup_databases()方法的返回值。

django.db.connection.creation

数据库后端的创建模块还提供了一些在测试期间可能很有用的实用程序。

create_test_db(verbosity=1, autoclobber=False, serialize=True, keepdb=False)

创建一个新的测试数据库,并对其运行migrate

verbosity的行为与run_tests()中的行为相同。

autoclobber描述了如果发现与测试数据库同名的数据库将发生的行为。

  • 如果autoclobberFalse,则会询问用户是否批准销毁现有数据库。sys.exit如果用户不同意,则调用。

  • 如果autoclobberTrue,则将销毁数据库,而无需咨询用户。

serialize确定Django是否在运行测试之前将数据库序列化为内存中的JSON字符串(如果没有任何事务,则用于在测试之间恢复数据库状态)。如果没有任何带有serialized_rollback=True的测试类,可以将其设置为False以加快创建时间。

keepdb确定测试运行是否应使用现有数据库或创建一个新的数据库。如果为True,则将使用现有数据库,或者如果不存在则创建新的数据库。如果为False,则将创建一个新的数据库,并提示用户删除现有的数据库(如果存在)。

返回它创建的测试数据库的名称。

create_test_db()具有修改NAMEDATABASES中的值以匹配测试数据库名称的副作用。

destroy_test_db(old_database_name, verbosity=1, keepdb=False)

销毁数据库,数据库名称为NAMEDATABASES 中的值,并将 NAME 设置为 old_database_name 的值。

verbosity 参数的行为与 DiscoverRunner 相同。

如果 keepdb 参数为 True,则数据库连接将被关闭,但数据库不会被销毁。

coverage.py 集成

代码覆盖率描述了有多少源代码经过测试。它显示了测试正在执行代码的哪些部分,以及哪些部分没有执行。它是应用程序测试的重要组成部分,因此强烈建议检查测试的覆盖率。

Django 可以轻松地与 coverage.py 集成,这是一个用于测量 Python 程序代码覆盖率的工具。首先,安装 coverage。接下来,从包含 manage.py 的项目文件夹中运行以下命令:

coverage run --source='.' manage.py test myapp

这将运行您的测试并收集项目中已执行文件的覆盖率数据。您可以通过键入以下命令查看此数据的报告:

coverage report

请注意,在运行测试时执行了一些 Django 代码,但由于传递给先前命令的 source 标志,此处未列出这些代码。

有关更多选项(例如详细说明未覆盖行的带注释的 HTML 列表),请参阅 coverage.py 文档。

返回顶部