编写和运行测试

本文档分为两个主要部分。首先,我们解释如何使用 Django 编写测试。然后,我们解释如何运行它们。

编写测试

Django 的单元测试使用 Python 标准库模块:unittest。此模块使用基于类的途径定义测试。

以下是一个从 django.test.TestCaseunittest.TestCase 的子类)继承的示例,它在事务中运行每个测试以提供隔离

from django.test import TestCase
from myapp.models import Animal


class AnimalTestCase(TestCase):
    def setUp(self):
        Animal.objects.create(name="lion", sound="roar")
        Animal.objects.create(name="cat", sound="meow")

    def test_animals_can_speak(self):
        """Animals that can speak are correctly identified"""
        lion = Animal.objects.get(name="lion")
        cat = Animal.objects.get(name="cat")
        self.assertEqual(lion.speak(), 'The lion says "roar"')
        self.assertEqual(cat.speak(), 'The cat says "meow"')

当你运行测试时,测试实用程序的默认行为是在任何名称以 test 开头的文件中找到所有测试用例类(即 unittest.TestCase 的子类),自动从这些测试用例类构建一个测试套件,并运行该套件。

有关 unittest 的更多详细信息,请参阅 Python 文档。

测试应该放在哪里?

默认的 startapp 模板会在新应用程序中创建一个 tests.py 文件。如果你只有几个测试,这可能没问题,但随着测试套件的增长,你可能希望将其重新构建为一个测试包,以便将测试拆分为不同的子模块,例如 test_models.pytest_views.pytest_forms.py 等。你可以随意选择你喜欢的组织方案。

另请参阅 使用 Django 测试运行器测试可重用应用程序

警告

如果你的测试依赖于数据库访问,例如创建或查询模型,请务必将测试类创建为 django.test.TestCase 的子类,而不是 unittest.TestCase

使用 unittest.TestCase 可以避免在事务中运行每个测试并刷新数据库的成本,但如果你的测试与数据库交互,它们的执行顺序将影响其行为。这可能导致单元测试在单独运行时通过,但在套件中运行时失败。

运行测试

编写完测试后,使用项目的 manage.py 实用程序的 test 命令运行它们

$ ./manage.py test

测试发现基于 unittest 模块的 内置测试发现。默认情况下,这将在当前工作目录下的任何名为 test*.py 的文件中发现测试。

你可以通过向 ./manage.py test 提供任意数量的“测试标签”来指定要运行的特定测试。每个测试标签可以是到包、模块、TestCase 子类或测试方法的完整 Python 点分路径。例如

# Run all the tests in the animals.tests module
$ ./manage.py test animals.tests

# Run all the tests found within the 'animals' package
$ ./manage.py test animals

# Run just one test case class
$ ./manage.py test animals.tests.AnimalTestCase

# Run just one test method
$ ./manage.py test animals.tests.AnimalTestCase.test_animals_can_speak

你还可以提供一个目录路径,以发现该目录下的测试

$ ./manage.py test animals/

如果测试文件与 test*.py 模式的命名不同,你可以使用 -p(或 --pattern)选项指定自定义文件名模式匹配

$ ./manage.py test --pattern="tests_*.py"

如果在测试运行时按 Ctrl-C,测试运行器将等待当前运行的测试完成,然后正常退出。在正常退出期间,测试运行器将输出任何测试失败的详细信息,报告运行了多少个测试以及遇到了多少个错误和失败,并像往常一样销毁任何测试数据库。因此,如果你忘记传递 --failfast 选项,注意到一些测试意外失败并希望在不等待完整的测试运行完成的情况下获取失败的详细信息,那么按 Ctrl-C 会非常有用。

如果你不想等待当前运行的测试完成,你可以再次按 Ctrl-C,测试运行将立即停止,但不会正常停止。不会报告中断之前运行的测试的任何详细信息,并且运行创建的任何测试数据库都不会被销毁。

