"django-test-plus 为Django的默认TestCase提供有用的扩展"
项目描述
django-test-plus
来自REVSYS的Django默认TestCase的有用扩展
理由
让我们面对现实,编写测试并不总是有趣。部分原因是需要编写大量样板代码。django-test-plus试图在编写Django测试时减少这些样板代码。我们保证这至少可以将您患上腕管综合征的时间推迟3周!
如果您想开始测试您的Django应用或提高团队测试的方式,我们提供TestStart来帮助您的团队显著提高生产力。
支持
支持:Python 3.8、3.9、3.10、3.11和3.12。
支持Django版本:3.2、4.2和5.0。
文档
完整的文档可在http://django-test-plus.readthedocs.org找到。
安装
$ pip install django-test-plus
使用
要使用 django-test-plus,请让您的测试从 test_plus.test.TestCase 继承,而不是正常的 django.test.TestCase:
from test_plus.test import TestCase
class MyViewTests(TestCase):
...
这足以让事情开始,但您被鼓励为项目创建自己的子类。这将允许您添加自己项目特定的辅助方法。
例如,如果您有一个名为 'myproject' 的 django 项目,您可以在 myproject/test.py
中创建以下内容
from test_plus.test import TestCase as PlusTestCase
class TestCase(PlusTestCase):
pass
然后在您的测试中使用
from myproject.test import TestCase
class MyViewTests(TestCase):
...
此导入方式与导入 Django 的 TestCase 类似,也是有效的
from test_plus import TestCase
pytest 使用
现在,您可以请求 tp
来获取一个类似 TestCase 的 pytest fixture。以下所有方法现在都可以在 pytest 函数中使用。例如
def test_url_reverse(tp):
expected_url = '/api/'
reversed_url = tp.reverse('api')
assert expected_url == reversed_url
tp_api
fixture 将提供一个使用 django-rest-framework 的 APIClient()
的 TestCase
def test_url_reverse(tp_api):
response = tp_api.client.post("myapi", format="json")
assert response.status_code == 200
方法
reverse(url_name, *args, **kwargs)
在测试视图时,您经常会发现自己需要反转 URL 的名称。在 django-test-plus 中,无需使用 from django.core.urlresolvers import reverse
烦琐代码。相反,使用
def test_something(self):
url = self.reverse('my-url-name')
slug_url = self.reverse('name-takes-a-slug', slug='my-slug')
pk_url = self.reverse('name-takes-a-pk', pk=12)
如您所见,我们的 reverse 也会传递任何您需要传递的 args 或 kwargs。
get(url_name, follow=True, *args, **kwargs)
您还经常执行 HTTP GET URL 操作。我们的 get()
方法假设您正在传递一个带有任何需要反转 url_name 的 args 或 kwargs 的命名 URL。如果需要,将 TestClient.get()
的 kwargs 放入 'extra' 字典中。
def test_get_named_url(self):
response = self.get('my-url-name')
# Get XML data via AJAX request
xml_response = self.get(
'my-url-name',
extra={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'})
使用此 get 方法时,还有两件事为您自动完成:我们将最后的响应存储在 self.last_response
中,并将响应的上下文存储在 self.context
中。
因此,而不是
def test_default_django(self):
response = self.client.get(reverse('my-url-name'))
self.assertTrue('foo' in response.context)
self.assertEqual(response.context['foo'], 12)
您可以编写
def test_testplus_get(self):
self.get('my-url-name')
self.assertInContext('foo')
self.assertEqual(self.context['foo'], 12)
它也非常智能地处理已反转的 URL,因此您可以偷懒并这样做
def test_testplus_get(self):
url = self.reverse('my-url-name')
self.get(url)
self.response_200()
如果您需要将查询字符串参数传递给 URL 名称,您可以这样做。假设名称 'search' 映射到 '/search/',那么
def test_testplus_get_query(self):
self.get('search', data={'query': 'testing'})
将会发送 GET /search/?query=testing
。
post(url_name, data, follow=True, *args, **kwargs)
我们的 post()
方法接受一个命名 URL、一个可选的数据字典(您希望发布的任何数据)以及任何反转 url_name 所必需的 args 或 kwargs。如果需要,将 TestClient.post()
的 kwargs 放入 'extra' 字典中。
def test_post_named_url(self):
response = self.post('my-url-name', data={'coolness-factor': 11.0},
extra={'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'})
注意 除了常用的 get 和 post 之外,我们还以相同的方式支持所有 HTTP 动词,例如 put、patch、head、trace、options 和 delete。
get_context(key)
您通常需要从模板上下文中获取内容
def test_context_data(self):
self.get('my-view-with-some-context')
slug = self.get_context('slug')
assertInContext(key)
您可以使用以下方法确保特定的键存在于最后的响应上下文中
def test_in_context(self):
self.get('my-view-with-some-context')
self.assertInContext('some-key')
assertContext(key, value)
我们可以获取上下文值并确保它们存在,同时我们还可以测试等价性。这断言 key == value
def test_in_context(self):
self.get('my-view-with-some-context')
self.assertContext('some-key', 'expected value')
assert_http_###_<status_name>(response, msg=None)
- 状态码检查
您经常需要执行的其他测试之一是检查响应是否具有特定的 HTTP 状态码。使用 Django 的默认 TestCase,您会写
from django.core.urlresolvers import reverse
def test_status(self):
response = self.client.get(reverse('my-url-name'))
self.assertEqual(response.status_code, 200)
使用 django-test-plus,您可以将其缩短为
def test_better_status(self):
response = self.get('my-url-name')
self.assert_http_200_ok(response)
Django-test-plus 为您提供了大多数状态码断言。状态断言可以在它们自己的 mixin 中找到,如果您使用的是像 pycharm 这样的 IDE,则应该可以搜索。应该注意的是,在之前的版本中,django-test-plus 有着 response_###()
模式的断言方法,这些方法仍然可用,但已被弃用。以下是一些方法的列表。
每个断言方法都接受一个可选的 Django 测试客户端 response
和一个字符串 msg
参数,如果指定,将在发生失败时用作错误信息。方法 assert_http_301_moved_permanently
和 assert_http_302_found
还接受一个可选的 url
参数,如果传递,将检查 response.url
是否匹配。
如果可用,assert_http_###_<status_name>
方法将使用最后一个响应。因此您可以这样做
def test_status(self):
self.get('my-url-name')
self.assert_http_200_ok()
这会稍微简短一些。
已经弃用但仍然可用的 response_###()
方法包括
response_200()
response_201()
response_204()
response_301()
response_302()
response_400()
response_401()
response_403()
response_404()
response_405()
response_409()
response_410()
所有这些方法都接受一个可选的 Django 测试客户端响应和一个 str msg 参数,如果指定,则在失败时用作错误消息。就像 assert_http_###_<status_name>()
方法一样,如果可用,这些方法将使用最后一个响应。
get_check_200(url_name, *args, **kwargs)
GET 和检查视图返回状态 200 是一个常见的测试。此方法使其更加方便:
def test_even_better_status(self):
response = self.get_check_200('my-url-name')
make_user(username='testuser', password='password', perms=None)
在测试视图时,您经常需要创建各种用户以确保所有逻辑都是安全可靠的。为了使此过程更容易,此方法将为您创建一个用户
def test_user_stuff(self)
user1 = self.make_user('u1')
user2 = self.make_user('u2')
如果创建项目中的用户更复杂,例如,您从默认的 Django Auth 模型中删除了 username
字段,您可以提供一个 Factory Boy 工厂来创建它或覆盖此方法的自定义子类。
要使用 Factory Boy 工厂,创建您的类如下:
from test_plus.test import TestCase
from .factories import UserFactory
class MySpecialTest(TestCase):
user_factory = UserFactory
def test_special_creation(self):
user1 = self.make_user('u1')
注意:此方法创建的用户默认将密码设置为字符串 'password',以简化测试。如果您需要特定的密码,可以覆盖 password
参数。
您还可以通过传递字符串 '<app_name>.<perm name>
' 或 '<app_name>.*
' 来传递用户权限。例如
user2 = self.make_user(perms=['myapp.create_widget', 'otherapp.*'])
print_form_errors(response_or_form=None)
在调试视图的失败测试时,此方法可以帮助您快速查看任何表单错误。
示例用法
class MyFormTest(TestCase):
self.post('my-url-name', data={})
self.print_form_errors()
# or
resp = self.post('my-url-name', data={})
self.print_form_errors(resp)
# or
form = MyForm(data={})
self.print_form_errors(form)
身份验证辅助工具
assertLoginRequired(url_name, *args, **kwargs)
此方法帮助您测试给定的命名 URL 是否需要授权
def test_auth(self):
self.assertLoginRequired('my-restricted-url')
self.assertLoginRequired('my-restricted-object', pk=12)
self.assertLoginRequired('my-restricted-object', slug='something')
login()
上下文
除了确保视图需要登录和创建用户外,您接下来要做的是以各种用户登录以测试您的限制逻辑
def test_restrictions(self):
user1 = self.make_user('u1')
user2 = self.make_user('u2')
self.assertLoginRequired('my-protected-view')
with self.login(username=user1.username, password='password'):
response = self.get('my-protected-view')
# Test user1 sees what they should be seeing
with self.login(username=user2.username, password='password'):
response = self.get('my-protected-view')
# Test user2 see what they should be seeing
由于我们很可能使用上面的 make_user()
创建用户,因此登录上下文假定密码是 'password',除非指定其他密码。因此您可以这样做
def test_restrictions(self):
user1 = self.make_user('u1')
with self.login(username=user1.username):
response = self.get('my-protected-view')
如果我们使用 make_user()
,我们还可以推导出用户名,这样我们可以进一步缩短如下:
def test_restrictions(self):
user1 = self.make_user('u1')
with self.login(user1):
response = self.get('my-protected-view')
确保低查询计数
assertNumQueriesLessThan(number)
- 上下文
Django 提供 assertNumQueries
,这对于您的代码生成特定数量的查询非常有用。然而,如果由于数据本身的性质,这个数字会变化,使用此方法您仍然可以测试确保代码不会生成比预期多得多的查询。
def test_something_out(self):
with self.assertNumQueriesLessThan(7):
self.get('some-view-with-6-queries')
assertGoodView(url_name, *args, **kwargs)
此方法为您做了一些事情。它
- 检索名称 URL
- 确保视图不会生成超过 50 个查询
- 确保响应状态码为 200
- 返回响应
这种广泛的测试通常比没有测试要好。您可以使用它如下:
def test_better_than_nothing(self):
response = self.assertGoodView('my-url-name')
测试 DRF 视图
为了利用 DRF 测试客户端的便利性,您可以创建 TestCase
的子类并设置 client_class
属性
from test_plus import TestCase
from rest_framework.test import APIClient
class APITestCase(TestCase):
client_class = APIClient
为了方便起见,test_plus
附带 APITestCase
,它就是这样做的
from test_plus import APITestCase
class MyAPITestCase(APITestCase):
def test_post(self):
data = {'testing': {'prop': 'value'}}
self.post('view-json', data=data, extra={'format': 'json'})
self.assert_http_200_ok()
请注意,使用 APITestCase
需要 Django >= 1.8 并已安装 django-rest-framework
。
测试基于类的“通用”视图
TestCase 方法的 get()
和 post()
适用于基于函数和基于类的视图。然而,这样做会调用 Django 的 URL 解析、中间件、模板处理和装饰器系统。对于集成测试来说,这是有利的,因为您想要确保您的 URL 能够正确解析,视图权限得到执行等。但对于单元测试来说,这是昂贵的,因为这些 Django 请求/响应系统会与您的方法一起调用,并且通常不会影响最终结果。
基于类的视图(派生自 Django 的 generic.models.View
类)包含方法和混合,这使得细粒度的单元测试(更)可行。通常,您对通用视图类的使用涉及重写现有方法。当您真正想直接调用重写的方法并测试结果时,调用整个视图和 Django 请求/响应堆栈是浪费时间。
CBVTestCase 来拯救!
与上面的 TestCase 一样,让您的测试继承自 test_plus.test.CBVTestCase 而不是 TestCase,如下所示
from test_plus.test import CBVTestCase
class MyViewTests(CBVTestCase):
方法
get_instance(cls, initkwargs=None, request=None, *args, **kwargs)
这个核心方法简化了类的实例化,为您提供了一种直接调用类方法的方式。
返回 cls
的一个实例,用 initkwargs
初始化。在类实例上设置 request
、args
和 kwargs
属性。args
和 kwargs
是您会传递给 reverse()
的相同值。
示例用法
from django.views import generic
from test_plus.test import CBVTestCase
class MyClass(generic.DetailView)
def get_context_data(self, **kwargs):
kwargs['answer'] = 42
return kwargs
class MyTests(CBVTestCase):
def test_context_data(self):
my_view = self.get_instance(MyClass, {'object': some_object})
context = my_view.get_context_data()
self.assertEqual(context['answer'], 42)
get(cls, initkwargs=None, *args, **kwargs)
调用 cls.get()
并返回响应,如果可能的话渲染模板。基于 CBVTestCase.get_instance()
的基础。
所有 test_plus.test.TestCase 方法都是有效的,所以以下也有效
response = self.get(MyClass)
self.assertContext('my_key', expected_value)
所有 test_plus TestCase 的副作用都被尊重,并且所有 test_plus TestCase 断言方法都与 CBVTestCase.get()
一起工作。
注意:此方法绕过了 Django 的中间件,因此中间件创建的上下文变量不可用。如果这影响了您的模板/上下文测试,您应使用 TestCase 而不是 CBVTestCase。
post(cls, data=None, initkwargs=None, *args, **kwargs)
调用 cls.post()
并返回响应,如果可能的话渲染模板。基于 CBVTestCase.get_instance()
的基础。
示例
response = self.post(MyClass, data={'search_term': 'revsys'})
self.response_200(response)
self.assertContext('company_name', 'RevSys')
所有 test_plus TestCase 的副作用都被尊重,并且所有 test_plus TestCase 断言方法都与 CBVTestCase.post()
一起工作。
注意:此方法绕过了 Django 的中间件,因此中间件创建的上下文变量不可用。如果这影响了您的模板/上下文测试,您应使用 TestCase 而不是 CBVTestCase。
get_check_200(cls, initkwargs=None, *args, **kwargs)
与 TestCase.get_check_200()
一样工作。调用者必须提供视图类而不是 URL 名称或路径参数。
所有 test_plus TestCase 的副作用都被尊重,并且所有 test_plus TestCase 断言方法都与 CBVTestCase.post()
一起工作。
assertGoodView(cls, initkwargs=None, *args, **kwargs)
与 TestCase.assertGoodView()
一样工作。调用者必须提供视图类而不是 URL 名称或路径参数。
所有 test_plus TestCase 的副作用都被尊重,并且所有 test_plus TestCase 断言方法都与 CBVTestCase.post()
一起工作。
开发
要处理 django-test-plus 本身,克隆此存储库并运行以下命令
$ pip install -e .
$ pip install -e .[test]
运行所有测试
$ nox
注意:您还需要确保位于此存储库根目录的 test_project
目录位于您的虚拟环境路径中。
保持联系!
如果您对此项目有任何疑问,请打开 GitHub 问题。如果您喜欢我们并想跟踪我们的动态,以下是我们可以在网上找到的地方
项目详情
下载文件
下载适用于您的平台的文件。如果您不确定选择哪一个,请了解更多关于安装包的信息。