تست کردن برنامه های فلاسک

فلاسک ابزارهایی را برای آزمایش یک برنامه ارائه می دهد. این مستندات به تکنیک‌های کار با بخش‌های مختلف برنامه در آزمایش‌ها می‌پردازد.

ما از فریمورک 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"