启用警告运行测试

最好在启用 Python 警告的情况下运行测试: python -Wa manage.py test-Wa 标志告诉 Python 显示弃用警告。Django 与许多其他 Python 库一样,使用这些警告来标记功能何时消失。它还可能标记你的代码中严格来说没有错但可以从更好的实现中受益的区域。

测试数据库

需要数据库的测试(即模型测试)将不会使用你的“真实”(生产)数据库。为测试创建了单独的空白数据库。

无论测试是否通过或失败,在执行完所有测试后都会销毁测试数据库。

你可以使用 test --keepdb 选项来防止销毁测试数据库。这将在运行之间保留测试数据库。如果数据库不存在,它将首先被创建。任何迁移也将被应用以使其保持最新状态。

如前一节所述,如果测试运行被强制中断,测试数据库可能不会被销毁。在下一次运行时,系统会询问您是否要重新使用或销毁数据库。使用 test --noinput 选项来禁止该提示并自动销毁数据库。例如,在持续集成服务器上运行测试时,测试可能会因超时而中断,此时此选项非常有用。

默认测试数据库名称是通过将 test_ 前置到 NAMEDATABASES 中的每个值来创建的。使用 SQLite 时,测试默认会使用内存数据库(即,数据库将在内存中创建,完全绕过文件系统!)。TEST 字典在 DATABASES 中提供许多设置来配置您的测试数据库。例如,如果您想使用不同的数据库名称,请在 TEST 字典中指定 NAME,用于 DATABASES 中的任何给定数据库。

在 PostgreSQL 上,USER 还需要对内置 postgres 数据库具有读取访问权限。

除了使用单独的数据库外,测试运行器还会使用您在设置文件中拥有的所有相同数据库设置:ENGINEUSERHOST 等。测试数据库是由 USER 指定的用户创建的,因此您需要确保给定的用户帐户具有在系统上创建新数据库的足够权限。

要对测试数据库的字符编码进行细粒度控制,请使用 CHARSET TEST 选项。如果您使用 MySQL,还可以使用 COLLATION 选项来控制测试数据库使用的特定校对规则。有关这些和其他高级设置的详细信息,请参阅 设置文档

如果在 SQLite 中使用 SQLite 内存数据库,则 共享缓存 已启用,因此您可以编写具有在各个线程之间共享数据库的能力的测试。

在运行测试时从生产数据库中查找数据?

如果您的代码在编译其模块时尝试访问数据库,则这将在设置测试数据库之前发生,可能会产生意外的结果。例如,如果您在模块级别代码中有一个数据库查询并且存在一个真实数据库,则生产数据可能会污染您的测试。无论如何,在您的代码中进行此类导入时查询数据库都是一个坏主意 - 重写您的代码,使其不执行此操作。

这也适用于 ready() 的自定义实现。

另请参阅

有关 高级多数据库测试主题

执行测试的顺序

为了确保所有 TestCase 代码都以一个干净的数据库开始,Django 测试运行器以下列方式重新排序测试

  • 所有 TestCase 子类首先运行。
  • 然后,所有其他基于 Django 的测试(基于 SimpleTestCase 的测试用例类,包括 TransactionTestCase)运行,在它们之间不保证或强制执行任何特定顺序。
  • 然后运行任何其他 unittest.TestCase 测试(包括 doctest),这些测试可能会更改数据库,而不会将其还原到其原始状态。

注意

测试的新顺序可能会显示出对测试用例顺序的意外依赖。对于依赖给定 TransactionTestCase 测试在数据库中留下的状态的 doctest 来说,就是这种情况,它们必须更新为能够独立运行。

注意

在加载测试时检测到的故障会在上述所有故障之前进行排序,以便更快地提供反馈。这包括找不到或由于语法错误而无法加载的测试模块等内容。

您可以使用 test --shuffle--reverse 选项在组内随机化和/或反转执行顺序。这有助于确保您的测试彼此独立。

