高级测试主题 — Django 文档

来自菜鸟教程
Django/docs/3.2.x/topics/testing/advanced
跳转至:导航、​搜索

高级测试主题

请求工厂

class RequestFactory

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

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

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

示例

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

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)

异步请求工厂

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

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


测试基于类的视图

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

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

视图.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() 方法:

测试.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)

测试和多个主机名

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

支持多租户或基于请求的主机以其他方式更改业务逻辑并在测试中使用自定义主机名的项目必须在 :setting:`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/', HTTP_HOST='docs.djangoproject.dev:8000')
        self.assertEqual(response.status_code, 200)

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

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

另一种选择是使用 override_settings()modify_settings() 将所需的主机添加到 :setting:`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/')

在运行测试时禁用 :setting:`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数据库已配置为测试镜像(使用 :设置:`镜子 ` 测试设置),表示在测试中,replica 应该被视为一面镜子default .

测试环境配置好后,会创建replica的测试版本而不是。 相反,与 replica 的连接将被重定向到指向 default。 结果,对default的写入会出现在replica上——但因为它们实际上是同一个数据库,而不是因为两个数据库之间存在数据复制。


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

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

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

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

如果有任何循环依赖 :setting:`依赖 ` 定义,一个配置不当将引发异常。


TransactionTestCase的高级功能

TransactionTestCase.available_apps

警告

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

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

默认情况下,available_apps 设置为 None。 每次测试后,Django 调用 :djadmin:`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

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

运行测试文件

#!/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 测试运行程序所需的最低限度。 您可能需要添加命令行选项来控制详细程度、传递特定的测试标签以运行等。

测试/test_settings.py

SECRET_KEY = 'fake-key'
INSTALLED_APPS = [
    "tests",
]

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

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

由于在运行测试时 tests 包包含在 :setting:`INSTALLED_APPS` 中,您可以在其 models.py 文件中定义仅测试模型。


使用不同的测试框架

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

当您运行 ./manage.py test 时,Django 会查看 :setting:`TEST_RUNNER` 设置来确定要做什么。 默认情况下,:setting:`TEST_RUNNER` 指向 'django.test.runner.DiscoverRunner'。 这个类定义了默认的 Django 测试行为。 这种行为涉及:

  1. 执行全局预测试设置。
  2. 在当前目录下的任何文件中查找名称与模式 test*.py 匹配的测试。
  3. 创建测试数据库。
  4. 运行 migrate 将模型和初始数据安装到测试数据库中。
  5. 运行 系统检查
  6. 运行找到的测试。
  7. 销毁测试数据库。
  8. 执行全局测试后拆卸。

如果您定义自己的测试运行程序类并将 :setting:`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, **kwargs)

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

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

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

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

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

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

如果 reverseTrue,则测试用例将以相反的顺序执行。 这对于调试未正确隔离且具有副作用的测试很有用。 使用该选项时,按测试类分组 被保留。

debug_mode 指定 :setting:`DEBUG` 设置在运行测试之前应该设置为什么。

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

tags 可用于指定一组 标签用于过滤测试 。 可与 exclude_tags 组合使用。

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

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

test_name_patterns 可用于指定一组模式,用于按名称过滤测试方法和类。

如果 pdbTrue,则每次测试错误或失败时都会生成调试器(pdbipdb)。

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

如果 enable_faulthandlerTrue,则将启用 faulthandler

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

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

您的测试运行程序还可以定义其他命令行选项。 创建或覆盖 add_arguments(cls, parser) 类方法并通过在方法内部调用 parser.add_argument() 添加自定义参数,以便 :djadmin:`test` 命令能够使用这些论据。

3.1 版新功能: 添加了 buffer 参数。

3.2 版新功能: 添加了 enable_faulthandlertiming 参数。

属性

DiscoverRunner.test_suite
用于构建测试套件的类。 默认设置为 unittest.TestSuite。 如果您希望实现不同的逻辑来收集测试,这可以被覆盖。
DiscoverRunner.test_runner
这是用于执行单个测试和格式化结果的低级测试运行器的类。 默认设置为 unittest.TextTestRunner。 尽管命名约定不幸相似,但这与 DiscoverRunner 不是同一类型的类,后者涵盖了更广泛的职责。 您可以覆盖此属性以修改运行和报告测试的方式。
DiscoverRunner.test_loader
这是加载测试的类,无论是来自 TestCases 还是模块或其他,并将它们捆绑到测试套件中以供运行程序执行。 默认设置为 unittest.defaultTestLoader。 如果您的测试将以不寻常的方式加载,您可以覆盖此属性。


方法

DiscoverRunner.run_tests(test_labels, extra_tests=None, **kwargs)

运行测试套件。

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

extra_tests 是要添加到测试运行器执行的套件中的额外 TestCase 实例列表。 除了在 test_labels 中列出的模块中发现的测试之外,还会运行这些额外的测试。

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

classmethod DiscoverRunner.add_arguments(parser)
覆盖此类方法以添加 :djadmin:`test` 管理命令接受的自定义参数。 有关向解析器添加参数的详细信息,请参阅 argparse.ArgumentParser.add_argument()
DiscoverRunner.setup_test_environment(**kwargs)
通过调用 setup_test_environment() 并将 :setting:`DEBUG` 设置为 self.debug_mode(默认为 False)来设置测试环境。
DiscoverRunner.build_suite(test_labels=None, extra_tests=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 匹配的所有文件中搜索测试(见上文)。

extra_tests 是要添加到测试运行器执行的套件中的额外 TestCase 实例列表。 除了在 test_labels 中列出的模块中发现的测试之外,还会运行这些额外的测试。

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

DiscoverRunner.setup_databases(**kwargs)
通过调用 setup_databases() 创建测试数据库。
DiscoverRunner.run_checks(databases)

在测试 databases 上运行 系统检查

3.1 新功能:增加了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)
根据测试套件和该测试套件的结果计算并返回返回码。


测试实用程序

django.test.utils

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

setup_test_environment(debug=None)

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

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

teardown_test_environment()
执行全局测试后拆卸,例如从模板系统中删除检测工具并恢复正常的电子邮件服务。
setup_databases(verbosity, interactive, *, time_keeper=None, keepdb=False, debug_sql=False, parallel=0, aliases=None, **kwargs)

创建测试数据库。

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

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

在 3.2 版更改:添加了 time_keeper kwarg,并且所有 kwargs 都仅限关键字。

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

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

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

  • 如果 autoclobberFalse,将要求用户同意销毁现有数据库。 如果用户不批准,则调用 sys.exit

  • 如果autoclobber 是True,数据库将在不咨询用户的情况下被销毁。

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

如果您使用的是默认测试运行器,则可以使用 :设置:`序列化 ` 进入 :设置:`测试 ` 字典。

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

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

create_test_db() 有修改 :setting:`DATABASES`:setting:`NAME` 的值以匹配测试数据库名称的副作用。

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

销毁名称为 :setting:`DATABASES`:setting:`NAME` 的值的数据库,并将 :setting:`NAME` 设置为old_database_name 的值。

verbosity 参数的行为与 DiscoverRunner 相同。

如果 keepdb 参数为 True,则与数据库的连接将关闭,但数据库不会被破坏。


与 coverage.py 集成

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

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

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

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

coverage report

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

有关详细说明遗漏行的带注释的 HTML 列表等更多选项,请参阅 coverage.py 文档。