Django REST framework API 指南(26):測試

wcode發表於2018-03-24

官方原文連結
本系列文章 github 地址
轉載請註明出處

測試

REST framework 包含一些擴充套件 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 framework 的 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 和 token 中的任一個或兩個。

例如,當使用令牌強行進行身份驗證時,你可能會執行以下操作:

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 framework 的 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 framework 檢視時不會應用 CSRF 驗證。如果你需要明確開啟 CSRF 驗證,則可以通過在例項化工廠時設定 enforce_csrf_checks 標誌來實現。

factory = APIRequestFactory(enforce_csrf_checks=True)
複製程式碼

注意: 值得注意的是,Django 的標準 RequestFactory 不需要包含這個選項,因為當使用常規的 Django 時,CSRF 驗證發生在中介軟體中,當直接測試檢視時該中介軟體不執行。在使用 REST framework 時,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,例如包含 AJAX 與 API 互動的網站。

.credentials(**kwargs)

credentials 方法可用於設定 header,這些 header 將包含在測試客戶端的所有後續請求中。

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 方法適用於測試需要驗證 header 的 API,例如 basic 驗證,OAuth1 和 OAuth2 驗證以及簡單令牌驗證方案。

.force_authenticate(user=None, token=None)

有時你可能想完全繞過認證,強制測試客戶端的所有請求被自動視為已認證。

如果你正在測試 API 但是不想構建有效的身份驗證憑據以進行測試請求,則這可能是一個有用的捷徑。

user = User.objects.get(username='lauren')
client = APIClient()
client.force_authenticate(user=user)
複製程式碼

要對後續請求進行身份驗證,請呼叫 force_authenticate 將 user 和(或) token 設定為 None

client.force_authenticate(user=None)
複製程式碼

CSRF 驗證

預設情況下,使用 APIClient 時不應用 CSRF 驗證。如果你需要明確啟用 CSRF 驗證,則可以通過在例項化客戶端時設定 enforce_csrf_checks 標誌來實現。

client = APIClient(enforce_csrf_checks=True)
複製程式碼

像往常一樣,CSRF 驗證將僅適用於任何會話驗證檢視。這意味著 CSRF 驗證只有在客戶端通過呼叫 login() 登入後才會發生。


RequestsClient

REST framework 還包含一個客戶端,用於使用流行的 Python 庫 requests 與應用程式進行互動。 這可能是有用的,如果:

  • 你期望主要從另一個 Python 服務與 API 進行互動,並且希望在與客戶端相同的級別測試該服務。
  • 你希望以這樣的方式編寫測試,以便它們也可以在分段或實時環境中執行。

它暴露了與直接使用請求會話完全相同的介面。

client = RequestsClient()
response = client.get('http://testserver/users/')
assert response.status_code == 200
複製程式碼

請注意,requests client 要求你傳遞完全限定的 URL。

RequestsClient 與資料庫一起工作

如果你想編寫僅與服務介面互動的測試,則 RequestsClient 類很有用。這比使用標準的 Django 測試客戶端要嚴格一些,因為這意味著所有的互動必須通過 API。

如果你使用的是 RequestsClient,你需要確保測試設定和結果斷言以常規 API 呼叫的方式執行,而不是直接與資料庫模型進行互動。例如,不是檢查 Customer.objects.count() == 3,而是列出 customers 端點,並確保它包含三條記錄。

Headers & 身份驗證

自定義 header 和身份驗證憑證的提供方式與使用標準 requests.Session 例項時的方式相同。

from requests.auth import HTTPBasicAuth

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

CSRF

如果你使用 SessionAuthentication ,那麼你需要為 POST, PUT, PATCHDELETE 請求包含一個 CSRF 令牌。

你可以通過遵循基於 JavaScript 的客戶端使用的相同流程來實現。首先進行 GET 請求以獲取 CRSF 令牌,然後在以下請求中呈現該令牌。

例如...

client = RequestsClient()

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

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

Live tests

使用 RequestsClientCoreAPIClient 可以編寫在開發環境中執行的測試用例,也可以直接根據測試伺服器或生產環境執行測試用例。

使用這種風格來建立幾個核心功能的基本測試是驗證你的實時服務的有效方法。這樣做可能需要仔細注意安裝和解除安裝(setup and teardown),以確保測試的執行方式不會直接影響客戶資料。


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'}])
複製程式碼

Headers & 身份驗證

自定義 header 和身份驗證可以與 RequestsClient 類似的方式和 CoreAPIClient 一起使用。

from requests.auth import HTTPBasicAuth

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

API Test cases

REST framework 包含以下測試用例類,它們類似現有的 Django 測試用例類,但使用 API​​Client 而不是 Django 的預設 Client

  • APISimpleTestCase
  • APITransactionTestCase
  • APITestCase
  • APILiveServerTestCase

舉個栗子

你可以像使用常規 Django 測試用例類一樣使用任何 REST framework 的測試用例類。 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 framework 還提供了一個用於隔離每個類的 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'})
複製程式碼

渲染響應

如果你使用 API​​RequestFactory 直接測試檢視,則返回的響應將不會渲染,因為模板響應的渲染由 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 setting key 進行設定。例如,預設情況下總是對測試請求使用 JSON 而不是標準的 multipart 表單請求,請在 settings.py 檔案中設定以下內容:

REST_FRAMEWORK = {
    ...
    'TEST_REQUEST_DEFAULT_FORMAT': 'json'
}
複製程式碼

設定可用的格式

如果你需要使用除 multipart 或 json 請求之外的其他方法來測試請求,則可以通過設定 TEST_REQUEST_RENDERER_CLASSES setting 來完成。

例如,要在測試請求中新增對 format ='html' 的支援,您可能在 settings.py 檔案中有這樣的內容。

REST_FRAMEWORK = {
    ...
    'TEST_REQUEST_RENDERER_CLASSES': (
        'rest_framework.renderers.MultiPartRenderer',
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.TemplateHTMLRenderer'
    )
}
複製程式碼