test.py

测试

没有测试的代码在设计上就是有缺陷的。

Jacob Kaplan-Moss

REST 框架包含一些帮助类,它们扩展了 Django 现有的测试框架,并改进了对发出 API 请求的支持。

APIRequestFactory

扩展 Django 现有的 RequestFactory

创建测试请求

APIRequestFactory 类支持与 Django 的标准 RequestFactory 类几乎相同的 API。这意味着标准的 .get().post().put().patch().delete().head().options() 方法都可用。

from rest_framework.test import APIRequestFactory

# Using the standard RequestFactory API to create a form POST request
factory = APIRequestFactory()
request = factory.post('/notes/', {'title': 'new idea'})

使用 format 参数

创建请求正文的方法(例如 postputpatch)包含一个 format 参数,使用它可以轻松生成使用除 multipart 表单数据之外的其他内容类型的请求。例如

# Create a JSON POST request
factory = APIRequestFactory()
request = factory.post('/notes/', {'title': 'new idea'}, format='json')

默认情况下,可用格式为 'multipart''json'。为了与 Django 现有的 RequestFactory 兼容,默认格式为 'multipart'

要支持更广泛的请求格式或更改默认格式,请参阅配置部分

显式编码请求正文

如果您需要显式编码请求正文,可以通过设置 content_type 标志来实现。例如

request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json')

使用表单数据的 PUT 和 PATCH

值得注意的是,Django 的 RequestFactory 和 REST 框架的 APIRequestFactory 之间的一个区别在于,multipart 表单数据将被编码为除 .post() 之外的其他方法。

例如,使用 APIRequestFactory,您可以像这样发出表单 PUT 请求

factory = APIRequestFactory()
request = factory.put('/notes/547/', {'title': 'remember to email dave'})

使用 Django 的 RequestFactory,您需要自己显式编码数据

from django.test.client import encode_multipart, RequestFactory

factory = RequestFactory()
data = {'title': 'remember to email dave'}
content = encode_multipart('BoUnDaRyStRiNg', data)
content_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
request = factory.put('/notes/547/', content, content_type=content_type)

强制认证

在使用请求工厂直接测试视图时,通常可以直接认证请求很方便,而不是必须构造正确的认证凭据。

要强制认证请求,请使用 force_authenticate() 方法。

from rest_framework.test import force_authenticate

factory = APIRequestFactory()
user = User.objects.get(username='olivia')
view = AccountDetail.as_view()

# Make an authenticated request to the view...
request = factory.get('/accounts/django-superstars/')
force_authenticate(request, user=user)
response = view(request)

该方法的签名为 force_authenticate(request, user=None, token=None)。在发出调用时,可以设置用户和令牌中的一个或两个。

例如,当使用令牌强制认证时,您可能会执行如下操作

user = User.objects.get(username='olivia')
request = factory.get('/accounts/django-superstars/')
force_authenticate(request, user=user, token=user.auth_token)

注意force_authenticate 直接将 request.user 设置为内存中的 user 实例。如果您在更新已保存 user 状态的多个测试中重新使用同一个 user 实例,您可能需要在测试之间调用 refresh_from_db()


注意:使用 APIRequestFactory 时,返回的对象是 Django 的标准 HttpRequest,而不是 REST 框架的 Request 对象,后者仅在调用视图时生成。

这意味着直接在请求对象上设置属性可能不会总是产生您期望的效果。例如,直接设置 .token 不会产生任何效果,而直接设置 .user 仅在使用会话认证时才有效。

# Request will only authenticate if `SessionAuthentication` is in use.
request = factory.get('/accounts/django-superstars/')
request.user = user
response = view(request)

强制 CSRF 验证

默认情况下,使用 APIRequestFactory 创建的请求在传递到 REST 框架视图时不会应用 CSRF 验证。如果您需要明确启用 CSRF 验证,可以在实例化工厂时设置 enforce_csrf_checks 标志来实现。

factory = APIRequestFactory(enforce_csrf_checks=True)

注意:值得注意的是,Django 的标准 RequestFactory 不需要包含此选项,因为在使用常规 Django 时,CSRF 验证在中间件中进行,而直接测试视图时不会运行中间件。使用 REST 框架时,CSRF 验证在视图内进行,因此请求工厂需要禁用视图级别的 CSRF 检查。


APIClient

扩展 Django 现有的 Client

发出请求

