測試工具¶
Django 提供一小組方便撰寫測試的工具。
測試客戶端¶
測試客戶端是一個 Python 類別,其作用如同一個虛擬的網頁瀏覽器,讓您能以程式化的方式測試您的檢視並與您的 Django 應用程式互動。
您可以使用測試客戶端執行以下操作
模擬 URL 的 GET 和 POST 請求並觀察回應 – 從低階 HTTP (結果標頭和狀態碼) 到頁面內容的所有項目。
查看重定向鏈 (如果有的話) 並檢查每個步驟的 URL 和狀態碼。
測試給定的請求是否由給定的 Django 範本呈現,且範本內容包含特定的值。
請注意,測試客戶端並非旨在取代 Selenium 或其他「瀏覽器內」框架。Django 的測試客戶端有不同的重點。簡而言之
使用 Django 的測試客戶端來確認是否正在呈現正確的範本,以及範本是否傳遞了正確的內容資料。
使用
RequestFactory
直接測試檢視函式,繞過路由和中介層。使用瀏覽器內框架 (例如 Selenium) 來測試 呈現的 HTML 和網頁的行為,即 JavaScript 功能。Django 也為這些框架提供特殊的支援;請參閱
LiveServerTestCase
一節以取得更多詳細資訊。
一個全面的測試套件應結合所有這些測試類型。
總覽和快速範例¶
若要使用測試客戶端,請實例化 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
。
請注意關於測試客戶端運作方式的幾點重要事項
測試客戶端不需要執行網頁伺服器。事實上,即使完全沒有網頁伺服器執行,它也能正常運作!這是因為它避免了 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, raise_request_exception=True, json_encoder=DjangoJSONEncoder, *, headers=None, query_params=None, **defaults)[原始碼]¶
一個測試 HTTP 客戶端。接受數個可用於自訂行為的引數。
headers
允許您指定將隨每個請求傳送的預設標頭。例如,若要設定User-Agent
標頭client = Client(headers={"user-agent": "curl/7.79.1"})
query_params
允許您指定將在每個請求上設定的預設查詢字串。**defaults
中的任意關鍵字引數會設定 WSGI environ 變數。例如,若要設定指令碼名稱client = Client(SCRIPT_NAME="/app/")
注意
以
HTTP_
字首開頭的關鍵字引數會設定為標頭,但為了提高可讀性,應優先使用headers
參數。傳遞至
get()
、post()
等的headers
、query_params
和extra
關鍵字引數的值,會優先於傳遞至類別建構函式的預設值。可以使用
enforce_csrf_checks
引數來測試 CSRF 保護 (請參閱上方)。raise_request_exception
引數允許控制在請求期間引發的例外是否也應該在測試中引發。預設為True
。json_encoder
引數允許為post()
中描述的 JSON 序列化設定自訂的 JSON 編碼器。在 Django 5.1 中變更已新增
query_params
引數。一旦您有了
Client
實例,您就可以呼叫下列任何方法- get(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra)[原始碼]¶
在提供的
path
上發出 GET 請求並傳回Response
物件,如下所述。query_params
字典中的鍵值對用於設定查詢字串。例如>>> c = Client() >>> c.get("/customers/details/", query_params={"name": "fred", "age": 7})
…將產生相當於下列項目的 GET 請求評估結果
/customers/details/?name=fred&age=7
也可以將這些參數傳遞至
data
參數。但是,由於query_params
適用於任何 HTTP 方法,因此優先使用它。headers
參數可用於指定要在請求中傳送的標頭。例如>>> c = Client() >>> c.get( ... "/customers/details/", ... query_params={"name": "fred", "age": 7}, ... headers={"accept": "application/json"}, ... )
…將會傳送 HTTP 標頭
HTTP_ACCEPT
到詳細檢視畫面,這是一個測試使用django.http.HttpRequest.accepts()
方法的程式碼路徑的好方法。任意關鍵字參數會設定 WSGI 環境變數。例如,設定腳本名稱的標頭:
>>> c = Client() >>> c.get("/", SCRIPT_NAME="/app/")
如果您已經有 URL 編碼格式的 GET 參數,您可以使用該編碼,而不是使用 data 參數。例如,先前的 GET 請求也可以表示為:
>>> c = Client() >>> c.get("/customers/details/?name=fred&age=7")
如果您提供的 URL 同時具有編碼的 GET 資料以及 query_params 或 data 參數,這些參數將優先處理。
如果您將
follow
設定為True
,客戶端將會追蹤任何重新導向,並且會在回應物件中設定一個redirect_chain
屬性,其中包含中間 URL 和狀態碼的元組。如果您有一個 URL
/redirect_me/
重新導向到/next/
,而/next/
又重新導向到/final/
,您將會看到以下內容:>>> response = c.get("/redirect_me/", follow=True) >>> response.redirect_chain [('http://testserver/next/', 302), ('http://testserver/final/', 302)]
如果您將
secure
設定為True
,客戶端將會模擬 HTTPS 請求。在 Django 5.1 中變更已新增
query_params
引數。
- post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, *, headers=None, query_params=None, **extra)[原始碼]¶
在提供的
path
上發出 POST 請求並返回一個Response
物件,該物件在下面有說明。data
字典中的鍵值對用於提交 POST 資料。例如:>>> c = Client() >>> c.post("/login/", {"name": "fred", "passwd": "secret"})
…將導致對此 URL 進行 POST 請求的評估
/login/
…以及此 POST 資料
name=fred&passwd=secret
如果您將
content_type
提供為 application/json,則data
會在使用json.dumps()
序列化(如果它是 dict、list 或 tuple)。序列化預設會使用DjangoJSONEncoder
執行,並且可以透過提供json_encoder
參數給Client
來覆寫。此序列化也適用於put()
、patch()
和delete()
請求。如果您提供任何其他
content_type
(例如,用於 XML 酬載的 text/xml),則data
的內容會按原樣在 POST 請求中傳送,並在 HTTPContent-Type
標頭中使用content_type
。如果您沒有為
content_type
提供值,則data
中的值將會以 multipart/form-data 的內容類型傳輸。在這種情況下,data
中的鍵值對將會被編碼為多部分訊息,並用於建立 POST 資料酬載。要為給定的鍵提交多個值(例如,為
<select multiple>
指定選取項目),請將這些值以清單或元組的形式提供給所需的鍵。例如,data
的此值將會為名為choices
的欄位提交三個選取的值{"choices": ["a", "b", "d"]}
提交檔案是一種特殊情況。要 POST 檔案,您只需要提供檔案欄位名稱作為鍵,並將您想要上傳的檔案的檔案控制代碼作為值。例如,如果您的表單具有欄位
name
和attachment
,後者是一個FileField
>>> c = Client() >>> with open("wishlist.doc", "rb") as fp: ... c.post("/customers/wishes/", {"name": "fred", "attachment": fp}) ...
您也可以提供任何類似檔案的物件(例如,
StringIO
或BytesIO
)作為檔案控制代碼。如果您要上傳到ImageField
,則該物件需要一個name
屬性,以通過validate_image_file_extension
驗證器。例如:>>> from io import BytesIO >>> img = BytesIO( ... b"GIF89a\x01\x00\x01\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00" ... b"\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x01\x00\x00" ... ) >>> img.name = "myimage.gif"
請注意,如果您希望在多個
post()
呼叫中使用相同的檔案控制代碼,則需要在 POST 之間手動重設檔案指標。執行此操作最簡單的方法是在將檔案提供給post()
後手動關閉檔案,如上所示。您還應確保以允許讀取資料的方式開啟檔案。如果您的檔案包含二進位資料(例如影像),這表示您需要以
rb
(讀取二進位)模式開啟檔案。headers
、query_params
和extra
參數的作用與Client.get()
相同。如果您使用 POST 請求的 URL 包含編碼的參數,這些參數將會在 request.GET 資料中可用。例如,如果您要提出以下請求:
>>> c.post( ... "/login/", {"name": "fred", "passwd": "secret"}, query_params={"visitor": "true"} ... )
…處理此請求的檢視可以查詢 request.POST 來檢索使用者名稱和密碼,並可以查詢 request.GET 來確定使用者是否為訪客。
如果您將
follow
設定為True
,客戶端將會追蹤任何重新導向,並且會在回應物件中設定一個redirect_chain
屬性,其中包含中間 URL 和狀態碼的元組。如果您將
secure
設定為True
,客戶端將會模擬 HTTPS 請求。在 Django 5.1 中變更已新增
query_params
引數。
- head(path, data=None, follow=False, secure=False, *, headers=None, query_params=None, **extra)[原始碼]¶
在提供的
path
上發出 HEAD 請求並返回一個Response
物件。此方法的作用與Client.get()
相同,包括follow
、secure
、headers
、query_params
和extra
參數,但它不返回訊息主體。在 Django 5.1 中變更已新增
query_params
引數。
- options(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)[原始碼]¶
對提供的
path
發出 OPTIONS 請求,並返回一個Response
物件。適用於測試 RESTful 介面。當提供
data
時,它將被用作請求主體,並且Content-Type
標頭會被設定為content_type
。follow
、secure
、headers
、query_params
和extra
參數的作用與Client.get()
相同。在 Django 5.1 中變更已新增
query_params
引數。
- put(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)[原始碼]¶
對提供的
path
發出 PUT 請求,並返回一個Response
物件。適用於測試 RESTful 介面。當提供
data
時,它將被用作請求主體,並且Content-Type
標頭會被設定為content_type
。follow
、secure
、headers
、query_params
和extra
參數的作用與Client.get()
相同。在 Django 5.1 中變更已新增
query_params
引數。
- patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)[原始碼]¶
對提供的
path
發出 PATCH 請求,並返回一個Response
物件。適用於測試 RESTful 介面。follow
、secure
、headers
、query_params
和extra
參數的作用與Client.get()
相同。在 Django 5.1 中變更已新增
query_params
引數。
- delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, *, headers=None, query_params=None, **extra)[原始碼]¶
對提供的
path
發出 DELETE 請求,並返回一個Response
物件。適用於測試 RESTful 介面。當提供
data
時,它將被用作請求主體,並且Content-Type
標頭會被設定為content_type
。follow
、secure
、headers
、query_params
和extra
參數的作用與Client.get()
相同。在 Django 5.1 中變更已新增
query_params
引數。
- trace(path, follow=False, secure=False, *, headers=None, query_params=None, **extra)[原始碼]¶
對提供的
path
發出 TRACE 請求,並返回一個Response
物件。適用於模擬診斷探測。與其他請求方法不同,為了符合 RFC 9110 第 9.3.8 節,
data
未作為關鍵字參數提供,該節規定 TRACE 請求不得有主體。follow
、secure
、headers
、query_params
和extra
參數的作用與Client.get()
相同。在 Django 5.1 中變更已新增
query_params
引數。
- login(**credentials)¶
- alogin(**credentials)¶
非同步版本:
alogin()
如果您的網站使用 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()
輔助方法建立具有正確雜湊密碼的新使用者。在 Django 5.0 中變更新增了
alogin()
方法。
- force_login(user, backend=None)¶
- aforce_login(user, backend=None)¶
非同步版本:
aforce_login()
如果您的網站使用 Django 的 身份驗證系統,您可以使用
force_login()
方法來模擬使用者登入網站的效果。當測試需要使用者登入,且使用者如何登入的細節並不重要時,請使用此方法來取代login()
。與
login()
不同,此方法會跳過身份驗證和驗證步驟:不活動的使用者(is_active=False
)也可以登入,且不需要提供使用者的憑證。使用者的
backend
屬性會被設定為backend
參數的值(應該是一個點分隔的 Python 路徑字串),如果沒有提供值,則設定為settings.AUTHENTICATION_BACKENDS[0]
。authenticate()
函式通常由login()
呼叫,以這種方式註解使用者。此方法比
login()
快,因為它會跳過昂貴的密碼雜湊演算法。此外,您也可以透過在測試時使用較弱的雜湊演算法來加快login()
的速度。在 Django 5.0 中變更已新增
aforce_login()
方法。
- logout()¶
- alogout()¶
非同步版本:
alogout()
如果您的網站使用 Django 的 身份驗證系統,則可以使用
logout()
方法來模擬使用者登出網站的效果。在您呼叫此方法後,測試用戶端將清除所有 cookie 和 session 資料為預設值。後續的請求將會像是來自
AnonymousUser
。在 Django 5.0 中變更已新增
alogout()
方法。
測試回應¶
get()
和 post()
方法都會傳回一個 Response
物件。此 Response
物件與 Django 視圖傳回的 HttpResponse
物件不同;測試回應物件有一些額外的資料,可用於測試程式碼進行驗證。
具體來說,Response
物件具有下列屬性
- class Response¶
- client¶
用於發出產生回應的請求的測試用戶端。
- content¶
回應的主體,為位元組字串。這是視圖呈現的最終頁面內容,或任何錯誤訊息。
- context¶
用於呈現產生回應內容的範本的範本
Context
實例。如果呈現的頁面使用了多個範本,則
context
將會是一個Context
物件的列表,其順序為它們被呈現的順序。無論在呈現過程中使用了多少個範本,您都可以使用
[]
運算子來擷取上下文值。例如,可以使用以下方式擷取上下文變數name
>>> response = client.get("/foo/") >>> response.context["name"] 'Arthur'
沒有使用 Django 範本?
此屬性僅在使用
DjangoTemplates
後端時才會填入。如果您使用其他範本引擎,則context_data
在具有該屬性的回應上可能是一個合適的替代方案。
- 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 狀態,為整數。如需已定義程式碼的完整列表,請參閱 IANA 狀態碼註冊表。
- templates¶
用於呈現最終內容的
Template
實例列表,順序為它們被呈現的順序。對於列表中的每個範本,請使用template.name
來取得範本的檔案名稱(如果範本是從檔案載入)。 (名稱是一個字串,例如'admin/index.html'
。)沒有使用 Django 範本?
此屬性僅在使用
DjangoTemplates
後端時才會填入。如果您使用其他範本引擎,如果您只需要用於呈現的範本名稱,template_name
可能是一個合適的替代方案。
- 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 compare the view_class, as the # functions generated by as_view() won't be equal. self.assertIs(response.resolver_match.func.view_class, MyView)
如果找不到給定的 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 的當前值。請參閱http.cookies
模組的文件以了解更多資訊。
設定語言¶
在測試支援國際化和本地化的應用程式時,您可能需要為測試客戶端請求設定語言。這樣做的方法取決於是否啟用 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("/", headers={"accept-language": "fr"})
self.assertEqual(response.content, b"Bienvenue sur mon site.")
注意
當使用這些方法時,請確保在每個測試結束時重置活動語言
def tearDown(self):
translation.activate(settings.LANGUAGE_CODE)
更多詳細資訊請參閱 Django 如何發現語言偏好。
如果未啟用中介軟體,可以使用 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)
提供的測試案例類別¶
正常的 Python 單元測試類別會擴展 unittest.TestCase
的基礎類別。 Django 提供了此基礎類別的一些擴展
Django 單元測試類別的層次結構¶
您可以將正常的 unittest.TestCase
轉換為任何子類別:將測試的基礎類別從 unittest.TestCase
變更為子類別。所有標準 Python 單元測試功能都將可用,並且它將使用下面每個章節中描述的一些有用新增功能進行增強。
SimpleTestCase
¶
unittest.TestCase
的子類別,它新增了此功能
一些有用的斷言,例如
能夠使用 修改後的設定 執行測試。
如果您的測試會進行任何資料庫查詢,請使用子類別 TransactionTestCase
或 TestCase
。
- SimpleTestCase.databases¶
預設情況下,
SimpleTestCase
禁止資料庫查詢。這有助於避免執行寫入查詢,因為每個SimpleTestCase
測試並非在交易中執行,所以會影響其他測試。如果您不擔心這個問題,您可以將測試類別的databases
類別屬性設定為'__all__'
來停用此行為。
警告
SimpleTestCase
及其子類別(例如 TestCase
,...)依賴 setUpClass()
和 tearDownClass()
來執行一些類別範圍的初始化(例如覆寫設定)。如果您需要覆寫這些方法,請不要忘記呼叫 super
實作。
class MyTestCase(TestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
...
@classmethod
def tearDownClass(cls):
...
super().tearDownClass()
如果 setUpClass()
期間引發例外,請務必考慮 Python 的行為。如果發生這種情況,類別中的測試和 tearDownClass()
都不會執行。以 django.test.TestCase
為例,這會洩漏在 super()
中建立的交易,導致各種症狀,包括在某些平台(在 macOS 上回報)上的區段錯誤。如果您想在 setUpClass()
中故意引發例外,例如 unittest.SkipTest
,請務必在呼叫 super()
之前執行此操作,以避免這種情況。
TransactionTestCase
¶
TransactionTestCase
繼承自 SimpleTestCase
以新增一些特定於資料庫的功能
在每個測試開始時將資料庫重設為已知狀態,以方便測試和使用 ORM。
資料庫
fixtures
。測試 基於資料庫後端功能跳過測試。
其餘專業化的
assert*
方法。
Django 的 TestCase
類別是 TransactionTestCase
更常用的子類別,它利用資料庫交易機制來加速在每個測試開始時將資料庫重設為已知狀態的過程。然而,這樣做的一個後果是,某些資料庫行為無法在 Django TestCase
類別中進行測試。例如,您無法測試一段程式碼是否在交易中執行,就像使用 select_for_update()
時所需要的那樣。在這種情況下,您應該使用 TransactionTestCase
。
TransactionTestCase
和 TestCase
除了將資料庫重設為已知狀態的方式,以及測試程式碼測試 commit 和 rollback 效果的能力之外,其他方面是相同的
TransactionTestCase
在測試執行後會透過截斷所有表格來重設資料庫。TransactionTestCase
可以呼叫 commit 和 rollback,並觀察這些呼叫對資料庫的影響。另一方面,
TestCase
在測試後不會截斷表格。相反地,它將測試程式碼包在資料庫交易中,該交易會在測試結束時回滾。這保證了測試結束時的回滾會將資料庫還原到初始狀態。
警告
在不支援回滾的資料庫上執行的 TestCase
(例如使用 MyISAM 儲存引擎的 MySQL)以及所有 TransactionTestCase
實例,都會在測試結束時透過刪除測試資料庫中的所有資料來回滾。
應用程式 將不會看到它們的資料重新載入;如果您需要此功能(例如,第三方應用程式應啟用此功能),您可以在 TestCase
主體內設定 serialized_rollback = True
。
TestCase
¶
這是 Django 中撰寫測試時最常用的類別。它繼承自 TransactionTestCase
(並延伸繼承自 SimpleTestCase
)。如果您的 Django 應用程式未使用資料庫,請使用 SimpleTestCase
。
此類別
將測試包在兩個巢狀的
atomic()
區塊中:一個用於整個類別,一個用於每個測試。因此,如果您想測試一些特定的資料庫交易行為,請使用TransactionTestCase
。在每個測試結束時檢查可延遲的資料庫約束。
它還提供了一個額外的方法
- classmethod TestCase.setUpTestData()[原始碼]¶
上述的類別層級
atomic
區塊允許在類別層級建立初始資料,針對整個TestCase
只建立一次。與使用setUp()
相比,此技術可以加快測試速度。例如
from django.test import TestCase class MyTests(TestCase): @classmethod def setUpTestData(cls): # Set up data for the whole TestCase cls.foo = Foo.objects.create(bar="Test") ... def test1(self): # Some test using self.foo ... def test2(self): # Some other test using self.foo ...
請注意,如果測試是在不支援交易的資料庫上執行(例如,使用 MyISAM 引擎的 MySQL),則會在每次測試之前呼叫
setUpTestData()
,抵消速度上的優勢。在
setUpTestData()
中指派給類別屬性的物件必須支援使用copy.deepcopy()
建立深層複製,以便將它們與每個測試方法執行的變更隔離。
- classmethod TestCase.captureOnCommitCallbacks(using=DEFAULT_DB_ALIAS, execute=False)[原始碼]¶
回傳一個內容管理器,用於捕獲指定資料庫連線的
transaction.on_commit()
回呼。它會回傳一個列表,其中包含在內容管理器結束時捕獲的回呼函式。您可以從這個列表中斷言回呼,或者呼叫它們來調用它們的副作用,以模擬提交。using
是要捕獲回呼的資料庫連線的別名。如果
execute
為True
,則在內容管理器結束時,如果沒有發生例外,將會呼叫所有回呼。這模擬了程式碼區塊包裹後的提交。例如
from django.core import mail from django.test import TestCase class ContactTests(TestCase): def test_post(self): with self.captureOnCommitCallbacks(execute=True) as callbacks: response = self.client.post( "/contact/", {"message": "I like your site"}, ) self.assertEqual(response.status_code, 200) self.assertEqual(len(callbacks), 1) self.assertEqual(len(mail.outbox), 1) self.assertEqual(mail.outbox[0].subject, "Contact Form") self.assertEqual(mail.outbox[0].body, "I like your site")
LiveServerTestCase
¶
LiveServerTestCase
的基本功能與 TransactionTestCase
相同,但多了一個額外功能:它會在設定時在背景啟動一個即時 Django 伺服器,並在拆卸時關閉它。這允許使用除了 Django 虛擬客戶端 之外的自動測試客戶端,例如 Selenium 客戶端,在瀏覽器內執行一系列功能測試並模擬真實使用者的操作。
即時伺服器會在 localhost
上監聽,並繫結到埠 0,這會使用作業系統分配的可用埠。伺服器的 URL 可以在測試期間使用 self.live_server_url
存取。
為了示範如何使用 LiveServerTestCase
,我們來編寫一個 Selenium 測試。首先,您需要安裝 selenium 套件
$ python -m pip install "selenium >= 4.8.0"
...\> py -m pip install "selenium >= 4.8.0"
然後,將一個基於 LiveServerTestCase
的測試新增到您應用程式的測試模組(例如:myapp/tests.py
)。在這個範例中,我們假設您正在使用 staticfiles
應用程式,並且想要在測試執行期間提供靜態檔案,這類似於我們在開發期間使用 DEBUG=True
時的情況,也就是說,不必使用 collectstatic
來收集它們。我們將使用 StaticLiveServerTestCase
子類別,它提供了該功能。如果您不需要該功能,請將其替換為 django.test.LiveServerTestCase
。
此測試的程式碼可能如下所示
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.webdriver import WebDriver
class MySeleniumTests(StaticLiveServerTestCase):
fixtures = ["user-data.json"]
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.selenium = WebDriver()
cls.selenium.implicitly_wait(10)
@classmethod
def tearDownClass(cls):
cls.selenium.quit()
super().tearDownClass()
def test_login(self):
self.selenium.get(f"{self.live_server_url}/login/")
username_input = self.selenium.find_element(By.NAME, "username")
username_input.send_keys("myuser")
password_input = self.selenium.find_element(By.NAME, "password")
password_input.send_keys("secret")
self.selenium.find_element(By.XPATH, '//input[@value="Log in"]').click()
最後,您可以如下所示執行測試
$ ./manage.py test myapp.tests.MySeleniumTests.test_login
...\> manage.py test myapp.tests.MySeleniumTests.test_login
此範例會自動開啟 Firefox,然後前往登入頁面,輸入憑證並按下「登入」按鈕。如果您的電腦上未安裝 Firefox 或想要使用其他瀏覽器,Selenium 會提供其他驅動程式。上面的範例僅是 Selenium 客戶端可以執行的一小部分;請參閱 完整參考文件 以了解更多詳細資訊。
注意
當使用記憶體中的 SQLite 資料庫來執行測試時,即時伺服器執行的執行緒和測試案例執行的執行緒將會平行共用相同的資料庫連線。務必防止兩個執行緒透過這個共用的連線同時進行資料庫查詢,因為這有時可能會隨機導致測試失敗。因此,您需要確保兩個執行緒不會同時存取資料庫。特別是,這表示在某些情況下(例如,在按一下連結或提交表單後),您可能需要檢查 Selenium 是否收到回應,以及是否已載入下一個頁面,然後才能繼續執行進一步的測試。例如,透過讓 Selenium 等待直到在回應中找到 <body>
HTML 標籤來執行此操作(需要 Selenium > 2.13)
def test_login(self):
from selenium.webdriver.support.wait import WebDriverWait
timeout = 2
...
self.selenium.find_element(By.XPATH, '//input[@value="Log in"]').click()
# Wait until the response is received
WebDriverWait(self.selenium, timeout).until(
lambda driver: driver.find_element(By.TAG_NAME, "body")
)
這裡的棘手之處在於,實際上沒有所謂的「頁面載入」,尤其是在現代 Web 應用程式中,它們會在伺服器產生初始文件後動態產生 HTML。因此,檢查回應中是否存在 <body>
可能不一定適用於所有用例。請參閱 Selenium 常見問題和 Selenium 文件以了解更多資訊。
測試案例功能¶
預設測試客戶端¶
- SimpleTestCase.client¶
在 django.test.*TestCase
實例中的每個測試案例都可以存取 Django 測試客戶端的實例。此客戶端可以作為 self.client
存取。這個客戶端會為每個測試重新建立,因此您不必擔心狀態(例如 Cookie)從一個測試帶到另一個測試。
這表示,不必在每個測試中實例化 Client
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)
自訂測試客戶端¶
- SimpleTestCase.client_class¶
如果您想要使用不同的 Client
類別(例如,具有自訂行為的子類別),請使用 client_class
類別屬性
from django.test import Client, TestCase
class MyTestClient(Client):
# Specialized methods for your environment
...
class MyTest(TestCase):
client_class = MyTestClient
def test_my_stuff(self):
# Here self.client is an instance of MyTestClient...
call_some_test_code()
載入固定裝置¶
- TransactionTestCase.fixtures¶
如果資料庫中沒有任何資料,那麼用於資料庫後端的網站的測試案例類別就沒什麼用處。使用 ORM 建立物件,例如在 TestCase.setUpTestData()
中,可以讓測試更具可讀性,並且更容易維護,但是,您也可以使用 固定裝置。
固定裝置是 Django 知道如何匯入資料庫的一組資料。例如,如果您的網站有使用者帳戶,您可能會設定一個虛假使用者帳戶的固定裝置,以便在測試期間填入您的資料庫。
建立固定裝置最直接的方法是使用 manage.py dumpdata
命令。這假設您的資料庫中已有一些資料。有關更多詳細資訊,請參閱 dumpdata 文件
。
一旦您建立了一個固定裝置並將其放置在您其中一個 INSTALLED_APPS
的 fixtures
目錄中,您就可以透過在您的 django.test.TestCase
子類別上指定 fixtures
類別屬性,在您的單元測試中使用它
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 test_fluffy_animals(self):
# A test that uses the fixtures.
call_some_test_code()
以下是將會發生的具體事項
在每個測試開始時,在執行
setUp()
之前,Django 將會清除資料庫,使資料庫返回到在呼叫migrate
之後的狀態。然後,將會安裝所有指定的固定裝置。在本例中,Django 將會安裝任何名為
mammals
的 JSON 固定裝置,然後安裝任何名為birds
的固定裝置。有關定義和安裝固定裝置的更多詳細資訊,請參閱 固定裝置 主題。
為了效能考量,TestCase
會在整個測試類別中只載入一次固定裝置,在 setUpTestData()
之前,而不是在每個測試之前,並且它會使用交易在每個測試之前清除資料庫。在任何情況下,您都可以確定測試的結果不會受到另一個測試或測試執行順序的影響。
預設情況下,固定裝置僅會載入到 default
資料庫中。如果您正在使用多個資料庫並設定 TransactionTestCase.databases
,固定裝置將會載入到所有指定的資料庫中。
URLconf 設定¶
如果您的應用程式提供檢視,您可能想要包含使用測試客戶端來執行這些檢視的測試。但是,最終使用者可以自由地選擇在他們選擇的任何 URL 上部署您應用程式中的檢視。這表示您的測試不能依賴於您的檢視會在特定 URL 上可用的事實。使用 @override_settings(ROOT_URLCONF=...)
裝飾您的測試類別或測試方法以進行 URLconf 設定。
多資料庫支援¶
- TransactionTestCase.databases¶
Django 會針對您設定中 DATABASES
定義的每個資料庫建立對應的測試資料庫,且至少有一個測試透過 databases
參考這些資料庫。
然而,執行 Django TestCase
所需時間很大一部分是由 flush
呼叫所消耗,這可確保您在每次測試執行開始時都有一個乾淨的資料庫。如果您有多個資料庫,則需要多次 flush(每個資料庫一次),這可能很耗時 – 特別是如果您的測試不需要測試多資料庫活動時。
作為最佳化,Django 只會在每次測試執行開始時 flush default
資料庫。如果您的設定包含多個資料庫,而且您的測試需要每個資料庫都乾淨,您可以使用測試套件上的 databases
屬性來請求 flush 額外的資料庫。
例如
class TestMyViews(TransactionTestCase):
databases = {"default", "other"}
def test_index_page_view(self):
call_some_test_code()
此測試案例類別會在執行 test_index_page_view
前 flush default
和 other
測試資料庫。您也可以使用 '__all__'
來指定必須 flush 所有測試資料庫。
databases
標記也控制將 TransactionTestCase.fixtures
載入到哪些資料庫。預設情況下,fixtures 只會載入到 default
資料庫中。
針對不在 databases
中的資料庫發出的查詢會產生斷言錯誤,以防止測試之間的狀態洩漏。
- TestCase.databases¶
預設情況下,只有 default
資料庫會在 TestCase
執行期間被包裝在事務中,嘗試查詢其他資料庫會導致斷言錯誤,以防止測試之間的狀態洩漏。
使用測試類別上的 databases
類別屬性來請求針對非 default
資料庫進行事務包裝。
例如
class OtherDBTests(TestCase):
databases = {"other"}
def test_other_db_query(self): ...
此測試只允許對 other
資料庫進行查詢。就像 SimpleTestCase.databases
和 TransactionTestCase.databases
一樣,可以使用 '__all__'
常數來指定測試應允許查詢所有資料庫。
覆寫設定¶
警告
使用以下函數來暫時變更測試中的設定值。請勿直接操作 django.conf.settings
,因為 Django 不會在這些操作後還原原始值。
為了測試目的,暫時變更設定並在執行測試程式碼後還原為原始值通常很有用。對於這種使用案例,Django 提供了一個標準的 Python 上下文管理器(請參閱 PEP 343)稱為 settings()
,可以像這樣使用
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
設定,並在之後將其值重設為先前的狀態。
重新定義包含值列表的設定可能很麻煩。實際上,新增或移除值通常就足夠了。Django 提供 modify_settings()
上下文管理器以方便設定變更
from django.test import TestCase
class MiddlewareTestCase(TestCase):
def test_cache_middleware(self):
with self.modify_settings(
MIDDLEWARE={
"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
不會有任何影響;當值不存在時,remove
也沒有任何影響。
如果您想為測試方法覆寫設定,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/")
同樣地,Django 提供 modify_settings()
裝飾器
from django.test import TestCase, modify_settings
class MiddlewareTestCase(TestCase):
@modify_settings(
MIDDLEWARE={
"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={
"append": "django.middleware.cache.FetchFromCacheMiddleware",
"prepend": "django.middleware.cache.UpdateCacheMiddleware",
}
)
class MiddlewareTestCase(TestCase):
def test_cache_middleware(self):
response = self.client.get("/")
# ...
注意
當給定類別時,這些裝飾器會直接修改類別並傳回;它們不會建立並傳回修改後的副本。因此,如果您嘗試調整上述範例,將傳回值指定為與 LoginTestCase
或 MiddlewareTestCase
不同的名稱,您可能會驚訝地發現原始測試案例類別仍然同樣受到裝飾器的影響。對於給定的類別,modify_settings()
始終在 override_settings()
之後應用。
警告
設定檔包含一些僅在 Django 內部初始化期間才會查詢的設定。如果您使用 override_settings
變更它們,如果您透過 django.conf.settings
模組存取該設定,則該設定會變更,但是,Django 的內部會以不同的方式存取它。實際上,使用 override_settings()
或 modify_settings()
這些設定可能不會產生您預期的效果。
我們不建議變更 DATABASES
設定。變更 CACHES
設定是可行的,但如果您使用內部使用快取的模組(例如 django.contrib.sessions
)可能會有點棘手。例如,您必須在測試中使用快取工作階段並覆寫 CACHES
的情況下重新初始化工作階段後端。
最後,避免將設定別名設定為模組級別常數,因為 override_settings()
不會對這些值起作用,因為它們僅在第一次匯入模組時才會求值。
您也可以在覆寫設定後刪除設定來模擬設定不存在,如下所示
@override_settings()
def test_something(self):
del settings.LOGIN_URL
...
當覆寫設定時,請務必處理應用程式程式碼使用快取或類似功能的情況,這些功能即使在設定變更後仍會保留狀態。Django 提供了 django.test.signals.setting_changed
信號,讓您可以註冊回呼函式,以便在設定變更時清除或重置狀態。
Django 本身使用此信號來重置各種資料
覆寫的設定 |
資料重置 |
---|---|
USE_TZ、TIME_ZONE |
資料庫時區 |
TEMPLATES |
樣板引擎 |
FORM_RENDERER |
預設渲染器 |
SERIALIZATION_MODULES |
序列化器快取 |
LOCALE_PATHS、LANGUAGE_CODE |
預設翻譯和載入的翻譯 |
STATIC_ROOT、STATIC_URL、STORAGES |
儲存體組態 |
已新增在 FORM_RENDERER
設定變更時重置預設渲染器的功能。
隔離應用程式¶
- utils.isolate_apps(*app_labels, attr_name=None, kwarg_name=None)¶
將封裝內容中定義的模型註冊到它們自己的隔離
apps
註冊表中。此功能在為測試建立模型類別時很有用,因為這些類別之後會被乾淨地刪除,而且不會有名稱衝突的風險。隔離的註冊表應包含的應用程式標籤必須作為個別引數傳遞。您可以使用
isolate_apps()
作為裝飾器或上下文管理器。例如from django.db import models from django.test import SimpleTestCase from django.test.utils import isolate_apps class MyModelTests(SimpleTestCase): @isolate_apps("app_label") def test_model_definition(self): class TestModel(models.Model): pass ...
… 或
with isolate_apps("app_label"): class TestModel(models.Model): pass ...
裝飾器形式也可以應用於類別。
可以指定兩個可選的關鍵字引數
attr_name
:如果用作類別裝飾器,則會將隔離的註冊表指定給此屬性。kwarg_name
:如果用作函式裝飾器,則會以此關鍵字引數傳遞隔離的註冊表。
當使用類別裝飾器時,可以使用
attr_name
參數將用於隔離模型註冊的暫時Apps
實例作為屬性檢索@isolate_apps("app_label", attr_name="apps") class TestModelDefinition(SimpleTestCase): def test_model_definition(self): class TestModel(models.Model): pass self.assertIs(self.apps.get_model("app_label", "TestModel"), TestModel)
… 或者,當使用方法裝飾器時,可以使用
kwarg_name
參數將其作為測試方法上的引數檢索class TestModelDefinition(SimpleTestCase): @isolate_apps("app_label", kwarg_name="apps") def test_model_definition(self, apps): class TestModel(models.Model): pass self.assertIs(apps.get_model("app_label", "TestModel"), TestModel)
清空測試寄件匣¶
如果您使用任何 Django 的自訂 TestCase
類別,測試執行器將在每個測試案例開始時清除測試電子郵件寄件匣的內容。
有關測試期間電子郵件服務的更多詳細資訊,請參閱下方的電子郵件服務。
斷言¶
由於 Python 的一般 unittest.TestCase
類別實作了諸如 assertTrue()
和 assertEqual()
等斷言方法,Django 的自訂 TestCase
類別提供了一些自訂斷言方法,這些方法對於測試 Web 應用程式很有用
大多數這些斷言方法提供的失敗訊息都可以使用 msg_prefix
引數進行自訂。此字串將會附加到斷言產生的任何失敗訊息之前。這讓您可以提供額外的詳細資訊,以協助您找出測試套件中失敗的位置和原因。
- SimpleTestCase.assertRaisesMessage(expected_exception, expected_message, callable, *args, **kwargs)[原始碼]¶
- SimpleTestCase.assertRaisesMessage(expected_exception, expected_message)
斷言執行
callable
會引發expected_exception
,且在例外狀況訊息中找到expected_message
。任何其他結果都會回報為失敗。它是unittest.TestCase.assertRaisesRegex()
的較簡單版本,不同之處在於expected_message
不會被視為正規表示式。如果只給定
expected_exception
和expected_message
參數,則會傳回上下文管理器,以便可以將受測程式碼內嵌撰寫,而不是作為函式with self.assertRaisesMessage(ValueError, "invalid literal for int()"): int("a")
- SimpleTestCase.assertWarnsMessage(expected_warning, expected_message, callable, *args, **kwargs)[原始碼]¶
- SimpleTestCase.assertWarnsMessage(expected_warning, expected_message)
類似於
SimpleTestCase.assertRaisesMessage()
,但用於assertWarnsRegex()
而不是assertRaisesRegex()
。
- SimpleTestCase.assertFieldOutput(fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value='')[原始碼]¶
斷言表單欄位在各種輸入下的行為是否正確。
- 參數:
fieldclass – 要測試的欄位類別。
valid – 將有效輸入對應到其預期清理值的字典。
invalid – 將無效輸入對應到一或多個引發錯誤訊息的字典。
field_args – 傳遞給執行個體化欄位的 args。
field_kwargs – 傳遞給執行個體化欄位的 kwargs。
empty_value –
empty_values
中輸入的預期清理輸出。
例如,以下程式碼測試
EmailField
接受a@a.com
作為有效電子郵件地址,但拒絕aaa
並顯示合理的錯誤訊息self.assertFieldOutput( EmailField, {"a@a.com": "a@a.com"}, {"aaa": ["Enter a valid email address."]} )
- SimpleTestCase.assertFormError(form, field, errors, msg_prefix='')[原始碼]¶
斷言表單上的欄位是否會引發提供的錯誤列表。
form
是Form
實例。表單必須是已繫結的,但不一定要驗證 (assertFormError()
會自動在表單上呼叫full_clean()
)。field
是要檢查的表單欄位名稱。若要檢查表單的非欄位錯誤
,請使用field=None
。errors
是欄位預期會有的所有錯誤字串的列表。如果您只預期有一個錯誤,也可以傳遞單一錯誤字串,這表示errors='error message'
等同於errors=['error message']
。
- SimpleTestCase.assertFormSetError(formset, form_index, field, errors, msg_prefix='')[原始碼]¶
斷言當渲染
formset
時會產生提供的錯誤列表。formset
是一個FormSet
實例。表單集必須被綁定,但不一定要驗證(assertFormSetError()
會自動在表單集上呼叫full_clean()
)。form_index
是FormSet
內表單的編號(從 0 開始)。使用form_index=None
來檢查表單集的非表單錯誤,也就是當呼叫formset.non_form_errors()
時會得到的錯誤。在這種情況下,您也必須使用field=None
。field
和errors
的含義與assertFormError()
的參數相同。
- SimpleTestCase.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False)[原始碼]¶
斷言
response
產生了指定的status_code
,並且text
出現在其content
中。如果提供了count
,則text
必須在回應中精確出現count
次。將
html
設定為True
以將text
作為 HTML 處理。與回應內容的比較將基於 HTML 語意,而不是逐字元相等。在大多數情況下,空格會被忽略,屬性順序並不重要。有關更多詳細資訊,請參閱assertHTMLEqual()
。在 Django 5.1 中變更在舊版本中,錯誤訊息不包含回應內容。
- SimpleTestCase.assertNotContains(response, text, status_code=200, msg_prefix='', html=False)[原始碼]¶
斷言
response
產生了指定的status_code
,並且text
沒有 出現在其content
中。將
html
設定為True
以將text
作為 HTML 處理。與回應內容的比較將基於 HTML 語意,而不是逐字元相等。在大多數情況下,空格會被忽略,屬性順序並不重要。有關更多詳細資訊,請參閱assertHTMLEqual()
。在 Django 5.1 中變更在舊版本中,錯誤訊息不包含回應內容。
- SimpleTestCase.assertTemplateUsed(response, template_name, msg_prefix='', count=None)[原始碼]¶
斷言在渲染回應時使用了具有指定名稱的模板。
response
必須是由測試 客戶端
返回的回應實例。template_name
應該是一個字串,例如'admin/index.html'
。count
引數是一個整數,表示應該渲染模板的次數。預設值為None
,表示應該渲染模板一次或多次。您可以將其用作上下文管理器,如下所示
with self.assertTemplateUsed("index.html"): render_to_string("index.html") with self.assertTemplateUsed(template_name="index.html"): render_to_string("index.html")
- SimpleTestCase.assertTemplateNotUsed(response, template_name, msg_prefix='')[原始碼]¶
斷言在渲染回應時沒有 使用具有指定名稱的模板。
您可以使用與
assertTemplateUsed()
相同的方式將其用作上下文管理器。
- SimpleTestCase.assertURLEqual(url1, url2, msg_prefix='')[原始碼]¶
斷言兩個 URL 相同,忽略查詢字串參數的順序,但具有相同名稱的參數除外。例如,
/path/?x=1&y=2
等於/path/?y=2&x=1
,但/path/?a=1&a=2
不等於/path/?a=2&a=1
。
- SimpleTestCase.assertRedirects(response, expected_url, status_code=302, target_status_code=200, msg_prefix='', fetch_redirect_response=True)[原始碼]¶
斷言
response
返回了status_code
重新導向狀態,重新導向到expected_url
(包括任何GET
資料),並且使用target_status_code
接收到最終頁面。如果您的請求使用了
follow
引數,則expected_url
和target_status_code
將是重新導向鏈最終點的 URL 和狀態碼。如果
fetch_redirect_response
是False
,則不會載入最終頁面。由於測試客戶端無法擷取外部 URL,如果expected_url
不是您 Django 應用程式的一部分,這會特別有用。在比較兩個 URL 時,會正確處理 Scheme。如果我們重新導向到的位置中沒有指定任何 Scheme,則會使用原始請求的 Scheme。如果存在,
expected_url
中的 Scheme 將用於進行比較。
- SimpleTestCase.assertHTMLEqual(html1, html2, msg=None)[原始碼]¶
斷言字串
html1
和html2
相等。此比較基於 HTML 語意。比較時會考量以下事項:HTML 標籤前後的空白會被忽略。
所有類型的空白都被視為等效。
所有開啟的標籤都會隱式關閉,例如當周圍的標籤關閉或 HTML 文件結束時。
空標籤與其自閉合版本等效。
HTML 元素屬性的順序並不重要。
沒有參數的布林屬性(如
checked
)與名稱和值相等的屬性相等(請參閱範例)。指向相同字元的文字、字元參照和實體參照是等效的。
以下範例是有效的測試,不會引發任何
AssertionError
。self.assertHTMLEqual( "<p>Hello <b>'world'!</p>", """<p> Hello <b>'world'! </b> </p>""", ) self.assertHTMLEqual( '<input type="checkbox" checked="checked" id="id_accept_terms" />', '<input id="id_accept_terms" type="checkbox" checked>', )
html1
和html2
必須包含 HTML。如果其中一個無法被解析,將會引發AssertionError
。可以使用
msg
參數自訂錯誤時的輸出。
- SimpleTestCase.assertHTMLNotEqual(html1, html2, msg=None)[原始碼]¶
斷言字串
html1
和html2
*不* 相等。此比較基於 HTML 語意。詳情請參閱assertHTMLEqual()
。html1
和html2
必須包含 HTML。如果其中一個無法被解析,將會引發AssertionError
。可以使用
msg
參數自訂錯誤時的輸出。
- SimpleTestCase.assertXMLEqual(xml1, xml2, msg=None)[原始碼]¶
斷言字串
xml1
和xml2
相等。此比較基於 XML 語意。與assertHTMLEqual()
類似,比較是在解析的內容上進行的,因此只會考量語意上的差異,而不是語法上的差異。當任何參數中傳入無效的 XML 時,即使兩個字串相同,也會總是引發AssertionError
。XML 宣告、文件類型、處理指令和註解都會被忽略。只會比較根元素及其子元素。
可以使用
msg
參數自訂錯誤時的輸出。
- SimpleTestCase.assertXMLNotEqual(xml1, xml2, msg=None)[原始碼]¶
斷言字串
xml1
和xml2
*不* 相等。此比較基於 XML 語意。詳情請參閱assertXMLEqual()
。可以使用
msg
參數自訂錯誤時的輸出。
- SimpleTestCase.assertInHTML(needle, haystack, count=None, msg_prefix='')[原始碼]¶
斷言 HTML 片段
needle
在haystack
中出現一次。如果指定了整數引數
count
,則還會嚴格驗證needle
的出現次數。在大多數情況下,空白會被忽略,且屬性順序並不重要。詳情請參閱
assertHTMLEqual()
。在 Django 5.1 中變更在較舊的版本中,錯誤訊息不包含
haystack
。
- SimpleTestCase.assertNotInHTML(needle, haystack, msg_prefix='')[原始碼]¶
- 在 Django 5.1 中新增。
斷言 HTML 片段
needle
*不* 包含在haystack
中。在大多數情況下,空白會被忽略,且屬性順序並不重要。詳情請參閱
assertHTMLEqual()
。
- SimpleTestCase.assertJSONEqual(raw, expected_data, msg=None)[原始碼]¶
斷言 JSON 片段
raw
和expected_data
相等。通常 JSON 中不重要的空白規則適用,因為繁重的工作已委派給json
函式庫。可以使用
msg
參數自訂錯誤時的輸出。
- SimpleTestCase.assertJSONNotEqual(raw, expected_data, msg=None)[原始碼]¶
斷言 JSON 片段
raw
和expected_data
*不* 相等。詳情請參閱assertJSONEqual()
。可以使用
msg
參數自訂錯誤時的輸出。
- TransactionTestCase.assertQuerySetEqual(qs, values, transform=None, ordered=True, msg=None)[原始碼]¶
斷言 queryset
qs
與特定的值可迭代物件values
相符。如果提供了
transform
,則會將values
與透過將transform
應用於qs
的每個成員所產生的清單進行比較。預設情況下,比較也與順序相關。如果
qs
沒有提供隱式排序,您可以將ordered
參數設定為False
,這會將比較轉換為collections.Counter
比較。如果順序未定義(如果給定的qs
未排序,且比較對象為多個已排序的值),則會引發ValueError
。可以使用
msg
參數自訂錯誤時的輸出。
- TransactionTestCase.assertNumQueries(num, func, *args, **kwargs)[原始碼]¶
斷言當使用
*args
和**kwargs
呼叫func
時,會執行num
次資料庫查詢。如果
kwargs
中存在"using"
鍵,它將被用作檢查查詢次數的資料庫別名。self.assertNumQueries(7, using="non_default_db")
如果您希望使用
using
參數呼叫函式,您可以使用lambda
包裝呼叫以新增額外參數。self.assertNumQueries(7, lambda: my_function(using=7))
您也可以將其用作上下文管理器。
with self.assertNumQueries(2): Person.objects.create(name="Aaron") Person.objects.create(name="Daniel")
標記測試¶
您可以標記您的測試,以便輕鬆執行特定的子集。 例如,您可以標記快速或慢速測試。
from django.test import tag
class SampleTestCase(TestCase):
@tag("fast")
def test_fast(self): ...
@tag("slow")
def test_slow(self): ...
@tag("slow", "core")
def test_slow_but_core(self): ...
您也可以標記測試案例類別。
@tag("slow", "core")
class SampleTestCase(TestCase): ...
子類別會繼承超類別的標籤,而方法會繼承其類別的標籤。 給定
@tag("foo")
class SampleTestCaseChild(SampleTestCase):
@tag("bar")
def test(self): ...
SampleTestCaseChild.test
將會被標記為 'slow'
、'core'
、'bar'
和 'foo'
。
然後您可以選擇要執行的測試。例如,僅執行快速測試
$ ./manage.py test --tag=fast
...\> manage.py test --tag=fast
或執行快速測試和核心測試 (即使它是慢速的)
$ ./manage.py test --tag=fast --tag=core
...\> manage.py test --tag=fast --tag=core
您也可以按標籤排除測試。要執行核心測試,如果它們不是慢速的
$ ./manage.py test --tag=core --exclude-tag=slow
...\> manage.py test --tag=core --exclude-tag=slow
test --exclude-tag
優先於 test --tag
,因此如果測試有兩個標籤,而您選擇其中一個並排除另一個,則不會執行該測試。
測試非同步程式碼¶
如果您只是想測試非同步視圖的輸出,標準測試客戶端將在它們自己的非同步迴圈中執行它們,而無需您額外進行任何操作。
但是,如果您想為 Django 專案編寫完全非同步的測試,則需要考慮幾個事項。
首先,您的測試必須是測試類別上的 async def
方法(以便為它們提供非同步上下文)。Django 會自動偵測任何 async def
測試,並將其包裝以便它們在自己的事件迴圈中執行。
如果您從非同步函式進行測試,則還必須使用非同步測試客戶端。這可以透過 django.test.AsyncClient
或任何測試上的 self.async_client
來取得。
- class AsyncClient(enforce_csrf_checks=False, raise_request_exception=True, *, headers=None, query_params=None, **defaults)[原始碼]¶
AsyncClient
與同步(正常)測試客戶端具有相同的方法和簽名,但有以下例外
在初始化中,
defaults
中的任意關鍵字引數會直接新增到 ASGI 範圍中。作為
extra
關鍵字引數傳遞的標頭不應具有同步客戶端要求的HTTP_
前綴(請參閱Client.get()
)。例如,以下是如何設定 HTTPAccept
標頭的方法>>> c = AsyncClient() >>> c.get("/customers/details/", {"name": "fred", "age": 7}, ACCEPT="application/json")
已將對 follow
參數的支援新增至 AsyncClient
。
已新增 query_params
引數。
使用 AsyncClient
時,必須等待任何發出請求的方法。
async def test_my_thing(self):
response = await self.async_client.get("/some-url/")
self.assertEqual(response.status_code, 200)
非同步客戶端也可以呼叫同步視圖;它會通過 Django 的 非同步請求路徑 執行,該路徑同時支援兩者。透過 AsyncClient
呼叫的任何視圖,其 request
都會取得 ASGIRequest
物件,而不是普通客戶端建立的 WSGIRequest
。
警告
如果您正在使用測試裝飾器,它們必須與非同步相容,以確保它們能正常運作。Django 的內建裝飾器會正常運作,但第三方裝飾器可能會顯示為未執行 (它們會「包裝」執行流程的錯誤部分,而不是您的測試)。
如果您需要使用這些裝飾器,則應該在測試方法中使用 async_to_sync()
在裡面 裝飾它們。
from asgiref.sync import async_to_sync
from django.test import TestCase
class MyTests(TestCase):
@mock.patch(...)
@async_to_sync
async def test_my_thing(self): ...
電子郵件服務¶
如果您的任何 Django 視圖使用 Django 的電子郵件功能傳送電子郵件,您可能不希望每次使用該視圖執行測試時都傳送電子郵件。因此,Django 的測試執行器會自動將所有 Django 傳送的電子郵件重新導向到虛擬寄件匣。這讓您可以測試傳送電子郵件的每個方面,從傳送的訊息數量到每則訊息的內容,而無需實際傳送訊息。
測試執行器透過以測試後端透明地取代正常的電子郵件後端來完成此作業。(請放心,這不會對 Django 以外的任何其他電子郵件傳送者造成影響,例如如果您正在執行的機器郵件伺服器。)
- django.core.mail.outbox¶
在測試執行期間,每則外寄電子郵件都會儲存在 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 io import StringIO
from django.core.management import call_command
from django.test import TestCase
class ClosepollTest(TestCase):
def test_command_output(self):
out = StringIO()
call_command("closepoll", poll_ids=[1], stdout=out)
self.assertIn('Successfully closed poll "1"', out.getvalue())
略過測試¶
unittest 程式庫提供了 @skipIf
和 @skipUnless
裝飾器,讓您可以在預先知道這些測試在特定條件下會失敗時略過它們。
例如,如果您的測試需要特定的選用程式庫才能成功,您可以使用 @skipIf
裝飾測試案例。然後,測試執行器會回報該測試未執行及其原因,而不是使測試失敗或完全省略測試。
為了補充這些測試略過行為,Django 提供了兩個額外的略過裝飾器。這些裝飾器不是測試一般布林值,而是檢查資料庫的功能,如果資料庫不支援特定的具名功能,則會略過測試。
裝飾器使用字串識別碼來描述資料庫功能。此字串對應於資料庫連線功能類別的屬性。請參閱 django.db.backends.base.features.BaseDatabaseFeatures 類別,以取得可用於略過測試的資料庫功能的完整清單。
如果所有指定的資料庫功能都支援,則跳過裝飾過的測試或 TestCase
。
例如,如果資料庫支援交易 (例如,在 PostgreSQL 下將會不執行,但在使用 MyISAM 資料表的 MySQL 下會執行),則以下測試不會執行:
class MyTests(TestCase):
@skipIfDBFeature("supports_transactions")
def test_transaction_behavior(self):
# ... conditional test code
pass
如果任何指定的資料庫功能不支援,則跳過裝飾過的測試或 TestCase
。
例如,如果資料庫支援交易 (例如,在 PostgreSQL 下會執行,但在使用 MyISAM 資料表的 MySQL 下則不會執行),則以下測試只會執行:
class MyTests(TestCase):
@skipUnlessDBFeature("supports_transactions")
def test_transaction_behavior(self):
# ... conditional test code
pass