编写并运行测试

本文档主要分为两部分。首先,我们介绍如何利用 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="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 避免了在事务中运行每个测试并刷新数据库的成本,但如果你的测试与数据库交互,它们的行为将根据测试运行器执行它们的顺序而变化。这可能导致单元测试在单独运行时通过,但在套件中运行时失败。

运行测试

Once you've written tests, run them using the test command of your project's manage.py utility:

$ ./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.

You can specify particular tests to run by supplying any number of "test labels" to ./manage.py test. Each test label can be a full Python dotted path to a package, module, TestCase subclass, or test method. For instance:

# 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
$ ./manage.py test animals.tests.AnimalTestCase

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

You can also provide a path to a directory to discover tests below that directory:

$ ./manage.py test animals/

You can specify a custom filename pattern match using the -p (or --pattern) option, if your test files are named differently from the test*.py 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 内存数据库,启用了 共享缓存,你就可以编写线程之间共享数据库的测试。

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

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

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

执行测试的顺序

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

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

备注

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

备注

Failures detected when loading tests are ordered before all of the above for quicker feedback. This includes things like test modules that couldn't be found or that couldn't be loaded due to syntax errors.

You may randomize and/or reverse the execution order inside groups using the test --shuffle and --reverse options. This can help with ensuring your tests are independent from each other.

回滚模拟

任何在迁移中加载的初始数据将只能在 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 运行。这是为了确保你的代码观察到的输出与生产环境下的输出一致。

Caches are not cleared after each test, and running manage.py test fooapp can insert data from the tests into the cache of a live system if you run your tests in production because, unlike databases, a separate "test cache" is not used. This behavior may change in the future.

了解测试输出

When you run your tests, you'll see a number of messages as the test runner prepares itself. You can control the level of detail of these messages with the verbosity option on the command line:

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

这告诉你测试运行程序正在创建测试数据库,如上一节所述。

Once the test database has been created, Django will run your tests. If everything goes well, you'll see something like this:

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

OK

If there are test failures, however, you'll see full details about which tests failed:

======================================================================
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 库的文档以了解详细信息。

Note that the return code for the test-runner script is 1 for any number of failed tests (whether the failure was caused by an error, a failed assertion, or an unexpected success). If all the tests pass, the return code is 0. This feature is useful if you're using the test-runner script in a shell script and need to test for success or failure at that level.

Changed in Django 4.1:

In older versions, the return code was 0 for unexpected successes.

加快测试

并行运行测试

只要测试正确隔离,你就可以并行运行它们以加快多核硬件的运行速度。 参见 test --parallel.

密码哈希

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

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

不要忘记在 PASSWORD_HASHERS 中包含在辅助工具中使用的任何哈希算法,如果有的话。

保留测试数据库

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

Avoiding disk access for media files

New in Django 4.2.

The InMemoryStorage is a convenient way to prevent disk access for media files. All data is kept in memory, then it gets discarded after tests run.