APIClient 类支持与 Django 的标准 Client 类相同的请求接口。这意味着标准的 .get().post().put().patch().delete().head().options() 方法都可用。例如

from rest_framework.test import APIClient

client = APIClient()
client.post('/notes/', {'title': 'new idea'}, format='json')

要支持更广泛的请求格式或更改默认格式,请参阅配置部分

认证

.login(**kwargs)

login 方法的功能与 Django 的常规 Client 类完全相同。这允许您对包含 SessionAuthentication 的任何视图进行请求认证。

# Make all requests in the context of a logged in session.
client = APIClient()
client.login(username='lauren', password='secret')

要注销,请照常调用 logout 方法。

# Log out
client.logout()

login 方法适用于测试使用会话认证的 API,例如包含与 API 的 AJAX 交互的网站。

.credentials(**kwargs)

credentials 方法可用于设置标头,然后在测试客户端的所有后续请求中包含这些标头。

from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient

# Include an appropriate `Authorization:` header on all requests.
token = Token.objects.get(user__username='lauren')
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)

请注意,第二次调用 credentials 会覆盖任何现有凭据。您可以通过不带任何参数调用该方法来取消设置任何现有凭据。

# Stop including any credentials
client.credentials()

credentials 方法适用于测试需要身份验证标头的 API,例如基本身份验证、OAuth1a 和 OAuth2 身份验证以及简单的令牌身份验证方案。

.force_authenticate(user=None, token=None)

有时您可能希望完全绕过身份验证,并强制测试客户端的所有请求自动视为已通过身份验证。

如果您正在测试 API,但不想构造有效的身份验证凭据来进行测试请求,这可能是一个有用的快捷方式。

user = User.objects.get(username='lauren')
client = APIClient()
client.force_authenticate(user=user)

要取消对后续请求进行身份验证,请调用 force_authenticate,将用户和/或令牌设置为 None

client.force_authenticate(user=None)

CSRF 验证

默认情况下,在使用 APIClient 时不应用 CSRF 验证。如果您需要显式启用 CSRF 验证,可以在实例化客户端时设置 enforce_csrf_checks 标志来执行此操作。

client = APIClient(enforce_csrf_checks=True)

与往常一样,CSRF 验证仅适用于任何会话身份验证视图。这意味着仅当客户端通过调用 login() 登录后,才会进行 CSRF 验证。


RequestsClient

REST 框架还包括一个客户端,用于使用流行的 Python 库 requests 与您的应用程序进行交互。如果

  • 您希望主要从另一个 Python 服务与 API 进行交互,并希望在客户端将看到的同一级别上测试该服务。
  • 您希望以这样的方式编写测试,以便它们也可以针对暂存或实时环境运行。(请参阅下面的“实时测试”。)

这公开的界面与直接使用请求会话完全相同。

from rest_framework.test import RequestsClient

client = RequestsClient()
response = client.get('http://testserver/users/')
assert response.status_code == 200

请注意,请求客户端要求您传递完全限定的 URL。

RequestsClient 及与数据库配合使用

如果您希望编写仅与服务界面交互的测试,则 RequestsClient 类非常有用。这比使用标准的 Django 测试客户端要严格一些,因为它意味着所有交互都应通过 API 进行。

如果您使用的是 RequestsClient,您需要确保测试设置和结果断言作为常规 API 调用执行,而不是直接与数据库模型交互。例如,不要检查 Customer.objects.count() == 3,而是列出客户终端节点,并确保它包含三条记录。

标头和认证

自定义标头和身份验证凭据可以按照 使用标准 requests.Session 实例时的方式 提供。

from requests.auth import HTTPBasicAuth

client.auth = HTTPBasicAuth('user', 'pass')
client.headers.update({'x-test': 'true'})

CSRF

如果您使用的是 SessionAuthentication,则需要为任何 POSTPUTPATCHDELETE 请求包含一个 CSRF 令牌。

您可以通过遵循 JavaScript 客户端将使用的方法来做到这一点。首先,执行 GET 请求以获取 CSRF 令牌,然后在以下请求中显示该令牌。

例如...

client = RequestsClient()

# Obtain a CSRF token.
response = client.get('http://testserver/homepage/')
assert response.status_code == 200
csrftoken = response.cookies['csrftoken']

# Interact with the API.
response = client.post('http://testserver/organisations/', json={
    'name': 'MegaCorp',
    'status': 'active'
}, headers={'X-CSRFToken': csrftoken})
assert response.status_code == 200

