測試 Flask 應用程式¶
Flask 提供了用於測試應用程式的工具。本文件將介紹在測試中處理應用程式不同部分的技術。
我們將使用 pytest 框架來設定和執行我們的測試。
$ pip install pytest
教學 說明了如何為範例 Flaskr 部落格應用程式編寫 100% 覆蓋率的測試。請參閱 關於測試的教學,以詳細了解應用程式的特定測試。
識別測試¶
測試通常位於 tests
資料夾中。測試是開頭為 test_
的函式,位於開頭為 test_
的 Python 模組中。測試也可以進一步分組在開頭為 Test
的類別中。
可能難以知道要測試什麼。一般來說,嘗試測試您編寫的程式碼,而不是您使用的函式庫的程式碼,因為它們已經過測試。嘗試將複雜的行為提取為單獨的函式以進行個別測試。
夾具 (Fixtures)¶
Pytest 的夾具允許編寫可在測試之間重複使用的程式碼片段。簡單的夾具會傳回一個值,但夾具也可以進行設定、產生一個值,然後進行拆卸。應用程式、測試客戶端和 CLI 執行器的夾具如下所示,它們可以放置在 tests/conftest.py
中。
如果您使用 應用程式工廠,請定義一個 app
夾具來建立和配置應用程式實例。您可以在 yield
之前和之後新增程式碼,以設定和拆卸其他資源,例如建立和清除資料庫。
如果您未使用工廠,您已經有一個可以直接匯入和配置的應用程式物件。您仍然可以使用 app
夾具來設定和拆卸資源。
import pytest
from my_project import create_app
@pytest.fixture()
def app():
app = create_app()
app.config.update({
"TESTING": True,
})
# other setup can go here
yield app
# clean up / reset resources here
@pytest.fixture()
def client(app):
return app.test_client()
@pytest.fixture()
def runner(app):
return app.test_cli_runner()
使用測試客戶端發送請求¶
測試客戶端向應用程式發送請求,而無需執行即時伺服器。Flask 的客戶端擴展了 Werkzeug 的客戶端,請參閱這些文件以獲取更多資訊。
client
具有與常見 HTTP 請求方法相符的方法,例如 client.get()
和 client.post()
。它們接受許多參數來建構請求;您可以在 EnvironBuilder
中找到完整的文檔。通常您會使用 path
、query_string
、headers
和 data
或 json
。
要發出請求,請使用請求應使用的方法調用要測試的路徑。將傳回 TestResponse
以檢查回應資料。它具有回應物件的所有常用屬性。您通常會查看 response.data
,這是視圖傳回的位元組。如果您想使用文字,Werkzeug 2.1 提供了 response.text
,或使用 response.get_data(as_text=True)
。
def test_request_example(client):
response = client.get("/posts")
assert b"<h2>Hello, World!</h2>" in response.data
傳遞字典 query_string={"key": "value", ...}
以在查詢字串中設定引數(在 URL 中的 ?
之後)。傳遞字典 headers={}
以設定請求標頭。
要在 POST 或 PUT 請求中發送請求正文,請將值傳遞給 data
。如果傳遞原始位元組,則會使用該確切正文。通常,您會傳遞字典以設定表單資料。
表單資料¶
要發送表單資料,請將字典傳遞給 data
。Content-Type
標頭將自動設定為 multipart/form-data
或 application/x-www-form-urlencoded
。
如果值是以位元組讀取模式 ("rb"
模式) 開啟的檔案物件,它將被視為上傳檔案。要更改偵測到的檔案名稱和內容類型,請傳遞 (file, filename, content_type)
元組。檔案物件將在發出請求後關閉,因此它們不需要使用常用的 with open() as f:
模式。
將檔案儲存在 tests/resources
資料夾中可能很有用,然後使用 pathlib.Path
來獲取相對於目前測試檔案的檔案。
from pathlib import Path
# get the resources folder in the tests folder
resources = Path(__file__).parent / "resources"
def test_edit_user(client):
response = client.post("/user/2/edit", data={
"name": "Flask",
"theme": "dark",
"picture": (resources / "picture.png").open("rb"),
})
assert response.status_code == 200
JSON 資料¶
要發送 JSON 資料,請將物件傳遞給 json
。Content-Type
標頭將自動設定為 application/json
。
同樣地,如果回應包含 JSON 資料,response.json
屬性將包含反序列化的物件。
def test_json_data(client):
response = client.post("/graphql", json={
"query": """
query User($id: String!) {
user(id: $id) {
name
theme
picture_url
}
}
""",
variables={"id": 2},
})
assert response.json["data"]["user"]["name"] == "Flask"
追蹤重新導向¶
預設情況下,如果回應是重新導向,客戶端不會發出額外請求。透過將 follow_redirects=True
傳遞給請求方法,客戶端將繼續發出請求,直到傳回非重新導向回應。
TestResponse.history
是導致最終回應的回應元組。每個回應都有一個 request
屬性,該屬性記錄了產生該回應的請求。
def test_logout_redirect(client):
response = client.get("/logout", follow_redirects=True)
# Check that there was one redirect response.
assert len(response.history) == 1
# Check that the second request was to the index page.
assert response.request.path == "/index"
存取和修改 Session¶
要存取 Flask 的上下文變數,主要是 session
,請在 with
語句中使用客戶端。應用程式和請求上下文將在發出請求後保持活動狀態,直到 with
區塊結束。
from flask import session
def test_access_session(client):
with client:
client.post("/auth/login", data={"username": "flask"})
# session is still accessible
assert session["user_id"] == 1
# session is no longer accessible
如果您想在發出請求之前存取或設定 session 中的值,請在 with
語句中使用客戶端的 session_transaction()
方法。它會傳回一個 session 物件,並在區塊結束時儲存 session。
from flask import session
def test_modify_session(client):
with client.session_transaction() as session:
# set a user id without going through the login route
session["user_id"] = 1
# session is saved now
response = client.get("/users/me")
assert response.json["username"] == "flask"
使用 CLI 執行器執行命令¶
Flask 提供了 test_cli_runner()
來建立 FlaskCliRunner
,它在隔離環境中執行 CLI 命令並將輸出捕獲到 Result
物件中。Flask 的執行器擴展了 Click 的執行器,請參閱這些文件以獲取更多資訊。
使用執行器的 invoke()
方法以與從命令列使用 flask
命令調用命令相同的方式調用命令。
import click
@app.cli.command("hello")
@click.option("--name", default="World")
def hello_command(name):
click.echo(f"Hello, {name}!")
def test_hello_command(runner):
result = runner.invoke(args="hello")
assert "World" in result.output
result = runner.invoke(args=["hello", "--name", "Flask"])
assert "Flask" in result.output
依賴活動上下文的測試¶
您可能有從視圖或命令調用的函式,它們期望活動的 應用程式上下文 或 請求上下文,因為它們存取 request
、session
或 current_app
。您可以直接建立並啟動上下文,而不是透過發出請求或調用命令來測試它們。
使用 with app.app_context()
來推送應用程式上下文。例如,資料庫擴展通常需要活動的應用程式上下文才能進行查詢。
def test_db_post_model(app):
with app.app_context():
post = db.session.query(Post).get(1)
使用 with app.test_request_context()
來推送請求上下文。它接受與測試客戶端的請求方法相同的引數。
def test_validate_user_edit(app):
with app.test_request_context(
"/user/2/edit", method="POST", data={"name": ""}
):
# call a function that accesses `request`
messages = validate_edit_user()
assert messages["name"][0] == "Name cannot be empty."
建立測試請求上下文不會執行任何 Flask 分發程式碼,因此不會調用 before_request
函式。如果您需要調用這些函式,通常最好改為發出完整請求。但是,可以手動調用它們。
def test_auth_token(app):
with app.test_request_context("/user/2/edit", headers={"X-Auth-Token": "1"}):
app.preprocess_request()
assert g.user.name == "Flask"