进阶测试主题

请求工厂

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)

AsyncRequestFactory

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

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

测试基于类的视图

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

你可以直接测试 get_context_data() 方法,首先实例化视图,然后向 setup() 传递一个 request,然后再进行测试代码。

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/', HTTP_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 上——但这是因为它们实际上是同一个数据库,而不是因为两个数据库之间有数据复制。

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

默认情况下,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, **kwargs)

DiscoverRunner 将在任何符合 pattern 的文件中搜索测试。

top_level 可以用来指定包含顶级 Python 模块的目录。通常 Django 会自动计算出这个目录,所以不需要指定这个选项。如果指定了这个选项,一般来说,它应该是包含你的 manage.py 文件的目录。

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

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

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

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

如果 reverseTrue,测试用例将以相反的顺序执行。这对于调试没有适当隔离和有副作用的测试可能很有用。使用这个选项时,按测试类分组 将被保留。

debug_mode 指定 DEBUG 设置在运行测试之前应该设置成什么。

parallel 指定进程数。如果 parallel 大于 1,则测试套件会在 parallel (平行)进程下运行。如果测试用例比配置的进程少,Django 将会相应地减少进程数量。每一个进程都有自己的数据库。该选择要求使用第三方包 tblib 来正确地显示回溯信息。

tags 用于指定一系列 测试标签。可以与 exclude_tags 结合使用。

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

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

test_name_patterns 可以用来指定一套模式,通过名称过滤测试方法和类。

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

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

如果 enable_faulthandlerTrue,那么 faulthandler 将被启用。

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

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

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

New in Django 3.1:

增加了 buffer 参数。

New in Django 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)

重写这个类方法来添加 test 管理命令接受的自定义参数。参见 argparse.ArgumentParser.add_argument() 了解关于向解析器添加参数的详细信息。

DiscoverRunner.setup_test_environment(**kwargs)

通过调用 setup_test_environment() 和设置 DEBUGself.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 上运行 系统检查

New in Django 3.1:

增加了 databases 参数。

DiscoverRunner.run_suite(suite, **kwargs)

运行测试套件。

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

DiscoverRunner.get_test_runner_kwargs()

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

DiscoverRunner.teardown_databases(old_config, **kwargs)

通过调用 trapdown_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,则 DEBUG 配置更新为其值。

teardown_test_environment()

进行全局性的测试后拆解,如从模板系统中删除仪器设备,恢复正常的邮件服务。

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

创建测试数据库。

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

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

Changed in Django 3.2:

增加了 time_keeper 关键字,并且所有的 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

verbosityrun_tests() 中的行为相同。

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

  • 如果 autoclobberFalse,将要求用户批准销毁现有数据库。如果用户不同意,则调用 sys.exit
  • 如果 autoclobber 为 True,数据库将在不与用户协商的情况下被销毁。

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

如果你使用的是默认的测试运行器,你可以通过 TEST 条目来控制。

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

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

create_test_db() 的副作用是修改 DATABASES 中的 NAME 的值,使其与测试数据库的名称相匹配。

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

销毁名称为 DATABASESNAME 值的数据库,并将 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 的文档。