Django4 中文入门教程 Django4.0 测试-编写并运行测试

2024-02-25 开发教程 Django4 中文入门教程 匿名 4

编写测试

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​ 的子类),从这些测试用例中自动构建一个测试套件,然后运行该套件。

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

如果您的测试依赖于数据库访问,例如创建或查询模型,请确保将您的测试类创建为 ​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
$ ./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​,测试运行将立即停止,但不会优雅地停止。不会报告中断前运行的测试细节,也不会销毁运行中创建的任何测试数据库。

测试数据库

需要数据库的测试(即模型测试)将不会使用“实际”(生产)数据库。 将为测试创建单独的空白数据库。
无论测试是通过还是失败,当所有测试执行完毕后,测试数据库都会被销毁。
你可以通过使用 ​test --keepdb​ 选项来防止测试数据库被破坏。 这将在两次运行之间保留测试数据库。 如果数据库不存在,将首先创建它。 任何迁移都将被应用,以使其保持最新状态。
如上一节所述,如果测试运行被强行中断,测试数据库可能不会被销毁。在下一次运行时,你会被问到是要重新使用还是销毁数据库。使用 ​test --noinput​ 选项禁止显示该提示并自动销毁数据库。 例如,在持续集成服务器上运行测试时这很有用,该测试可能会因超时而中断。
默认的测试数据库名称是通过在 DATABASES 中每个 ​NAME ​的值前加上 ​test_​ 来创建的。当使用 SQLite时,默认情况下测试将使用内存数据库(即数据库将在内存中创建,完全绕开文件系统!)。DATABASES 中的 ​TEST ​字典提供了许多设置来配置你的测试数据库。例如,如果你想使用不同的数据库名称,给 DATABASES 中的每个数据库在 ​TEST ​字典中指定 ​NAME​。
在 PostgreSQL 上,​USER ​也需要对内置的 postgres 数据库进行读取访问。
除了使用单独的数据库外,测试运行器还将使用你在配置文件中的所有相同的数据库设置: ​ENGINE​、​USER​、​HOST ​等。测试数据库是由 ​USER ​指定的用户创建的,所以你需要确保给定的用户账户有足够的权限在系统上创建一个新的数据库。
为了对测试数据库的字符编码进行精细控制,请使用 ​CHARSET ​TEST 选项。如果你使用的是 MySQL,你也可以使用 ​COLLATION ​选项来控制测试数据库使用的特定字符序。
如果使用 SQLite 内存数据库,启用了 共享缓存,你就可以编写线程之间共享数据库的测试。

执行测试的顺序

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

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

回滚模拟

任何在迁移中加载的初始数据将只能在 ​TestCase ​测试中使用,而不能在 ​TransactionTestCase ​测试中使用,此外,只有在支持事务的后端(最重要的例外是 MyISAM)上才能使用。对于依赖 ​TransactionTestCase ​的测试也是如此,比如 ​LiveServerTestCase ​和 ​StaticLiveServerTestCase​。
Django 可以通过在 ​TestCase ​或 ​TransactionTestCase ​中设置 ​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)

对这个错误输出的完整解释超出了本文的范围,但它非常直观。
请注意,对于任何数量的失败和错误测试,​test-runner​ 脚本的返回码均为 1。 如果所有测试均通过,则返回码为 0。如果你在 shell 脚本中使用 ​test-runner​ 脚本,并且需要在该级别上测试成功或失败,则此功能很有用。

加快测试

并行运行测试

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

密码哈希

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

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

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

保留测试数据库

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