本文档主要分为两部分。首先,我们介绍如何利用 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.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
,测试运行将立即停止,但不会优雅地停止。不会报告中断前运行的测试细节,也不会销毁运行中创建的任何测试数据库。
在启用警告的情况下进行测试
启用 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
数据库进行读取访问。
除了使用单独的数据库外,测试运行器还将使用你在配置文件中的所有相同的数据库设置: ENGINE
、USER
、HOST
等。测试数据库是由 USER
指定的用户创建的,所以你需要确保给定的用户账户有足够的权限在系统上创建一个新的数据库。
为了对测试数据库的字符编码进行精细控制,请使用 CHARSET
TEST 选项。如果你使用的是 MySQL,你也可以使用 COLLATION
选项来控制测试数据库使用的特定字符序。请参阅 配置文档 了解这些和其他高级设置的细节。
如果使用 SQLite 内存数据库,启用了 共享缓存,你就可以编写线程之间共享数据库的测试。
运行测试时从生产数据库中查找数据?
如果你的代码在编译模块时试图访问数据库,这将在测试数据库建立 之前 发生,可能会产生意想不到的结果。例如,如果你在模块级代码中进行数据库查询,并且存在真实的数据库,则生产数据可能会污染你的测试。 无论如何,在代码中都包含这样的导入时数据库查询是一个坏主意——重写代码,使其不会执行此操作。
这也适用于 ready()
的自定义实现。
参见
为了保证所有的 TestCase
代码都从干净的数据库开始,Django 测试运行器以如下方式重新排序测试:
TestCase
的子类首先运行。SimpleTestCase
的测试用例,包括 TransactionTestCase
)都会被运行,它们之间不保证也不强制执行特定的顺序。unittest.TestCase
测试(包括 doctests),这些测试可能会改变数据库而不将其恢复到原始状态。注解
新的测试顺序可能会意外的揭示出测试用例对顺序的依赖性。在 doctests 依赖于数据库中给定的 TransactionTestCase
测试的情况下,必须更新它们才能独立运行。
你可以使用 test --reverse
选项反转组内的执行顺序。 这可以帮助确保你的测试彼此独立。
任何在迁移中加载的初始数据将只能在 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)
对这个错误输出的完整解释超出了本文的范围,但它非常直观。你可以参考 Python 的 unittest
库的文档以了解详细信息。
请注意,对于任何数量的失败和错误测试,test-runner 脚本的返回码均为 1。 如果所有测试均通过,则返回码为 0。如果你在 shell 脚本中使用 test-runner 脚本,并且需要在该级别上测试成功或失败,则此功能很有用。
只要测试正确隔离,你就可以并行运行它们以加快多核硬件的运行速度。 参见 test --parallel
.
默认密码哈希器在设计上相当慢。 如果要在测试中对许多用户进行身份验证,则可能需要使用自定义设置文件,并将 PASSWORD_HASHERS
设置为更快的哈希算法:
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.MD5PasswordHasher',
]
不要忘记在 PASSWORD_HASHERS
中包含在辅助工具中使用的任何哈希算法,如果有的话。
test --keepdb
选项在两次测试运行之间保留测试数据库。 它跳过了创建和销毁操作,这可以大大减少运行测试的时间。
12月 07, 2021