تست کردن برنامه های فلاسک¶
فلاسک ابزارهایی را برای آزمایش یک برنامه ارائه می دهد. این مستندات به تکنیکهای کار با بخشهای مختلف برنامه در آزمایشها میپردازد.
ما از فریمورک pytest برای تنظیم و اجرای تست های خود استفاده خواهیم کرد.
$ pip install pytest
بخش آموزش به نحوه نوشتن تست برای پوشش۱۰۰ درصدی نمونه برنامه وبلاگ Flaskr می پردازد. برای توضیح دقیق تستهای خاص برای یک برنامه، به آموزش تست ها مراجعه کنید.
شناسایی تست ها¶
تستها معمولاً در پوشه tests
قرار دارند. تست ها توابعی هستند که با test_
شروع می شوند در ماژول هایی که اول آنها نیز test_
دارند شروع میشوند. همچنین میتوان تست ها را در کلاسهایی که با Test
شروع میشوند گروهبندی کرد.
دانستن اینکه چه چیزی را باید آزمایش کنید ممکن است دشوار باشد. به طور کلی، سعی کنید کدهایی را که می نویسید تست کنید، نه کد کتابخانه هایی که استفاده می کنید، زیرا قبلاً آزمایش شده اند. سعی کنید رفتارهای پیچیده را به عنوان توابع جداگانه استخراج کنید تا به صورت جداگانه آزمایش کنید.
فیکسچرس(Fixtures)¶
Pytest fixtures اجازه می دهد تا قطعات کدی را بنویسید که در تست ها قابل استفاده مجدد هستند. یک فیکسچر ساده یک مقدار را برمیگرداند، اما یک فیکسچر همچنین میتواند راهاندازی کند، مقداری تولید کند و سپس خرابی را انجام دهد. وسایل مربوط به برنامه، سرویس گیرنده آزمایشی و اجراکننده CLI در زیر نشان داده شده است، آنها را می توان در tests/conftest.py
قرار داد.
اگر از یک Application Factory استفاده می کنید، برای ایجاد و پیکربندی یک نمونه برنامه، فیکسچر 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()
درخواست با کلاینت تست¶
کلاینت تست بدون اجرای یک لایو سرور درخواست هایی برای برنامه ایجاد میکند. کلاینت تست فلاسک شامل Werkzeug کلاینت است که برای اطلاعات بیشتر میتوانید به اسناد آن مراجعه کنید.
client
دارای روش هایی است که با روش های رایج درخواست HTTP، مانند client.get()
و client.post()
مطابقت دارد. آنها استدلال های زیادی را برای ایجاد درخواست می گیرند. می توانید مستندات کامل را در EnvironBuilder
بیابید. معمولا از path
، query_string
، headers
، data
یا json
. استفاده میکنید.
برای درخواست، روشی را که درخواست باید از آن استفاده کند با مسیر مسیری که باید آزمایش کند، فراخوانی کنید. یک TestResponse
برای بررسی داده های پاسخ بازگردانده می شود. تمام خصوصیات معمول یک شی پاسخگو را دارد. شما معمولاً به response.data
نگاه می کنید، که بایت های بازگردانده شده توسط view است. اگر میخواهید از متن استفاده کنید، response.get_data(as_text=True)
را ارائه می دهد، یا از نیز میتوانید استفاده کنید.
def test_request_example(client):
response = client.get("/posts")
assert b"<h2>Hello, World!</h2>" in response.data
برای تنظیم آرگومان ها در رشته کوئری (بعد از ?
در URL) یک دستور query_string={"key": "value"، ...}
ارسال کنید. همچنین اگر میخواهید مقدار خاصی را مستقیماً تنظیم کنید، میتوانید یک رشته را ارسال کنید.
برای ارسال بدنه درخواست در یک درخواست POST یا PUT، یک مقدار را به data
ارسال کنید. اگر بایت های خام ارسال شوند، دقیقاً از آن متن استفاده می شود. معمولاً برای تنظیم دادههای فرم یک دستور ارسال میکنید.
فرم داده¶
برای ارسال فرم داده، یک دیکشنری به data
ارسال کنید. هدر Content-Type
به صورت خودکار روی multipart/form-data
یا application/x-www-form-urlencoded
automatically تنظیم میشود.
اگر یک مقدار برای آپلود فایل ها برای خواندن بایت های آن فایل(در مود "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")
# 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
در کلاینت با عبارت 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_transaction()
کلاینت را در عبارت with
استفاده کنید. یک شی جلسه را برمی گرداند و پس از پایان بلوک، جلسه را ذخیره می کند.
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 Runner¶
test_cli_runner()
برای ایجاد FlaskCliRunner
ارائه میدهد که این دستورات CLI را به صورت مجزا اجرا میکند و خروجی را در یک شی Result
میگیرد. برای اطلاعات بیشتر درباره Runner فلاسک میتوانید Click's runner را ببینید.
از متود 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 context یا application context فعال باشد زیرا به 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."
ایجاد زمینه درخواست آزمایشی هیچ یک از کدهای ارسال فلاسک را اجرا نمی کند، بنابراین توابع 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"