编写并运行测试

本文档主要分为两部分。首先,我们介绍如何利用 Django 编写测试。接着,我们介绍如何运行它们。

编写测试

Django 的单元测试采用 Python 的标准模块: unittest。该模块以类的形式定义测试。

下面是一个例子,它是 django.test.TestCase 的子类,同时父类也是 unittest.TestCase 的子类,在事务内部运行每个测试以提供隔离:

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


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

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

        cat.sound = "hiss"
        cat.save(update_fields=["sound"])
        self.assertEqual(cat.speak(), 'The cat says "hiss"')

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

更多关于 unittest 的细节,参考 Python 文档。

测试代码应该放在哪?

默认的 startapp 会在新的应用程序中创建一个 tests.py 文件。如果你只有几个测试,这可能是好的,但随着你的测试套件的增长,你可能会想把它重组为一个测试包,这样你就可以把你的测试分成不同的子模块,如 test_models.pytest_views.pytest_forms.py 等。你可以自由选择任何你喜欢的组织方案。

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

Warning

如果你的测试依赖数据库连接,比如创建或查询模型,请确保继承 django.test.TestCase 实现你的测试类,而不是 unittest.TestCase

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

运行测试

一旦你编写了测试,就可以使用项目的 manage.py 工具的 test 命令来运行它们:

$ ./manage.py test

Test discovery is based on the unittest module's built-in test discovery. By default, this will discover tests in any file named test*.py under the current working directory.

你可以通过向 ./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 选项禁止显示该提示并自动销毁数据库。 例如,在持续集成服务器上运行测试时这很有用,该测试可能会因超时而中断。

默认的测试数据库名称是通过在 DATABASES 中每个 NAME 的值前加上 test_ 来创建的。当使用 SQLite时,默认情况下测试将使用内存数据库(即数据库将在内存中创建,完全绕开文件系统!)。DATABASES 中的 TEST 字典提供了许多设置来配置你的测试数据库。例如,如果你想使用不同的数据库名称,给 DATABASES 中的每个数据库在 TEST 字典中指定 NAME

在 PostgreSQL 上,USER 也需要对内置的 postgres 数据库进行读取访问。

除了使用单独的数据库外,测试运行器还将使用你在配置文件中的所有相同的数据库设置: ENGINEUSERHOST 等。测试数据库是由 USER 指定的用户创建的,所以你需要确保给定的用户账户有足够的权限在系统上创建一个新的数据库。

为了对测试数据库的字符编码进行精细控制,请使用 CHARSET TEST 选项。如果你使用的是 MySQL,你也可以使用 COLLATION 选项来控制测试数据库使用的特定字符序。请参阅 配置文档 了解这些和其他高级设置的细节。

如果使用 SQLite 内存数据库,启用了 共享缓存,你就可以编写线程之间共享数据库的测试。

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

如果你的代码在编译模块时试图访问数据库,这将在测试数据库建立 之前 发生,可能会产生意想不到的结果。例如,如果你在模块级代码中进行数据库查询,并且存在真实的数据库,则生产数据可能会污染你的测试。 无论如何,在代码中都包含这样的导入时数据库查询是一个坏主意——重写代码,使其不会执行此操作。

This also applies to customized implementations of ready().

执行测试的顺序

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

  • 所有 TestCase 的子类首先运行。

  • 然后,所有其他基于 Django 的测试(基于 SimpleTestCase 的测试用例类,包括 TransactionTestCase)会按照没有特定顺序保证或强制执行的方式运行。

  • 然后运行任何其他的 unittest.TestCase 测试(包括 doctests),这些测试可能会改变数据库而不将其恢复到原始状态。

Note

新的测试顺序可能会意外的揭示出测试用例对顺序的依赖性。在 doctests 依赖于数据库中给定的 TransactionTestCase 测试的情况下,必须更新它们才能独立运行。

Note

在加载测试时检测到的失败会排在上述所有测试之前,以便更快地获得反馈。这包括找不到的测试模块或由于语法错误而无法加载的模块等问题。

你可以使用 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 选项在两次测试运行之间保留测试数据库。 它跳过了创建和销毁操作,这可以大大减少运行测试的时间。

避免磁盘访问的媒体文件处理

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

Using setUpTestData to create test data

Moving common test data to TestCase.setUpTestData() will reduce the number of times your tests need to create the data. Using TestCase.setUpTestData() will create test data once per class. While TestCase.setUp() will create the test data once per test.