Django4.0 测试工具-测试用例特性
默认测试客户端
SimpleTestCase.client
django.test.*TestCase
实例中的每个测试用例都可以访问一个 Django 测试客户端的实例。这个客户端可以用 self.client
来访问。这个客户端在每个测试中都会被重新创建,所以你不必担心状态(比如 cookie)会从一个测试转移到另一个测试中。
这意味着,不必每个测试中实例化一个 Client:
import unittest
from django.test import Client
class SimpleTest(unittest.TestCase):
def test_details(self):
client = Client()
response = client.get('/customer/details/')
self.assertEqual(response.status_code, 200)
def test_index(self):
client = Client()
response = client.get('/customer/index/')
self.assertEqual(response.status_code, 200)
你也可以引用 self.client
,像这样:
from django.test import TestCase
class SimpleTest(TestCase):
def test_details(self):
response = self.client.get('/customer/details/')
self.assertEqual(response.status_code, 200)
def test_index(self):
response = self.client.get('/customer/index/')
self.assertEqual(response.status_code, 200)
自定义测试客户端
SimpleTestCase.client_class
如果你想使用不同的 Client 类(例如,一个具有自定义行为的子类),使用 client_class
类属性:
from django.test import Client, TestCase
class MyTestClient(Client):
# Specialized methods for your environment
...
class MyTest(TestCase):
client_class = MyTestClient
def test_my_stuff(self):
# Here self.client is an instance of MyTestClient...
call_some_test_code()
辅助工具加载
TransactionTestCase.fixtures
如果数据库中没有任何数据,那么数据库支持的网站的测试用例就没什么用了。测试使用ORM创建对象更易读,也更易维护,例如在 TestCase.setUpTestData()
中。但是,你也可以使用辅助工具。
辅助工具是 Django 知道如何导入数据库的数据集合。例如,如果你的网站有用户账户,你可能会设置一个假用户账户的辅助工具,以便在测试时填充你的数据库。
创建辅助工具的最直接方法是使用 manage.py dumpdata
命令。这假定你已经在你的数据库中拥有一些数据。
一旦你创建了一个辅助工具,并把它放在你的 INSTALLED_APPS
中的 fixtures
目录下,你就可以通过在你的 django.test.TestCase
子类上指定一个 fixtures
类属性来在你的单元测试中使用它。
from django.test import TestCase
from myapp.models import Animal
class AnimalTestCase(TestCase):
fixtures = ['mammals.json', 'birds']
def setUp(self):
# Test definitions as before.
call_setup_methods()
def test_fluffy_animals(self):
# A test that uses the fixtures.
call_some_test_code()
具体来说,将发生以下情况:
- 在每次测试开始时,在
setUp()
运行之前,Django 会对数据库进行刷新,将数据库直接返回到 migrate
被调用后的状态。 - 然后,所有命名的辅助工具都会被安装。在这个例子中,Django 将安装任何名为
mammals
的 JSON 辅助工具,然后是任何名为 birds
的辅助工具。
出于性能方面的考虑, TestCase
在 setUpTestData()
之前为整个测试类加载一次辅助工具,而不是在每次测试之前加载,并且它在每次测试之前使用事务来清理数据库。在任何情况下,你都可以确定一个测试的结果不会受到另一个测试或测试执行顺序的影响。
默认情况下,辅助工具只被加载到 default
数据库中。如果你使用多个数据库并且设置了 TransactionTestCase.databases
,辅助工具将被加载到所有指定的数据库中。
URLconf配置
如果你的应用程序提供了视图,你可能希望包含使用测试客户端来行使这些视图的测试。然而,最终用户可以自由地在他们选择的任何 URL 上部署应用程序中的视图。这意味着你的测试不能依赖于你的视图将在特定的 URL 上可用这一事实。用 @override_settings(ROOT_URLCONF=...)
来装饰你的测试类或测试方法的 URLconf 配置。
多数据库支持
TransactionTestCase.databases
Django 设置了一个测试数据库,对应于你设置中的 DATABASES
定义的并且至少有一个测试引用了 databases
的每个数据库。
然而,运行一个 Django TestCase
所花费的时间很大一部分是被调用 flush
所消耗的,它确保了你在每次测试运行开始时有一个干净的数据库。如果你有多个数据库,就需要多次刷新(每个数据库一个),这可能是一个耗时的活动——特别是当你的测试不需要测试多数据库活动时。
作为一种优化,Django 只在每次测试运行开始时刷新 default
数据库。如果你的设置包含多个数据库,并且你的测试要求每个数据库都是干净的,你可以使用测试套件上的 databases
属性来请求额外的数据库被刷新。
例如:
class TestMyViews(TransactionTestCase):
databases = {'default', 'other'}
def test_index_page_view(self):
call_some_test_code()
这个测试用例将在运行 test_index_page_view
之前刷新 default
和 other
测试数据库。你也可以使用 '__all__'
来指定所有的测试数据库必须被刷新。
databases
标志也控制 TransactionTestCase.fixtures
被加载到哪些数据库。默认情况下,辅助工具只被加载到 default
数据库中。
对不在 databases
中的数据库的查询将给出断言错误,以防止测试之间的状态泄露。
TestCase.databases
默认情况下,在 TestCase
期间,仅将 default
数据库包装在事务中,并且尝试查询其他数据库将导致断言错误,以防止测试之间的状态泄漏。
在测试类上使用 databases
类属性来请求对非 default
数据库进行事务包装。
例如:
class OtherDBTests(TestCase):
databases = {'other'}
def test_other_db_query(self):
...
这个测试只允许对 other
数据库进行查询。就像 SimpleTestCase.databases
和 TransactionTestCase.databases
一样,'__all__'
常量可以用来指定测试应该允许对所有数据库进行查询。
覆盖配置
警告
使用下面的函数可以临时改变测试中的设置值。不要直接操作 django.conf.settings
,因为 Django 不会在这种操作后恢复原始值。
SimpleTestCase.settings()
为了测试的目的,经常需要临时改变一个设置,并在运行测试代码后恢复到原始值。对于这个用例,Django 提供了一个标准的 Python 上下文管理器(见 PEP 343),叫做 settings()
,可以这样使用:
from django.test import TestCase
class LoginTestCase(TestCase):
def test_login(self):
# First check for the default behavior
response = self.client.get('/sekrit/')
self.assertRedirects(response, '/accounts/login/?next=/sekrit/')
# Then override the LOGIN_URL setting
with self.settings(LOGIN_URL='/other/login/'):
response = self.client.get('/sekrit/')
self.assertRedirects(response, '/other/login/?next=/sekrit/')
此示例将覆盖 with
块中代码的 LOGIN_URL
设置,然后将其值重置为先前的状态。
SimpleTestCase.modify_settings()
重新定义包含一系列值的设置可能会很麻烦。在实践中,添加或删除值通常是足够的。Django 提供了 modify_settings()
上下文管理器,以方便更改设置:
from django.test import TestCase
class MiddlewareTestCase(TestCase):
def test_cache_middleware(self):
with self.modify_settings(MIDDLEWARE={
'append': 'django.middleware.cache.FetchFromCacheMiddleware',
'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
'remove': [
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
],
}):
response = self.client.get('/')
# ...
对于每个操作,你可以提供一个值的列表或一个字符串。当值已经存在于列表中时,append
和 prepend
没有效果;当值不存在时,remove
也没有效果。
override_settings(**kwargs)
如果你想覆盖一个测试方法的设置,Django 提供了 override_settings()
装饰器。它的用法是这样的:
from django.test import TestCase, override_settings
class LoginTestCase(TestCase):
@override_settings(LOGIN_URL='/other/login/')
def test_login(self):
response = self.client.get('/sekrit/')
self.assertRedirects(response, '/other/login/?next=/sekrit/')
装饰器也可以应用于 TestCase
类:
from django.test import TestCase, override_settings
@override_settings(LOGIN_URL='/other/login/')
class LoginTestCase(TestCase):
def test_login(self):
response = self.client.get('/sekrit/')
self.assertRedirects(response, '/other/login/?next=/sekrit/')
modify_settings(*args, **kwargs)
同样,Django 也提供了 modify_settings()
装饰器:
from django.test import TestCase, modify_settings
class MiddlewareTestCase(TestCase):
@modify_settings(MIDDLEWARE={
'append': 'django.middleware.cache.FetchFromCacheMiddleware',
'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
})
def test_cache_middleware(self):
response = self.client.get('/')
# ...
此装饰器也可以应用于测试用例类:
from django.test import TestCase, modify_settings
@modify_settings(MIDDLEWARE={
'append': 'django.middleware.cache.FetchFromCacheMiddleware',
'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
})
class MiddlewareTestCase(TestCase):
def test_cache_middleware(self):
response = self.client.get('/')
# ...
当给定一个类时,这些装饰器直接修改该类并返回它,它们不会创建并返回一个修改后的副本。因此,如果你试图调整上面的例子,将返回值分配给一个不同于 LoginTestCase
或 MiddlewareTestCase
的名称,你可能会惊讶地发现,原来的测试用例类仍然同样受到装饰器的影响。对于一个给定的类,modify_settings()
总是应用在 override_settings()
之后。
配置文件中包含了一些设置,这些设置只有在 Django 内部初始化时才会被使用。如果你用 override_settings
改变它们,当你通过django.conf.settings
模块访问会得到被改变的配置。但是,Django 的内部程序访问它的方式是不同的。实际上,使用 override_settings()
或者 modify_settings()
来使用这些设置,很可能达不到你预期的效果。
我们不建议改变 DATABASES
的设置。改变 CACHES
的设置是可能的,但如果你使用的是内部缓存,比如 django.contrib.session
,就有点棘手。例如,你必须在使用缓存会话并覆盖 CACHES
的测试中重新初始化会话后端。
最后,避免将你的配置别名为模块级常量,因为 override_settings()
不会对这些值起作用,它们只在第一次导入模块时才被评估。
你也可以在配置被覆盖后,通过删除配置来模拟没有配置,比如这样:
@override_settings()
def test_something(self):
del settings.LOGIN_URL
...
覆盖配置时,请确保处理你的应用代码使用即使保留配置更改也能保持状态的缓存或类似功能的情况。Django 提供了 django.test.signals.setting_changed
信号,让你在设置被改变时,可以注册回调来清理和重置状态。
Django 自己也使用这个信号来重置各种数据。
覆盖配置 | 数据重置 |
---|---|
USE_TZ,TIME_ZONE | 数据库时区 |
TEMPLATES | 模板引擎 |
SERIALIZATION_MODULES | 序列化器缓存 |
LOCALE_PATHS,LANGUAGE_CODE | 默认翻译和加载的翻译 |
MEDIA_ROOT,DEFAULT_FILE_STORAGE | 默认文件存储 |
清空测试发件箱
如果你使用任何 Django 的自定义 TestCase
类,测试运行器将在每个测试用例开始时清除测试邮件发件箱的内容。
断言
由于 Python 的普通 unittest.TestCase
类实现了 assertTrue()
和 assertEqual()
等断言方法,Django 的自定义 TestCase 类提供了许多对测试 Web 应用程序有用的自定义断言方法。
大多数这些断言方法给出的失败消息可以使用 msg_prefix
参数进行自定义。 该字符串将作为断言生成的任何失败消息的前缀。 这使您可以提供其他详细信息,以帮助您确定测试套件中失败的位置和原因。
-
SimpleTestCase.assertRaisesMessage(expected_exception, expected_message, callable, *args, **kwargs)
-
SimpleTestCase.assertRaisesMessage(expected_exception, expected_message)
断言执行 callable
引起 expected_exception
,并且在异常信息中发现 expected_message
。任何其他结果都会被报告为失败。它是 unittest.TestCase.assertRaisesRegex()
的简单版本,不同的是 expected_message
不作为正则表达式处理。
如果只给了 expected_exception
和 expected_message
参数,则返回一个上下文管理器,以便被测试的代码可以内联而不是作为一个函数来写:
with self.assertRaisesMessage(ValueError, 'invalid literal for int()'):
int('a')
-
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message, callable, *args, **kwargs)
-
SimpleTestCase.assertWarnsMessage(expected_warning, expected_message)
类似于 SimpleTestCase.assertRaisesMessage()
,但是 assertWarnsRegex()
代替 assertRaisesRegex()
。
SimpleTestCase.assertFieldOutput(fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value='')
断言表单字段在不同的输入情况下表现正确。
参数:
-
fieldclass
-- 待测试字段的类。 -
valid
-- 一个字典,将有效输入映射到它们的预期干净值。 -
invalid
-- 一个字典,将无效输入映射到一个或多个引发的错误信息 -
field_args
-- 传递给实例化字段的 args
。 -
field_kwargs
-- 传递给实例化字段的 kwargs
。 -
empty_value
-- empty_values
中输入的预期干净输出。
例如,以下代码测试 EmailField
接受 a@a.com
作为有效的电子邮件地址,但拒绝 aaa
,并给出合理的错误信息:
self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': ['Enter a valid email address.']})
SimpleTestCase.assertFormError(response, form, field, errors, msg_prefix='')
断言表单中的某个字段在表单中呈现时,会引发所提供的错误列表。
response
必须是测试客户端返回的响应实例。
form
是 Form
实例在响应的模板上下文中给出的名称。
field
是表单中要检查的字段名。如果 field
的值为 None
,则会检查非字段错误(可以通过 form.non_field_errors()
)。
errors
是一个错误字符串,或一个错误字符串列表,是表单验证的结果。
SimpleTestCase.assertFormsetError(response, formset, form_index, field, errors, msg_prefix='')
断言 formset
在渲染时,会引发所提供的错误列表。
response
必须是测试客户端返回的响应实例。
formset
是 Formset
实例在响应的模板上下文中给出的名称。
form_index
是 Formset
中表单的编号。 如果 form_index
的值为 None
,则将检查非表单错误(可以通过 formset.non_form_errors()
访问的错误)。
field
是表单中要检查的字段名。如果 field
的值为 None
,则会检查非字段错误(可以通过 form.non_field_errors()
)。
errors
是一个错误字符串,或一个错误字符串列表,是表单验证的结果。
SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False)
断言响应产生了给定的 status_code
并且该文本出现在其内容中。 如果提供了 count
,则文本必须在响应中准确出现 count
次。
将 html
设置为 True
,将 text
作为 HTML 处理。与响应内容的比较将基于 HTML 语义,而不是逐个字符的平等。在大多数情况下,空格会被忽略,属性排序并不重要。
SimpleTestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False)
断言响应产生了给定的 status_code
并且该文本未出现在其内容中。
将 html
设置为 True
,将 text
作为 HTML 处理。与响应内容的比较将基于 HTML 语义,而不是逐个字符的平等。在大多数情况下,空格会被忽略,属性排序并不重要。
SimpleTestCase.assertTemplateUsed(response, template_name, msg_prefix='', count=None)
断言具有给定名称的模板用于呈现响应。
response
必须是测试客户端返回的响应实例。
template_name
应该是一个字符串,例如admin/index.html
count
参数是一个整数,表示模板应该被渲染的次数。 默认为无,这意味着模板应该被渲染一次或多次。
您可以将其用作上下文管理器,如下所示:
with self.assertTemplateUsed('index.html'):
render_to_string('index.html')
with self.assertTemplateUsed(template_name='index.html'):
render_to_string('index.html')
SimpleTestCase.assertTemplateNotUsed(response, template_name, msg_prefix='')
断言给定名称的模板在渲染响应时 没有 被使用。
你可以用 assertTemplateUsed()
一样的方式将其作为上下文管理器。
SimpleTestCase.assertURLEqual(url1, url2, msg_prefix='')
断言两个 URL 是相同的,忽略查询字符串参数的顺序,但同名参数除外。例如,/path/?x=1&y=2
等于 /path/?y=2&x=1
,但 /path/?a=1&a=2
不等于 /path/?a=2&a=1
。
SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='', fetch_redirect_response=True)
断言响应返回了 status_code
重定向状态,重定向到了 expected_url
(包括任何 GET 数据),并且最后一页收到了 target_status_code
。
如果您的请求使用了 follow
参数,则 expected_url
和 target_status_code
将是重定向链最后点的 url 和状态码。
如果 fetch_redirect_response
为 False
,则不会加载最终页面。 由于测试客户端无法获取外部 URL,因此如果 expected_url
不是您的 Django 应用程序的一部分,这将特别有用。
在两个 URL 之间进行比较时,Scheme 得到了正确处理。 如果在我们被重定向到的位置没有指定任何方案,则使用原始请求的方案。 如果存在,则 expected_url
中的方案是用于进行比较的方案。
SimpleTestCase.assertHTMLEqual(html1, html2, msg=None)
断言字符串 html1 和 html2 相等。比较是基于 HTML 语义的。比较时考虑到以下因素:
- 忽略 HTML 标记前后的空格。
- 所有类型的空格都被认为是等效的。
- 所有打开的标签都是隐式关闭的,例如 当周围的标记关闭或 HTML 文档结束时。
- 空标签相当于它们的自闭合版本。
- HTML 元素的属性顺序并不重要。
- 没有参数的布尔属性(如检查)等于名称和值相等的属性。
- 引用相同字符的文本、字符引用和实体引用是等效的。
下面的例子是有效的测试,并且没有引起任何 AssertionError
:
self.assertHTMLEqual(
'<p>Hello <b>'world'!</p>',
'''<p>
Hello <b>'world'! </b>
</p>'''
)
self.assertHTMLEqual(
'<input type="checkbox" checked="checked" id="id_accept_terms" />',
'<input id="id_accept_terms" type="checkbox" checked>'
)
html1 和 html2 必须包含 HTML。如果其中一个不能被解析,将产生一个 AssertionError
。
错误时的输出可以用 msg 参数自定义。
SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None)
断言字符串 html1 和 html2 不 相等。比较是基于 HTML 语义的。
html1 和 html2 必须包含 HTML。如果其中一个不能被解析,将产生一个 AssertionError
。
错误时的输出可以用 msg
参数自定义。
SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None)
断言字符串 xml1 和 xml2 相等。比较是基于 XML 语义的。与 assertHTMLEqual()
类似,比较是在解析内容上进行的,因此只考虑语义差异,而不是语法差异。当任何参数中传递了无效的 XML 时,即使两个字符串相同,也总是会引发一个 AssertionError
。
忽略 XML 声明、文档类型、处理指令和注释。只有根元素和它的子元素被比较。
错误时的输出可以用 msg
参数自定义。
SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None)
断言字符串 xml1 和 xml2 不 相等。比较是基于 XML 语义的。
错误时的输出可以用 msg
参数自定义。
SimpleTestCase.assertInHTML(needle, haystack, count=None, msg_prefix='')
断言 HTML 片段 needle
包含在 haystack
中。
如果指定了 count
整数参数,则将严格核查 needle
的出现次数。
在大多数情况下,空白是被忽略的,属性排序并不重要。
SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None)
断言 JSON 片段 raw
和 expected_data
相等。通常的 JSON 非显性空格规则适用,因为重量级是委托给 json 库的。
错误时的输出可以用 msg
参数自定义。
SimpleTestCase.assertJSONNotEqual(raw, expected_data, msg=None)
断言 JSON 片段 raw
和 expected_data
不相等。
错误时的输出可以用 msg
参数自定义。
TransactionTestCase.assertQuerysetEqual(qs, values, transform=None, ordered=True, msg=None)
断言一个查询集 qs
与一个特定的可迭代对象 values
的值匹配。
如果提供了 transform
,values
将与应用 transform
于 qs
而产生的列表中每个成员进行比较。
默认情况下,比较也是依赖于顺序的。如果 qs
不提供隐式排序,你可以将 ordered
参数设置为 False
,这将使比较变成 collections.Counter
比较。如果顺序是未定义的(如果给定的 qs
不是有序的,并且比较的对象是一个以上的有序值),会产生一个 ValueError
。
错误时的输出可以用 msg
参数自定义。
TransactionTestCase.assertNumQueries(num, func, *args, **kwargs)
断言当 func
与 *args
和 **kwargs
一起调用时,会执行 num
次数据库查询。
如果 kwargs
中存在 using
键,则使用该键作为数据库别名,以检查查询次数:
self.assertNumQueries(7, using='non_default_db')
如果你想调用一个带有 using
参数的函数,你可以通过用 lambda
包装调用来增加一个额外的参数:
self.assertNumQueries(7, lambda: my_function(using=7))
你也可以用它作为上下文管理器:
with self.assertNumQueries(2):
Person.objects.create(name="Aaron")
Person.objects.create(name="Daniel")
标记测试
你可以给你的测试打上标签,这样你就可以轻松地运行一个特定的子集。例如,你可以标记快速或慢速测试:
from django.test import tag
class SampleTestCase(TestCase):
@tag('fast')
def test_fast(self):
...
@tag('slow')
def test_slow(self):
...
@tag('slow', 'core')
def test_slow_but_core(self):
...
你也可以标记一个测试用例:
@tag('slow', 'core')
class SampleTestCase(TestCase):
...
子类从超类继承标签,方法从其类继承标签。如:
@tag('foo')
class SampleTestCaseChild(SampleTestCase):
@tag('bar')
def test(self):
...
SampleTestCaseChild.test
将用 slow
、core
、bar
和 foo
来标注。
然后你可以选择要运行的测试。例如,只运行快速测试:
...\> manage.py test --tag=fast
或者运行快速测试和核心测试(即使它很慢):
...\> manage.py test --tag=fast --tag=core
你也可以通过标签来排除测试。如果要运行不慢的核心测试:
...\> manage.py test --tag=core --exclude-tag=slow
test --exclud-tag
优先于 test --tag
,所以如果一个测试有两个标签,你选择了其中一个而排除了另一个,测试就不会被运行。