实时测试

通过谨慎使用,RequestsClientCoreAPIClient 都能够编写可以在开发中运行或直接针对您的暂存服务器或生产环境运行的测试用例。

使用这种样式来创建几个核心功能的基本测试是验证您的实时服务的有力方法。这样做可能需要仔细注意设置和拆除,以确保测试以不会直接影响客户数据的方式运行。


CoreAPIClient

CoreAPIClient 允许您使用 Python coreapi 客户端库与您的 API 交互。

# Fetch the API schema
client = CoreAPIClient()
schema = client.get('http://testserver/schema/')

# Create a new organisation
params = {'name': 'MegaCorp', 'status': 'active'}
client.action(schema, ['organisations', 'create'], params)

# Ensure that the organisation exists in the listing
data = client.action(schema, ['organisations', 'list'])
assert(len(data) == 1)
assert(data == [{'name': 'MegaCorp', 'status': 'active'}])

标头和认证

自定义标头和身份验证可以与 CoreAPIClient 一起使用,方式与 RequestsClient 类似。

from requests.auth import HTTPBasicAuth

client = CoreAPIClient()
client.session.auth = HTTPBasicAuth('user', 'pass')
client.session.headers.update({'x-test': 'true'})

API 测试用例

REST 框架包括以下测试用例类,它们反映了现有的 Django 的测试用例类,但使用 APIClient 而不是 Django 的默认 Client

  • APISimpleTestCase
  • APITransactionTestCase
  • APITestCase
  • APILiveServerTestCase

示例

您可以像使用常规 Django 测试用例类一样使用 REST 框架的任何测试用例类。self.client 属性将是 APIClient 实例。

from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from myproject.apps.core.models import Account

class AccountTests(APITestCase):
    def test_create_account(self):
        """
        Ensure we can create a new account object.
        """
        url = reverse('account-list')
        data = {'name': 'DabApps'}
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
        self.assertEqual(Account.objects.count(), 1)
        self.assertEqual(Account.objects.get().name, 'DabApps')

URLPatternsTestCase

REST 框架还提供了一个测试用例类,用于按每个类隔离 urlpatterns。请注意,它继承自 Django 的 SimpleTestCase,并且很可能需要与另一个测试用例类混合。

示例

from django.urls import include, path, reverse
from rest_framework.test import APITestCase, URLPatternsTestCase


class AccountTests(APITestCase, URLPatternsTestCase):
    urlpatterns = [
        path('api/', include('api.urls')),
    ]

    def test_create_account(self):
        """
        Ensure we can create a new account object.
        """
        url = reverse('account-list')
        response = self.client.get(url, format='json')
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(len(response.data), 1)

测试响应

检查响应数据

在检查测试响应的有效性时,通常更方便检查创建响应的数据,而不是检查完全呈现的响应。

例如,检查 response.data 更容易

response = self.client.get('/users/4/')
self.assertEqual(response.data, {'id': 4, 'username': 'lauren'})

而不是检查 response.content 解析结果

response = self.client.get('/users/4/')
self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'})

呈现响应

如果你使用 APIRequestFactory 直接测试视图,返回的响应尚未呈现,因为模板响应的呈现是由 Django 的内部请求-响应周期执行的。为了访问 response.content,首先需要呈现响应。

view = UserDetail.as_view()
request = factory.get('/users/4')
response = view(request, pk='4')
response.render()  # Cannot access `response.content` without this.
self.assertEqual(response.content, '{"username": "lauren", "id": 4}')

配置

设置默认格式

用于进行测试请求的默认格式可以使用 TEST_REQUEST_DEFAULT_FORMAT 设置键设置。例如,要始终默认对测试请求使用 JSON 而非标准的多部分表单请求,请在 settings.py 文件中设置以下内容

REST_FRAMEWORK = {
    ...
    'TEST_REQUEST_DEFAULT_FORMAT': 'json'
}

设置可用格式

如果你需要使用多部分或 json 请求之外的其他内容测试请求,可以通过设置 TEST_REQUEST_RENDERER_CLASSES 设置来实现。

例如,要在测试请求中添加对使用 format='html' 的支持,可以在 settings.py 文件中添加类似以下内容。

REST_FRAMEWORK = {
    ...
    'TEST_REQUEST_RENDERER_CLASSES': [
        'rest_framework.renderers.MultiPartRenderer',
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.TemplateHTMLRenderer'
    ]
}