回滚模拟

在迁移中加载的任何初始数据都只能在 TestCase 测试中使用,而不能在 TransactionTestCase 测试中使用,此外,只能在支持事务的后端中使用(最重要的例外是 MyISAM)。对于依赖 TransactionTestCase 的测试,例如 LiveServerTestCaseStaticLiveServerTestCase,也是如此。

Django 可以通过在 TestCaseTransactionTestCase 的正文中将 serialized_rollback 选项设置为 True 来为您逐个测试用例重新加载该数据,但请注意,这会使该测试套件的速度降低约 3 倍。

第三方应用或针对 MyISAM 开发的应用需要设置此项;然而,一般来说,您应该针对事务数据库开发自己的项目,并对大多数测试使用 TestCase,因此不需要此设置。

初始序列化通常非常快,但如果你希望从这个过程中排除一些应用(并略微加快测试运行速度),你可以将这些应用添加到 TEST_NON_SERIALIZED_APPS 中。

为了防止序列化数据被加载两次,设置 serialized_rollback=True 会在刷新测试数据库时禁用 post_migrate 信号。

其他测试条件

无论配置文件中 DEBUG 设置的值是什么,所有 Django 测试都以 DEBUG=False 运行。这是为了确保你代码的观察输出与在生产环境中看到的输出相匹配。

缓存不会在每次测试后清除,并且运行 manage.py test fooapp 可以将测试中的数据插入到实时系统的缓存中,如果你在生产环境中运行测试,因为与数据库不同,不会使用单独的“测试缓存”。这种行为 可能会改变 在未来。

理解测试输出

当你运行测试时,你会看到一些消息,因为测试运行器正在准备自己。你可以使用命令行上的 verbosity 选项来控制这些消息的详细程度

Creating test database...
Creating table myapp_animal
Creating table myapp_mineral

这告诉你测试运行器正在创建一个测试数据库,如前一节所述。

一旦创建了测试数据库,Django 就会运行你的测试。如果一切顺利,你会看到类似这样的东西

----------------------------------------------------------------------
Ran 22 tests in 0.221s

OK

但是,如果有测试失败,你会看到有关哪些测试失败的完整详细信息

======================================================================
FAIL: test_was_published_recently_with_future_poll (polls.tests.PollMethodTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/dev/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_poll
    self.assertIs(future_poll.was_published_recently(), False)
AssertionError: True is not False

----------------------------------------------------------------------
Ran 1 test in 0.003s

FAILED (failures=1)

对这个错误输出的完整解释超出了本文档的范围,但它非常直观。你可以查阅 Python 的 unittest 库的文档以获取详细信息。

请注意,对于任何数量的失败测试(无论失败是由错误、失败的断言还是意外的成功引起的),测试运行器脚本的返回代码都是 1。如果所有测试都通过,则返回代码为 0。如果你在 shell 脚本中使用测试运行器脚本并且需要在该级别测试成功或失败,此功能非常有用。

加快测试速度

并行运行测试

只要您的测试正确隔离,您就可以并行运行它们,以在多核硬件上获得速度提升。请参阅 test --parallel

密码哈希

默认密码哈希器设计上相当慢。如果您在测试中对许多用户进行身份验证,您可能希望使用自定义设置文件并将 PASSWORD_HASHERS 设置为更快的哈希算法

PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.MD5PasswordHasher",
]

不要忘记在 PASSWORD_HASHERS 中包括固定装置中使用的任何哈希算法(如果有)。

保留测试数据库

test --keepdb 选项在测试运行之间保留测试数据库。它跳过创建和销毁操作,这可以极大地减少运行测试的时间。

避免对媒体文件进行磁盘访问

Django 4.2 中的新增功能。

InMemoryStorage 是一种防止对媒体文件进行磁盘访问的便捷方式。所有数据都保存在内存中,然后在测试运行后将其丢弃。

返回顶部