Django4.0 测试工具-测试客户端
测试客户端是一个 Python 类,它充当虚拟 Web 浏览器,允许您测试视图并以编程方式与 Django 驱动的应用程序交互。
你可以使用测试客户端执行以下操作:
- 模拟 URL 上的
GET
和 POST
请求并观察响应——从低级 HTTP(结果头和状态码)到页面内容,应有尽有。 - 查看重定向链(如果有的话),并检查每个步骤的 URL 和状态码。
- 测试给定的请求是否由给定的包含某些值以及模板上下文的 Django 模板渲染。
请注意,测试客户端并不是要取代 Selenium
或其他“浏览器内”框架。Django 的测试客户端有不同的侧重点。简而言之:
- 使用 Django 的测试客户端来确定要渲染的模板正确,并且模板已传递了正确的上下文数据
- 使用
Selenium
等浏览器内框架来测试呈现的 HTML 和网页的行为,即 JavaScript 功能。 Django 还为这些框架提供了特殊支持
一个全面的测试套件应该使用这两种测试类型的组合。
概述和一个简单的例子
要使用测试客户端,请实例化 django.test.Client
并检索网页:
>>> from django.test import Client
>>> c = Client()
>>> response = c.post('/login/', {'username': 'john', 'password': 'smith'})
>>> response.status_code
200
>>> response = c.get('/customer/details/')
>>> response.content
b'<!DOCTYPE html...'
如本例所示,你可以从 Python 交互式解释器的会话中实例化 Client
。
请注意测试客户端如何工作的一些重要事项:
- 测试客户端不需要运行 Web 服务器。 事实上,它会运行得很好,根本没有运行 Web 服务器! 那是因为它避免了 HTTP 的开销,直接与 Django 框架打交道。 这有助于使单元测试快速运行。
- 检索页面时,请记住指定 URL 的 路径,而不是整个域。例如,这是正确的:
>>> c.get('/login/')
这是错误的:
>>> c.get('https://www.example.com/login/')
测试客户端无法检索不是由您的 Django 项目提供支持的网页。 如果您需要检索其他网页,请使用 Python 标准库模块,例如 urllib
。
- 为了解析 URL,测试客户端使用你的
ROOT_URLCONF
设置指向的任何 URLconf
。 - 尽管上面的示例可以在 Python 交互式解释器中运行,但测试客户端的某些功能,尤其是与模板相关的功能,仅在测试运行时才可用。这样做的原因是 Django 的测试运行器执行了一些黑魔法,以确定给定视图加载了哪个模板。 这种黑魔法(本质上是在内存中修补 Django 的模板系统)仅在测试运行期间发生。
- 默认情况下,测试客户端将禁用您的站点执行的任何 CSRF 检查。如果出于某种原因,您希望测试客户端执行 CSRF 检查,您可以创建一个强制执行 CSRF 检查的测试客户端实例。 为此,请在构建客户端时传入
enforce_csrf_checks
参数:
>>> from django.test import Client
>>> csrf_client = Client(enforce_csrf_checks=True)
发出请求
使用 django.test.Client
类发出请求。
class Client(enforce_csrf_checks=False, json_encoder=DjangoJSONEncoder, **defaults)
它在构造时不需要任何参数。然而,你可以使用关键字参数来指定一些默认头信息。例如,这将在每个请求中发送一个 User-Agent
HTTP 头:
>>> c = Client(HTTP_USER_AGENT='Mozilla/5.0')
传递给 get()
、post()
等方法的 extra
关键字参数的值,优先于传递给类构造函数的默认值。
enforce_csrf_checks
参数可用于测试 CSRF 保护。
json_encoder
参数允许为 post()
中描述的 JSON 序列化设置一个自定义 JSON 编码器。
raise_request_exception
参数允许控制是否在请求过程中引出的异常也应该在测试中引出。默认值为 True
。
一旦有了 Client 实例,就可以调用以下任何一种方法:
get(path, data=None, follow=False, secure=False, **extra)
对提供的 path
上发出 GET
请求,并返回一个 Response
对象,如下所述。
data
字典中的键值对用于创建 GET
数据有效载荷。例如:
>>> c = Client()
>>> c.get('/customers/details/', {'name': 'fred', 'age': 7})
产生等效的 GET 请求:
/customers/details/?name=fred&age=7
extra
关键词参数可以用来指定请求中要发送的头信息。例如:
>>> c = Client()
>>> c.get('/customers/details/', {'name': 'fred', 'age': 7},
... HTTP_ACCEPT='application/json')
将 HTTP 头 HTTP_ACCEPT
发送到 detail
视图,这是测试使用 django.http.HttpRequest.accepts()
方法的代码路径的好方法。
如果你已经有了 URL 编码形式的 GET
参数,你可以使用该编码代替使用数据参数。例如,之前的 GET
请求也可以改成:
>>> c = Client()
>>> c.get('/customers/details/?name=fred&age=7')
如果你提供的 URL 同时包含编码的 GET
数据和数据参数,数据参数将优先。
如果将 follow
设置为 True
,客户端将遵循所有重定向,并且将在响应对象中设置 redirect_chain
属性,该属性是包含中间 URL 和状态码的元组。
如果你有一个 URL /redirect_me/
,重定向到 /next/
,再重定向到 /final/
,这是你会看到的:
>>> response = c.get('/redirect_me/', follow=True)
>>> response.redirect_chain
[('http://testserver/next/', 302), ('http://testserver/final/', 302)]
如果你把 secure
设置为 True
,则客户端将模拟 HTTPS 请求。
post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, **extra)
在提供的 path
上发出一个 POST
请求,并返回一个 Response
对象,如下所述。
data
字典中的键值对用于提交 POST
数据。例如:
>>> c = Client()
>>> c.post('/login/', {'name': 'fred', 'passwd': 'secret'})
这将产生对这个 URL 的 POST
请求:
/login/
且具有此 POST
数据:
name=fred&passwd=secret
如果你提供 application/json
为 content_type
,则如果 data
是一个字典、列表或元组时,使用 json.dumps()
进行序列化。序列化默认是通过 DjangoJSONEncoder
,可以通过为 Client 提供 json_encoder
参数覆盖。这个序列化也会发生在 put()
、patch()
和 delete()
请求中。
如果你要提供任何其他的 content_type
(例如 text/xml
用于 XML 有效载荷),使用HTTP Content-Type
头中的 content_type
,data
的内容在 POST
请求中按原样发送。
如果你没有为 content_type
提供一个值,data 中的值将以 multipart/form-data
的内容类型进行传输。在这种情况下,data 中的键值对将被编码为多部分消息,并用于创建 POST
数据有效载荷。
要为一个给定的键提交多个值——例如,要指定 <select multiple>
的选择——为所需键提供一个列表或元组的值。例如,这个 data
的值将为名为 choices
的字段提交三个选择值:
{'choices': ('a', 'b', 'd')}
提交文件是一种特殊情况。要 POST
一个文件,你只需要提供文件字段名作为键,以及你想上传的文件的文件句柄作为值。例如:
>>> c = Client()
>>> with open('wishlist.doc', 'rb') as fp:
... c.post('/customers/wishes/', {'name': 'fred', 'attachment': fp})
这里的attachment
可以修改成你处理代码时想要的名称
你也可以提供任何类似于文件的对象(例如 StringIO
或 BytesIO
)作为文件句柄。如果你要上传到 ImageField
,这个对象需要一个可以通过 validate_image_file_extension
验证器的 name
属性。例如:
>>> from io import BytesIO
>>> img = BytesIO(b'mybinarydata')
>>> img.name = 'myimage.jpg'
请注意,如果你想在多次调用 post()
时使用同一个文件句柄,那么你需要在两次调用之间手动重置文件指针。最简单的方法是在向 post()
提供文件后手动关闭文件,如上所示。
你还应确保文件的打开方式允许数据被读取。如果你的文件包含二进制数据,如图像,这意味着你需要以 rb
(读取二进制)模式打开文件。
extra
参数的作用与 Client.get()
相同。
如果你用 POST
请求的 URL 包含编码参数,这些参数将在 request.GET
数据中提供。例如,如果你要请求:
>>> c.post('/login/?visitor=true', {'name': 'fred', 'passwd': 'secret'})
处理这个请求的视图可以询问 request.POST
来检索用户名和密码,也可以询问 request.GET
来确定该用户是否是访客。
如果将 follow
设置为 True
,客户端将遵循所有重定向,并且将在响应对象中设置 redirect_chain
属性,该属性是包含中间 URL 和状态码的元组。
如果你把 secure
设置为 True
,则客户端将模拟 HTTPS 请求。
head(path, data=None, follow=False, secure=False, **extra)
在提供的 path
上发出一个 HEAD
请求,并返回一个 Response
对象。这个方法的工作原理和 Client.get()
一样,包括 follow
、secure
和 extra
参数,只是它不返回消息主体。
options(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
在提供的 path
上发出一个 OPTIONS
请求并返回一个 Response
对象。用于测试 RESTful 接口。
当提供 data
时,它将被用作请求主体并且 Content-Type
头被设置为 content_type
。
follow
、 secure
和 extra
参数的作用与 Client.get()
相同。
put(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
在提供的 path
上发出一个 PUT
请求,并返回一个 Response
对象。用于测试 RESTful 接口。
当提供 data
时,它将被用作请求主体并且 Content-Type
头被设置为 content_type
。
follow
、 secure
和 extra
参数的作用与 Client.get()
相同。
patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
在提供的 path
上发出一个 PATCH
请求,并返回一个 Response
对象。用于测试 RESTful 接口。
follow
、 secure
和 extra
参数的作用与 Client.get()
相同。
delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
在提供的 path
上发出一个 DELETE
请求,并返回一个 Response
对象。用于测试 RESTful 接口。
当提供 data
时,它将被用作请求主体并且 Content-Type
头被设置为 content_type
。
follow
、 secure
和 extra
参数的作用与 Client.get()
相同。
trace(path, follow=False, secure=False, **extra)
在提供的 path
上发出一个 TRACE
请求,并返回一个 Response
对象。用于模拟诊断探针。
与其他请求方法不同,为了符合 RFC 7231#section-4.3.8 的要求,不提供 data
作为关键字参数,该 RFC 要求跟踪请求不能有主体。
follow
、 secure
和 extra
参数的作用与 Client.get()
相同。
login(**credentials)
如果你的网站使用了 Django 的 认证系统,并且你需要处理登录用户的问题,你可以使用测试客户端的 login()
方法来模拟用户登录网站的效果。
调用此方法后,测试客户端将拥有通过任何可能构成视图一部分的基于登录的测试所需的所有 cookie 和会话数据。
credentials
参数的格式取决于你使用的 认证后端 (这是由你的 AUTHENTICATION_BACKENDS
配置)。如果你使用的是 Django 提供的标准认证后端(ModelBackend
),credentials
应该是用户的用户名和密码,并作为关键字参数提供:
>>> c = Client()
>>> c.login(username='fred', password='secret')
# Now you can access a view that's only available to logged-in users.
如果你使用的是不同的认证后端,这个方法可能需要不同的凭证。它需要你的后端 authenticate()
方法所需要的任何凭证。
如果凭证被接受且登录成功,则 login()
返回 True
。
最后,在使用这个方法之前,你需要记得创建用户账户。正如我们上面所解释的,测试运行器是使用测试数据库执行的,默认情况下,数据库中不包含用户。因此,在生产站点上有效的用户账户在测试条件下将无法工作。你需要创建用户作为测试套件的一部分--无论是手动创建(使用 Django 模型 API)还是使用测试夹具。记住,如果你想让你的测试用户有一个密码,你不能直接通过设置密码属性来设置用户的密码——你必须使用 set_password()
函数来存储一个正确的哈希密码。或者,你可以使用 create_user()
辅助方法来创建一个具有正确哈希密码的新用户。
force_login(user, backend=None)
如果你的网站使用了 Django 的 认证系统,你可以使用 force_login()
方法来模拟用户登录网站的效果。当测试需要用户登录,而用户如何登录的细节并不重要时,可以使用这个方法代替 login()
。
与 login()
不同的是,这个方法跳过了认证和验证步骤:不活跃的用户(is_active=False
)被允许登录,并且不需要提供用户凭证。
用户的 backend
属性将被设置为 backend
参数的值(应该是一个点分隔 Python 路径字符串),如果没有提供值,则设置为 settings.AUTHENTICATION_BACKENDS[0]
login()
调用的 authenticate()
函数通常会对用户进行注释。
这个方法比 login()
快,因为它绕过了昂贵的密码散列算法。另外,你也可以通过 在测试时使用较弱的哈希算法 来加快 login() 速度。
logout()
如果你的网站使用了 Django 的 认证系统,logout()
方法可以用来模拟用户注销网站的效果。
调用此方法后,测试客户端的所有 cookie 和会话数据都会被清除为默认值。随后的请求将看起来来自一个 AnonymousUser
。
测试响应
get()
和 post()
方法都会返回一个 Response
对象,这个 Response
对象与 Django 视图返回的 HttpResponse
对象是 不 一样的;测试响应对象有一些额外的数据,对测试代码验证很有用。
具体来说,Response
对象具有以下属性:
class Response
client
:用于发出请求并得到响应的测试客户端。
content
:以字节字符串形式的响应主体。 这是视图或任何错误消息所呈现的最终页面内容。
context
:模板 Context
实例,用于渲染产生响应内容的模板。如果渲染的页面使用了多个模板,那么 context
将是一个按渲染顺序排列的 Context
对象列表。无论在渲染过程中使用了多少模板,你都可以使用 []
操作符来检索上下文值。例如,上下文变量 name
可以使用:
>>> response = client.get('/foo/')
>>> response.context['name']
'Arthur'
exc_info
:一个由三个值组成的元组,它提供了关于在视图期间发生的未处理异常(如果有)的信息。值是(type,value,traceback),与 Python 的 sys.exc_info()
返回的值相同。它们的含义是:
-
type
:异常的类型。 -
value
:异常的实例。 -
traceback
:一个追溯对象,在最初发生异常的地方封装了调用堆栈。
如果没有发生异常,那么 exc_info
将是 None
。
json(**kwargs)
:解析为 JSON 的响应主体。额外的关键字参数传递给 json.loads()
。例如:
>>> response = client.get('/foo/')
>>> response.json()['name']
'Arthur'
如果 Content-Type
头不是 application/json
,那么在试图解析响应时将会出现一个 ValueError
。
request
:激发响应的请求数据。
wsgi_request
:由生成响应的测试处理程序生成的 WSGIRequest
实例。
status_code
:整数形式的响应 HTTP 状态。
templates
:用于渲染最终内容的 Template
实例列表,按渲染顺序排列。对于列表中的每个模板,如果模板是从文件中加载的,则使用 template.name
获得模板的文件名。(名字是一个字符串,如 admin/index.html
。)
resolver_match
:响应的 ResolverMatch
的实例。你可以使用 func
属性,例如,验证服务于响应的视图:
# my_view here is a function based view
self.assertEqual(response.resolver_match.func, my_view)
# class-based views need to be compared by name, as the functions
# generated by as_view() won't be equal
self.assertEqual(response.resolver_match.func.__name__, MyView.as_view().__name__)
如果找不到给定的 URL,访问这个属性会引发一个 Resolver404
异常。
和普通的响应一样,你也可以通过 HttpResponse.headers
访问头信息。例如,你可以使用 response.headers['Content-Type']
来确定一个响应的内容类型。
例外
如果你把测试客户端指向一个会引发异常的视图,并且 Client.raise_request_exception
是 True
,那么这个异常将在测试用例中可见。然后你可以使用标准的 try ... except
块或 assertRaises()
来测试异常。
测试客户端看不到的异常只有 Http404
、PermissionDenied
、SystemExit
和 SuspiciousOperation
。Django 在内部捕获这些异常,并将其转换为相应的 HTTP 响应代码。在这些情况下,你可以在测试中检查 response.status_code
。
如果 Client.raise_request_exception
为 False
,测试客户端将返回一个 500 的响应,就像返回给浏览器一样。响应有属性 exc_info
来提供关于未处理的异常的信息。
持久状态
测试客户端是有状态的。如果一个响应返回一个 cookie,那么这个 cookie 将被存储在测试客户端,并与所有后续的 get()
和 post()
请求一起发送。
不遵循这些 cookie 的过期策略。如果你希望 cookie 过期,请手动删除它或创建一个新的 Client 实例(这将有效地删除所有 cookie)。
测试客户端有两个属性,存储持久化的状态信息。你可以作为测试条件的一部分来访问这些属性。
Client.cookies
:一个 Python SimpleCookie
对象,包含所有客户端 cookie 的当前值。
Client.session
:一个类似字典的对象,包含会话信息。要修改会话然后保存,必须先将其存储在一个变量中(因为每次访问该属性时都会创建一个新的 SessionStore
):
def test_something(self):
session = self.client.session
session['somekey'] = 'test'
session.save()
设置语言
在测试支持国际化和本地化的应用程序时,你可能想为测试客户端请求设置语言。这样做的方法取决于 LocaleMiddleware
是否启用。
如果启用了中间件,可以通过创建一个名为 LANGUAGE_COOKIE_NAME
的 cookie 来设置语言,其值为语言代码:
from django.conf import settings
def test_language_using_cookie(self):
self.client.cookies.load({settings.LANGUAGE_COOKIE_NAME: 'fr'})
response = self.client.get('/')
self.assertEqual(response.content, b"Bienvenue sur mon site.")
或在请求中加入 Accept-Language
HTTP 头:
def test_language_using_header(self):
response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='fr')
self.assertEqual(response.content, b"Bienvenue sur mon site.")
如果中间件没有启用,可以使用 translation.override()
设置活动语言:
from django.utils import translation
def test_language_using_override(self):
with translation.override('fr'):
response = self.client.get('/')
self.assertEqual(response.content, b"Bienvenue sur mon site.")
以下是使用测试客户端进行的单元测试:
import unittest
from django.test import Client
class SimpleTest(unittest.TestCase):
def setUp(self):
# Every test needs a client.
self.client = Client()
def test_details(self):
# Issue a GET request.
response = self.client.get('/customer/details/')
# Check that the response is 200 OK.
self.assertEqual(response.status_code, 200)
# Check that the rendered context contains 5 customers.
self.assertEqual(len(response.context['customers']), 5)