编写和运行测试¶
本文档分为两个主要部分。首先,我们解释如何使用 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 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
数据库具有读取权限。
除了使用单独的数据库外,测试运行器将使用设置文件中相同的数据库设置:ENGINE
、USER
、HOST
等。测试数据库由USER
指定的用户创建,因此你需要确保给定的用户帐户具有在系统上创建新数据库的足够权限。
要对测试数据库的字符编码进行细粒度控制,请使用CHARSET
测试选项。如果使用 MySQL,还可以使用COLLATION
选项来控制测试数据库使用的特定排序规则。有关这些和其他高级设置的详细信息,请参阅设置文档。
如果使用 SQLite 内存数据库,则 SQLite 的共享缓存已启用,因此您可以编写能够在线程之间共享数据库的测试。
在运行测试时查找生产数据库中的数据?
如果代码在模块编译时尝试访问数据库,则这将在设置测试数据库之前发生,并可能产生意外的结果。例如,如果模块级代码中存在数据库查询,并且存在真实数据库,则生产数据可能会污染您的测试。无论如何,在代码中进行此类导入时数据库查询都是一个坏主意 - 重写您的代码,使其不执行此操作。
这也适用于ready()
的自定义实现。
另请参阅
有关高级多数据库测试主题。
测试执行顺序¶
为了保证所有TestCase
代码都以干净的数据库开始,Django 测试运行器会以以下方式重新排序测试
首先运行所有
TestCase
子类。然后,运行所有其他基于 Django 的测试(基于
SimpleTestCase
的测试用例类,包括TransactionTestCase
),不保证也不强制执行它们之间的任何特定顺序。然后运行任何其他
unittest.TestCase
测试(包括 doctest),这些测试可能会更改数据库而没有将其恢复到其原始状态。
注意
测试的新顺序可能会揭示对测试用例顺序的意外依赖关系。对于依赖于给定TransactionTestCase
测试在数据库中留下的状态的 doctest,情况就是如此,必须更新它们才能独立运行。
注意
在加载测试时检测到的故障在上述所有故障之前排序,以便更快地反馈。这包括找不到或由于语法错误而无法加载的测试模块等。
您可以使用test --shuffle
和--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
库的文档以获取详细信息。
请注意,对于任何数量的失败测试(无论是由于错误、失败的断言还是意外成功导致的失败),测试运行器脚本的返回代码都是 1。如果所有测试都通过,则返回代码为 0。如果您在 shell 脚本中使用测试运行器脚本,并且需要在该级别测试成功或失败,此功能非常有用。
加快测试速度¶
并行运行测试¶
只要您的测试正确隔离,就可以并行运行它们,以在多核硬件上获得加速。请参阅test --parallel
。
密码哈希¶
默认的密码哈希算法在设计上相当慢。如果您在测试中对许多用户进行身份验证,则可能需要使用自定义设置文件并将PASSWORD_HASHERS
设置设置为更快的哈希算法
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.MD5PasswordHasher",
]
如果存在任何修复程序中使用的哈希算法,请不要忘记将其也包含在PASSWORD_HASHERS
中。
保留测试数据库¶
test --keepdb
选项在测试运行之间保留测试数据库。它跳过创建和销毁操作,这可以大大减少运行测试的时间。
避免媒体文件的磁盘访问¶
使用InMemoryStorage
是一种方便的方法,可以避免访问媒体文件所在的磁盘。所有数据都保存在内存中,并在测试运行结束后被丢弃。