Django提供了一组工具,在编写测试时可以派上用场。
测试客户端
测试客户端是一个充当虚拟Web浏览器的Python类,允许您以编程方式测试视图并与Django驱动的应用程序进行交互。 测试客户端可以做的一些事情是:
-
在URL上模拟GET和POST请求并观察响应 - 从低级HTTP(结果标头和状态码)到页面内容的所有内容。
-
查看重定向链(如果有的话),并在每个步骤检查URL和状态代码。
-
测试给定的请求是否由给定的Django模板呈现,并且包含特定值的模板上下文。
请注意,测试客户端不是为了替代Selenium或其他浏览器内的框架。 Django的测试客户端有不同的重点。
简而言之:
- 使用Django的测试客户端来确定正在呈现正确的模板,并且模板传递了正确的上下文数据。
- 使用Selenium等浏览器内部框架来测试呈现的HTML和网页的行为,即JavaScript功能。
Django还为这些框架提供特别支持; 有关更多详细信息,请参阅LiveServerTestCase部分。 综合测试套件应该使用两种测试类型的组合。 有关示例的更详细的Django测试客户端,请参阅Django Project网站。
提供了测试用例类
正常的Python单元测试类扩展了unittest.TestCase的基类。 Django提供了这个基类的一些扩展:
SimpleTestCase
用一些基本功能扩展unittest.TestCase,如:
- 保存并恢复Python警告机器状态。
- 添加一些有用的断言,包括:
- 检查可调用是否会引发某种异常。
- 测试表单字段呈现和错误处理。
- 测试存在/不存在给定片段的HTML响应。
- 验证模板是否已用于生成给定的响应内容。
- 验证HTTP重定向是由应用程序执行的。
- 严格测试两个HTML片段的等式/不等式或遏制。
- 严格测试两个XML片段的平等/不平等。
- 严格测试两个JSON片段的相等性。
- 能够使用修改的设置运行测试。
- 使用测试客户端。
- 自定义测试时间URL映射。
TransactionTestCase
Django的TestCase类(如下所述)利用数据库事务工具来加速在每次测试开始时将数据库重置为已知状态的过程。 但是,这样做的后果是某些数据库行为无法在Django TestCase类中进行测试。
在这些情况下,您应该使用TransactionTestCase。 除了数据库重置为已知状态的方式以及测试代码测试提交和回滚效果的能力之外,TransactionTestCase和TestCase是相同的:
-
TransactionTestCase通过截断所有表来重置数据库。 一个TransactionTestCase可能会调用commit和rollback,并观察这些调用对数据库的影响。
-
另一方面,TestCase在测试后不会截断表格。 而是将测试代码封装在测试结束时回滚的数据库事务中。 这保证了测试结束时的回滚将数据库恢复到初始状态。
TransactionTestCase继承自SimpleTestCase。
TestCase
此类提供了一些可用于测试网站的附加功能。 将正常的unittest.TestCase转换为Django TestCase很简单:只需将您的测试的基类从unittest.TestCase更改为django.test.TestCase。 所有标准的Python单元测试功能都将继续可用,但它会增加一些有用的附加功能,其中包括:
- 自动加载夹具。
- 将测试封装在两个嵌套的原子块中:一个用于整个类,一个用于每个测试。
- 创建一个TestClient实例。
- Django特定的断言,用于测试重定向和表单错误等。
TestCase继承自TransactionTestCase。
LiveServerTestCase
LiveServerTestCase与TransactionTestCase的基本相同,只有一个额外的功能:它在安装时在后台启动一个活的Django服务器,并在卸载时关闭它。 这允许使用除Django虚拟客户端以外的自动化测试客户端,例如Selenium客户端,
在浏览器内执行一系列功能测试并模拟真实用户的操作。
测试用例功能
默认测试客户端
*TestCase实例中的每个测试用例都可以访问Django测试客户端的实例。 该客户端可以作为self.client访问。 这个客户端是为每个测试重新创建的,所以您不必担心状态(如cookie)从一个测试传递到另一个测试。 这意味着,而不是在每个测试中实例化一个客户端:
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)
Fixture装载
如果数据库中没有任何数据,则用于数据库支持的Web站点的测试用例用处不大。 为了使测试数据容易放入数据库,Django的自定义TransactionTestCase类提供了一种加载Fixture的方法。 fixture是Django知道如何导入数据库的数据集合。 例如,如果您的网站具有用户帐户,则可能会设置一个假用户帐户的夹具,以便在测试期间填充数据库。
创建fixture的最直接的方法是使用manage.py dumpdata命令。 这假定你已经在你的数据库中有一些数据。 (有关更多详细信息,请参阅dumpdata文档)。
一旦你创建了一个fixture并把它放在你的INSTALLED_APPS的fixtures目录中,你就可以在你的单元测试中通过在你的django.test.TestCase子类中指定一个fixtures class属性来使用它:
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 testFluffyAnimals(self):
# A test that uses the fixtures.
call_some_test_code()
具体会发生什么:
-
在每个测试用例开始时,在运行setUp()之前,Django将刷新数据库,将数据库返回到它在调用迁移后直接处于的状态。
-
然后,安装所有命名的夹具。 在这个例子中,Django将安装任何名为哺乳动物的JSON夹具,接下来是任何名为bird的夹具。
有关定义和安装夹具的更多详细信息,请参阅loaddata文档。
对测试用例中的每个测试都重复此刷新/加载过程,因此您可以确定测试的结果不会受到另一个测试或测试执行顺序的影响。 默认情况下,灯具只加载到默认数据库中。 如果您正在使用多个数据库并将multi_db设置为True,则会将Fixture加载到所有数据库中。
覆写设置
使用以下功能临时更改测试中的设置值。 不要直接操作django.conf.settings,因为在这种操作之后,Django不会恢复原始值。
settings()
出于测试目的,在运行测试代码后暂时更改设置并恢复为原始值通常很有用。 对于这个用例,Django提供了一个名为settings()的标准Python上下文管理器(请参阅PEP 343),它可以像这样使用:
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设置,然后将其值重置为之前的状态。
modify_settings()
重新定义包含值列表的设置可能难以实现。 实际上,增加或删除值通常就足够了。 modify_settings()上下文管理器使其变得非常简单:
from django.test import TestCase
class MiddlewareTestCase(TestCase):
def test_cache_middleware(self):
with self.modify_settings(MIDDLEWARE_CLASSES={
'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不起作用; 当值不存在时,也不会删除。
override_settings()
如果你想覆盖一个测试方法的设置,Django提供了override_settings()装饰器(见PEP 318)。 它是这样使用的:
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()
同样,Django提供了modify_settings()装饰器:
from django.test import TestCase, modify_settings
class MiddlewareTestCase(TestCase):
@modify_settings(MIDDLEWARE_CLASSES={
'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_CLASSES={
'append': 'django.middleware.cache.FetchFromCacheMiddleware',
'prepend': 'django.middleware.cache.UpdateCacheMiddleware',
})
class MiddlewareTestCase(TestCase):
def test_cache_middleware(self):
response = self.client.get('/')
# ...
在覆盖设置时,请确保处理应用代码使用缓存或保留状态的类似功能的情况,即使设置已更改。 Django提供django.test.signals.setting_changed信号,允许您注册回调以清除并在设置更改时重置状态。
断言
由于Python的普通unittest.TestCase类实现了assertTrue()和assertEqual()等断言方法,因此Django的自定义TestCase类提供了许多可用于测试Web应用程序的自定义断言方法:
-
assertRaisesMessage - 声明可执行对象的执行引发了具有expected_message表示的异常。
-
assertFieldOutput - 声明表单字段在各种输入下正确运行。
-
assertFormError - 声明表单上的字段在窗体上呈现时引发所提供的错误列表。
-
assertFormsetError - 声明formset在渲染时引发提供的错误列表。
-
assertContains - 声明Response实例产生给定的status_code,并且该文本出现在响应的内容中。
-
assertNotContains - 声明Response实例产生给定的status_code,并且该文本不会出现在响应的内容中。
-
assertTemplateUsed - 声明具有给定名称的模板用于呈现响应。该名称是一个字符串,如'admin / index.html'。
-
assertTemplateNotUsed - 声明具有给定名称的模板未用于呈现响应。
-
assertRedirects - 声明响应返回了一个status_code重定向状态,重定向到expected_url(包括任何GET数据),并且最终页面被target_status_code收到。
-
assertHTMLEqual - 断言字符串html1和html2是相等的。该比较基于HTML语义。考虑到以下因素进行比较:
- 忽略HTML标签前后的空格。
- 所有类型的空白都被认为是等同的。
- 所有打开的标签都隐式关闭,例如 当周围的标签被关闭或HTML文档结束时。
- 空标签相当于它们的自闭版本。
- HTML元素的属性排序并不重要。
- 没有参数的属性等于名称和值相同的属性(参见示例)。
-
assertHTMLNotEqual - 声明字符串html1和html2不相等。
该比较基于HTML语义。有关详细信息,请参阅assertHTMLEqual()。 -
assertXMLEqual - 声明字符串xml1和xml2是相等的。该比较基于XML语义。与assertHTMLEqual()类似,对解析的内容进行比较,因此只考虑语义差异,而不考虑语法差异。
-
assertXMLNotEqual - 声明字符串xml1和xml2不相等。该比较基于XML语义。有关详细信息,请参阅assertXMLEqual()。
-
assertInHTML - 断言HTML碎片针包含在干草堆中。
-
assertJSONEqual - 声明JSON片段raw和expected_data相等。
-
assertJSONNotEqual - 声明JSON片段raw和expected_data不相等。
-
assertQuerysetEqual - 声明查询集qs返回值列表的特定值。使用函数变换执行qs和值的内容比较;默认情况下,这意味着比较每个值的repr()。
-
assertNumQueries - 断言当用* args和** kwargs调用func时,执行num数据库查询。
电邮服务
如果您的任何Django视图使用Django的电子邮件功能发送电子邮件,那么您可能不希望每次使用该视图运行测试时发送电子邮件。出于这个原因,Django的测试运行器会自动将所有Django发送的电子邮件重定向到一个虚拟发件箱。这使您可以测试发送电子邮件的每个方面 - 从发送到每封邮件内容的邮件数量 - 而不实际发送邮件。测试运行者通过用测试后端透明地替换正常的电子邮件后端来完成此操作。 (别担心 - 如果您正在运行其他电子邮件发件人,则不会影响Django以外的任何其他电子邮件发件人,例如您的计算机的邮件服务器。)
在测试运行期间,每封发出的电子邮件都保存在django.core.mail.outbox中。这是已发送的所有EmailMessage实例的简单列表。 outbox属性是仅在使用locmem电子邮件后端时创建的特殊属性。它通常不会作为django.core.mail模块的一部分存在,您无法直接导入它。下面的代码显示了如何正确访问该属性。以下是一个示例测试,用于检查django.core.mail.outbox的长度和内容:
from django.core import mail
from django.test import TestCase
class EmailTest(TestCase):
def test_send_email(self):
# Send message.
mail.send_mail('Subject here', 'Here is the message.',
'from@example.com', ['to@example.com'],
fail_silently=False)
# Test that one message has been sent.
self.assertEqual(len(mail.outbox), 1)
# Verify that the subject of the first message is correct.
self.assertEqual(mail.outbox[0].subject, 'Subject here')
如前所述,在Django * TestCase的每个测试开始时,测试发件箱都将被清空。 要手动清空发件箱,请将空列表分配给mail.outbox:
from django.core import mail
# Empty the test outbox
mail.outbox = []
管理命令
可以使用call_command()函数来测试管理命令。 输出可以被重定向到一个StringIO实例中:
from django.core.management import call_command
from django.test import TestCase
from django.utils.six import StringIO
class ClosepollTest(TestCase):
def test_command_output(self):
out = StringIO()
call_command('closepoll', stdout=out)
self.assertIn('Expected output', out.getvalue())
跳过测试
unittest库提供了@skipIf和@skipUnless修饰器,如果您事先知道这些测试在某些条件下会失败,您可以跳过测试。 例如,如果您的测试需要特定的可选库才能成功,则可以使用@skipIf修饰测试用例。 然后,测试运行人员将报告测试未执行以及为什么,而不是通过测试或完全省略测试。
测试数据库
需要数据库的测试(即模型测试)不会使用您的生产数据库; 为测试创建单独的空白数据库。 无论测试通过还是失败,测试数据库在所有测试执行完毕后都会被销毁。 您可以通过向测试命令添加--keepdb标志来防止销毁测试数据库。 这将在运行之间保留测试数据库。
如果数据库不存在,它将首先被创建。 任何迁移也将被应用,以保持它的最新状态。 默认情况下,测试数据库通过将test_添加到NAME的值来获取其名称
DATABASES中定义的数据库的设置。 在使用SQLite数据库引擎时,测试默认使用内存数据库(即,数据库将在内存中创建,完全绕过文件系统!)。
如果要使用不同的数据库名称,请在TEST字典中为DATABASES中的任何给定数据库指定NAME。 在PostgreSQL上,USER还需要对内置postgres数据库的读取权限。 除了使用单独的数据库以外,测试运行器还会使用您的设置文件中所有相同的数据库设置:ENGINE,USER,HOST等。测试数据库由USER指定的用户创建,因此您可以 需要确保给定的用户帐户有足够的权限在系统上创建新的数据库。
使用不同的测试框架
显然,unittest不是唯一的Python测试框架。 虽然Django没有提供对替代框架的明确支持,但它确实提供了一种方法来调用为替代框架构建的测试,就好像它们是正常的Django测试一样。
当你运行 ./manage.py test时,Django会查看TEST_RUNNER设置以确定要执行的操作。 默认情况下,TEST_RUNNER指向django.test.runner.DiscoverRunner。 这个类定义了默认的Django测试行为。 这种行为涉及:
-
执行全局预测试设置。
-
在名称与模式test * .py匹配的当前目录下面的任何文件中查找测试。
-
创建测试数据库。
-
正在运行迁移以将模型和初始数据安装到测试数据库中。
-
运行找到的测试。
-
销毁测试数据库。
-
执行全局的测试后拆解。
如果您定义了您自己的测试运行器类并在该类上指向了TEST_RUNNER,则只要运行 ./manage.py test,Django就会执行测试运行器。
通过这种方式,可以使用任何可以从Python代码执行的测试框架,或者修改Django测试执行流程以满足您可能具有的任何测试需求。
有关使用不同测试框架的更多信息,请参阅Django项目网站。
下一步是什么?
既然您已经知道如何为您的Django项目编写测试,那么一旦您准备将项目变成一个真正的实时网站 - 我们将把Django部署到一个Web服务器上,我们将继续讨论一个非常重要的主题。
网友评论