高级测试主题

请求工厂

class RequestFactory

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

RequestFactory 的 API 是测试客户端 API 的一个稍受限制的子集

  • 它只能访问 HTTP 方法 get()post()put()delete()head()options()trace()
  • 这些方法接受所有相同的参数,除了 follow。由于这只是用于生成请求的工厂,因此由你来处理响应。
  • 它不支持中间件。如果视图要正常运行,则会话和身份验证属性必须由测试本身提供。
Django 4.2 中已更改

添加了 headers 参数。

示例

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

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

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

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

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

Django 4.2 中已更改

添加了 headers 参数。

测试基于类的视图

为了在请求/响应周期之外测试基于类的视图,你必须确保它们已正确配置,方法是在实例化后调用 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", ...]

另一种选择是使用 ALLOWED_HOSTS 将所需主机添加到 override_settings()modify_settings() 中。此选项对于无法打包其自身设置文件或域列表不是静态的项目(例如,多租户的子域)中的独立应用程序可能更可取。例如,您可以为域 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 在每个测试之前触发,为 available apps 中的每个模型创建内容类型和权限(如果它们不存在)。
  • 在每个测试之后,Django 仅清空与 available apps 中的模型对应的表。但是,在数据库级别,截断可能会级联到 unavailable apps 中的相关模型。此外,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 来顺序运行它们。此混入使用文件系统 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)

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 对象。如果提供了此对象,则会使用它来记录消息,而不是打印到控制台。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)

运行测试套件。

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

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

classmethod DiscoverRunner.add_arguments(parser)

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

DiscoverRunner.setup_test_environment(**kwargs)

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

DiscoverRunner.build_suite(test_labels=None, **kwargs)

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

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)

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

DiscoverRunner.run_checks(databases)

在测试 databases 上运行 系统检查

DiscoverRunner.run_suite(suite, **kwargs)

运行测试套件。

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

DiscoverRunner.get_test_runner_kwargs()

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

DiscoverRunner.teardown_databases(old_config, **kwargs)

销毁测试数据库,通过调用 teardown_databases() 恢复测试前条件。

DiscoverRunner.teardown_test_environment(**kwargs)

恢复测试前环境。

DiscoverRunner.suite_result(suite, result, **kwargs)

根据测试套件和该测试套件的结果计算并返回一个返回代码。

DiscoverRunner.log(msg, level=None)

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

测试实用工具

django.test.utils

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

setup_test_environment(debug=None)

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

如果 debug 不是 NoneDEBUG 设置将更新为其值。

teardown_test_environment()

执行全局测试后清理,例如从模板系统中移除检测工具和恢复正常电子邮件服务。

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

创建测试数据库。

返回一个提供足够细节的数据结构,以便撤消所做的更改。此数据将在测试结束时提供给 teardown_databases() 函数。

aliases 参数决定了应该为哪些 DATABASES 别名测试数据库进行设置。如果没有提供,则默认为所有 DATABASES 别名。

serialized_aliases 参数决定了哪些 aliases 测试数据库的子集应该将它们的状态序列化,以便使用 serialized_rollback 特性。如果没有提供,则默认为 aliases

teardown_databases(old_config, parallel=0, keepdb=False)

销毁测试数据库,恢复测试前的条件。

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

django.db.connection.creation

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

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

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

verbosityrun_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 集成,coverage.py 是用于测量 Python 程序代码覆盖率的工具。首先,安装 coverage。接下来,从包含 manage.py 的项目文件夹中运行以下内容

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

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

coverage report

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

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

返回顶部