آشنایی با موضوع
تستکردن یکی از قسمتهای ضروری فرآیند توسعهی نرمافزار است که اطمینان حاصل میکند کد همانطور که انتظار میرود رفتار کرده و عاری از نقص باشد. در زبان برنامهنویسی پایتون، pytest
یک چارچوب تست محبوب است که برتریهایی نسبت به ماژول استاندارد unittest
دارد که خود از ماژولهای تست داخلی پایتون بوده و بخشی از کتابخانهی استاندارد است. pytest
شامل یک نحو سادهتر، خروجی بهتر، فیکسچرهای قدرتمند و یک اکوسیستم غنی از پلاگینها است. این آموزش شما را در راهاندازی یک برنامه Flask، ادغام فیکسچرهای pytest
و نوشتن تستهای واحد با استفاده از pytest
راهنمایی خواهد کرد.
پیشنیازها
قبل از شروع، نیازمند موارد زیر خواهید بود:
- یک سرور که روی آن سیستمعامل اوبونتو نصب شده و کاربری غیر-ریشه با دسترسیهای sudo و یک دیوار آتش فعال دارید. جهت دریافت راهنمایی چگونگی انجام این کار در توزیع مورد نظر خود به لیست توزیعها مراجعه کنید و از راهنمای راهاندازی اولیهی سرور ما پیروی کنید. لطفا از کار کردن با یک [نسخهی پشتیبانیشده](https://releases.ubuntu.com/) اوبونتو اطمینان حاصل فرمایید.
- آشنایی با خط فرمان لینوکس.
- درک پایهای از برنامهنویسی پایتون و چارچوب تست
pytest
در پایتون.
پایتون نسخهی 3.7 یا بالاتر را بر روی سیستم اوبونتوی خود نصب داشته باشید. برای یادگیری چگونگی اجرای اسکریپت پایتون در اوبونتو، میتوانید به آموزش ما در مورد مراجعه کنید.
چرا pytest
بهعنوان جایگزینی بهتر از unittest
است
pytest
برتریهایی نسبت به چارچوب تست داخلی unittest
دارد:
- Pytest به شما اجازه میدهد تا تست ها را با کمترین کدهای تکراری بنویسید و از گزارههای ادعای ساده به جای متدهای پرحجمتر مورد نیاز توسط
unittest
استفاده کنید.
- خروجیهای دقیقتر و قابل خواندنتری فراهم میکند، که شناسایی دلیل و محل شکست یک تست را سادهتر میسازد.
- فیکسچرهای Pytest اجازه میدهند که تنظیمات تست قابل انعطافتری و قابل استفادهی مجدد داشته باشید، در مقایسه با متدهای
setUp
وtearDown
در unittest.
- اجرای یک تابع تست با مجموعههای ورودی گوناگون را آسان میسازد، که در
unittest
به این راحتی نیست.
- Pytest مجموعهای غنی از پلاگینها را دارد که قابلیتهای آن را توسعه میدهند، از ابزارهای پوشش کد گرفته تا اجرای موازی تستها.
- بهصورت خودکار فایلها و توابع تست را که با قراردادهای نامگذاری آن مطابقت دارند، کشف میکند، که وقت و تلاش در مدیریت مجموعههای تست را صرفهجویی میکند.
با در نظر گرفتن این مزایا، pytest
غالبا گزینهی مورد ترجیح برای تستکردن مدرن پایتون است. حالا بیایید یک برنامه Flask راهاندازی کنیم و تستهای واحد را با استفاده از pytest
بنویسیم.
گام نخست – راهاندازی محیط
اوبونتو 24.04 پایتون 3 را بهطور پیشفرض به همراه دارد.
ترمینال را باز کرده و دستور زیر را برای بررسی نصب Python 3 اجرا کنید:
root@ubuntu:~# python3 --version
Python 3.8.5
Code language: Bash (bash)
اگر Python 3 در حال حاضر بر روی دستگاه شما نصب شده باشد، دستور فوق نسخه کنونی نصب Python 3 را باز میگرداند. در صورتی که نصب نشده باشد، میتوانید دستور زیر را برای نصب Python 3 اجرا کنید:
root@ubuntu:~# sudo apt install python3
Code language: Bash (bash)
سپس، باید نصاب بسته pip
را بر روی سیستم خود نصب کنید:
root@ubuntu:~# sudo apt install python3-pip
Code language: Bash (bash)
پس از نصب pip
، حال بیایید Flask را نصب کنیم.
گام دوم – ایجاد یک برنامه Flask
برای شروع، یک برنامه Flask ساده ایجاد کنیم. یک دایرکتوری جدید برای پروژه خود ایجاد کرده و وارد آن شوید:
root@ubuntu:~# mkdir flask_testing_app
root@ubuntu:~# cd flask_testing_app
Code language: Bash (bash)
هم اکنون، بیایید یک محیط مجازی ایجاد کرده و فعال کنیم تا وابستگیها را مدیریت کنیم:
root@ubuntu:~# python3 -m venv venv
root@ubuntu:~# source venv/bin/activate
Code language: Bash (bash)
Flask را با استفاده از pip
نصب کنید:
<code data-shcb-language-name="bash">
root@ubuntu:~# pip install Flask
</code>
Code language: Bash (bash)
اکنون، بیایید یک برنامه Flask ساده ایجاد کنیم. یک فایل جدید با نام app.py
ایجاد کرده و کد زیر را به آن اضافه کنید:
# app.py
from flask import Flask, jsonify<code data-shcb-language-name="python">
app = Flask(__name__)</code>
@app.route('/')
def home():
return jsonify(message="Hello, Flask!")
@app.route('/about')
def about():
return jsonify(message="This is the About page")
@app.route('/multiply/<int:x>/<int:y>')
def multiply(x, y):
result = x * y
return jsonify(result=result)</int:y></int:x>
if __name__ == '__main__':
app.run(debug=True)
Code language: Python (python)
این برنامه دارای سه مسیر است:
/
: یک پیام ساده “Hello, Flask!” باز میگرداند.
/about
: یک پیام ساده “This is the About page” باز میگرداند.
/multiply/<int:x>/<int:y>
: دو عدد صحیح را در هم ضرب کرده و نتیجه را بازمیگرداند.
برای اجرای برنامه، دستور زیر را اجرا کنید:
root@ubuntu:~# flask run
* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: This is a development server
Do not use it in a production deployment
<code data-shcb-language-name="bash"> Use a production WSGI server instead</code>
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Code language: Bash (bash)
از خروجی بالا میتوان متوجه شد که سرور در حال اجرا بر روی http://127.0.0.1
بوده و بر روی پورت 5000
به دنبال درخواستها است. یک کنسول اوبونتو دیگر را باز کرده و دستورات curl
زیر را یک به یک اجرا کنید:
- GET:
curl http://127.0.0.1:5000/
- GET:
curl http://127.0.0.1:5000/about
- GET:
curl http://127.0.0.1:5000/multiply/10/20
بیایید بدانیم این درخواستهای GET چه کاری انجام میدهند:
curl http://127.0.0.1:5000/
:
این یک درخواستGET
به مسیر ریشه (‘/’) برنامه Flask ما میفرستد. سرور با یک شیء JSON حاوی پیام “Hello, Flask!” پاسخ میدهد، که عملکرد اصلی مسیر اصلی ما را نشان میدهد.curl http://127.0.0.1:5000/about
:
این یک درخواست GET به مسیر/about
ارسال میکند.
درخواستهای GET
سرور با یک JSON که پیام “This is the About page” را شامل میشود، پاسخ میدهد. این نشان میدهد که مسیر ما به درستی عمل میکند.
درخواست دیگر:
curl http://127.0.0.1:5000/multiply/10/20
این دستور یک درخواست GET
را به مسیر /multiply
با دو پارامتر: 10 و 20 میفرستد. سرور این اعداد را ضرب کرده و با یک JSON که نتیجه (200) را دارد، پاسخ میدهد. این نشان میدهد که مسیر multiply میتواند به درستی پارامترهای URL را پردازش کند و محاسبات را انجام دهد.
این درخواستهای GET
به ما امکان میدهند تا با API اندپوینتهای برنامه Flask خود تعامل داشته باشیم، اطلاعات را بازیابی کنیم یا عملکردهایی را بر روی سرور بدون تغییر هیچ دادهای فعال کنیم. اینها برای فراخوانی دادهها، تست کردن عملکرد اندپوینتها و تأیید اینکه مسیرهای ما همانطور که انتظار داریم پاسخ میدهند، مفید هستند.
بیایید هر یک از این درخواستهای GET
را عملی ببینیم:
curl http://127.0.0.1:5000/
{"message":"Hello, Flask!"}
curl http://127.0.0.1:5000/about
{"message":"This is the About page"}
curl http://127.0.0.1:5000/multiply/10/20
<code data-shcb-language-name="bash">{"result":200}</code>
Code language: JavaScript (javascript)
گام ۳ – نصب pytest
و نوشتن اولین تست
حال که شما یک برنامه ابتدایی Flask دارید، بیایید pytest
را نصب کنیم و چند تست واحد بنویسیم.
نصب pytest
با استفاده از pip
:
pip install pytest
ایجاد یک پوشه تست برای ذخیره فایلهای تست شما:
mkdir tests
حالا، یک فایل جدید به نام test_app.py
ایجاد کنید و کدهای زیر را اضافه نمایید:
# Import sys module for modifying Python's runtime environment
import sys
# Import os module for interacting with the operating system
import os
# Add the parent directory to sys.path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Import the Flask app instance from the main app file
from app import app
# Import pytest for writing and running tests
import pytest
@pytest.fixture
def client():
"""یک مشتری تست برای اپ."""
with app.test_client() as client:
yield client
def test_home(client):
"""تست کردن مسیر home."""
response = client.get('/')
assert response.status_code == 200
assert response.json == {"message": "Hello, Flask!"}
def test_about(client):
"""تست کردن مسیر about."""
response = client.get('/about')
assert response.status_code == 200
assert response.json == {"message": "This is the About page"}
def test_multiply(client):
"""تست کردن مسیر multiply با ورودی معتبر."""
response = client.get('/multiply/3/4')
assert response.status_code == 200
assert response.json == {"result": 12}
def test_multiply_invalid_input(client):
"""تست کردن مسیر multiply با ورودی نامعتبر."""
response = client.get('/multiply/three/four')
assert response.status_code == 200
<title>تست اپلیکیشن Flask با استفاده از pytest - بخش 4</title>
status_code == 404
def test_non_existent_route(client):
"""تست برای مسیری غیرموجود"""
response = client.get('/non-existent')
assert response.status_code == 404
Code language: Python (python)
بیایید عملکرد توابع موجود در این فایل تست را تجزیه و تحلیل کنیم:
@pytest.fixture
def client():این یک fixture در pytest است که یک کلاینت تست برای اپلیکیشن Flask ما ایجاد میکند. این تابع با استفاده از متود
app.test_client()
یک کلاینت میسازد که میتواند درخواستهایی را به اپلیکیشن ما بفرستد بدون اجرای واقعی سرور. دستورyield
اجازه میدهد تا کلاینت در تستها مورد استفاده قرار گیرد و بعد از هر تست به درستی بسته شود.
def test_home(client):این تابع مسیر اصلی (یعنی
/
) اپلیکیشن ما را تست میکند. این تست درخواست GET را به مسیر با استفاده از کلاینت تست ارسال میکند، سپس ادعا میکند که کد وضعیت پاسخ 200 (OK) است و که پاسخ JSON با پیام مورد انتظار مطابقت دارد.
def test_about(client):مشابه
test_home
، این تابع مسیر درباره (یعنی/about
) را تست میکند. این تابع برای کد وضعیت 200 بررسی میکند و محتوای پاسخ JSON را تایید میکند.
def test_multiply(client):این تابع مسیر ضرب کردن با ورودی معتبر (یعنی
/multiply/3/4
) را تست میکند. این بررسی میکند که کد وضعیت 200 است و پاسخ JSON حاوی نتیجه صحیح ضرب میباشد.
def test_multiply_invalid_input(client):این تابع مسیر ضرب کردن با ورودی نامعتبر (یعنی
multiply/three/four
) را تست میکند. این بررسی میکند که کد وضعیت 404 (یافت نشد) باشد، که رفتار مورد انتظار است وقتی که مسیر نمیتواند ورودیهای متنی را با پارامترهای اعداد صحیح مورد نیاز متناظر کند.
def test_non_existent_route(client):این تابع رفتار اپلیکیشن را زمانی که به یک مسیر غیرموجود دسترسی پیدا میشود تست میکند. این درخواست GET را به
/non-existent
ارسال میکند، که در اپلیکیشن Flask ما تعریف نشده است. تست ادعا میکند که کد وضعیت پاسخ 404 (یافت نشد) است، که این اطمینان میدهد که اپلیکیشن ما به درستی درخواستها به مسیرهای تعریف نشده را مدیریت میکند.
این تستها عملکرد پایهای اپلیکیشن Flask ما را پوشش میدهند، اطمینان حاصل میکنند که هر مسیر به درستی به ورودیهای معتبر پاسخ میدهد و اینکه مسیر ضرب کردن به درستی ورودیهای نامعتبر را مدیریت میکند. با استفاده از pytest
، به راحتی میتوانیم این تستها را اجرا کنیم تا تایید کنیم که اپلیکیشن ما به شکل مورد انتظار کار میکند.
گام چهارم – اجرای تستها
برای اجرای تستها، دستور زیر را اجرا کنید:
root@ubuntu:~# pytest
به صورت پیشفرض، فرآیند کشف pytest
پوشه فعلی و زیرپوشههای آن را به صورت بازگشتی برای فایلهایی که با نامهایی شروع میشوند که یا با **“test_”** یا به **“_test”** ختم میشوند، اسکن میکند. تستهایی که در آن فایلها یافت میشوند، سپس اجرا میگردند. باید خروجی مشابه به موارد زیر را ببینید:
platform linux -- Python 3.8.5, pytest-6.2.2, pluggy-0.13.1
rootdir: /home/user/flask_testing_app
collected 5 items
tests/test_app.py ...
<title>استفاده از Fixtures در pytest - قسمت 5</title>
Code language: HTML, XML (xml)
مرحله 5: استفاده از Fixtures در pytest
Fixtures توابعی هستند که برای فراهم کردن دادهها یا منابع به تستها استفاده میشوند. میتوانند برای راهاندازی و جمعآوری محیطهای تست، بارگذاری دادهها، یا انجام وظایف دیگر نصب، به کار برده شوند. در pytest، Fixtures با استفاده از دکوراتور @pytest.fixture
تعریف میگردند.
در اینجا نحوه بهبود Fixture موجود آمده است. Fixture موجود برای کلاینت را برای استفاده از منطق راهاندازی و جمعآوری به روز کنید:
@pytest.fixture
def client():
"""Set up a test client for the app with setup and teardown logic"""
print("\nSetting up the test client")
with app.test_client() as client:
yield client # This is where the testing happens
print("Tearing down the test client")
def test_home(client):
"""Test the home route"""
response = client.get('/')
assert response.status_code == 200
assert response.json == {"message": "Hello, Flask!"}
def test_about(client):
"""Test the about route"""
response = client.get('/about')
assert response.status_code == 200
assert response.json == {"message": "This is the About page"}
def test_multiply(client):
"""Test the multiply route with valid input"""
response = client.get('/multiply/3/4')
assert response.status_code == 200
assert response.json == {"result": 12}
def test_multiply_invalid_input(client):
"""Test the multiply route with invalid input"""
response = client.get('/multiply/three/four')
assert response.status_code == 404
def test_non_existent_route(client):
"""Test for a non-existent route"""
response = client.get('/non-existent')
assert response.status_code == 404
Code language: Python (python)
این تنظیمات خروجیهای print را اضافه میکند تا مراحل راهاندازی و جمعآوری در خروجی تست نمایش داده شوند. اینها ممکن است با کد مدیریت منابع واقعی در صورت لزوم جایگزین شوند.
بیایید دوباره تستها را اجرا کنیم:
root@ubuntu:~# pytest -vs
فلگ -v
سطح جزئیات را افزایش میدهد، و پرچم -s
اجازه میدهد که خروجیهای print در خروجی کنسول نمایش داده شوند.
شما باید خروجی زیر را مشاهده کنید:
platform linux -- Python 3.8.3, pytest-6.2.2, pluggy-0.13.1
rootdir: /home/user/flask_testing_app, cachedir: .pytest_cache
collected 5 items
tests/test_app.py::test_home
Setting up the test client
PASSED
Tearing down the test client
tests/test_app.py::test_about
Setting up the test client
PASSED
Tearing down the test client
tests/test_app.py::test_multiply
Setting up the test client
PASSED
Tearing down the test client
tests/test_app.py::test_multiply_invalid_input
Setting up the test client
PASSED
Tearing down the test client
tests/test_app.py::test_non_existent_route
Setting up the test client
PASSED
Tearing down the test client
Code language: PHP (php)
قدم 6: افزودن یک مورد آزمایش ناموفق
بگذارید یک مورد آزمایش ناموفق را به فایل آزمایش موجود اضافه کنیم. فایل test_app.py
را تغییر داده و تابع زیر را در انتهای آن برای ایجاد یک مورد آزمایش ناموفق برای نتیجهی نادرست افزوده کنید:
def test_multiply_edge_cases(client):
"""Test the multiply route with edge cases to demonstrate failing tests"""
# آزمایش با صفر
response = client.get('/multiply/0/5')
assert response.status_code == 200
assert response.json == {"result": 0}
# آزمایش با اعداد بزرگ (این ممکن است در صورت عدم مدیریت صحیح شکست بخورد)
response = client.get('/multiply/1000000/1000000')
assert response.status_code == 200
assert response.json == {"result": 1000000000000}
# آزمایش ناموفق عمدی: نتیجه نادرست
response = client.get('/multiply/2/3')
assert response.status_code == 200
assert response.json == {"result": 7}, "این آزمایش باید به منظور نمایش یک مورد ناموفق شکست بخورد"
Code language: Python (python)
بیایید تابع test_multiply_edge_cases
را تجزیه و تحلیل کنیم و توضیح دهیم که هر بخش چه کاری انجام میدهد:
- آزمایش با صفر:
این آزمایش بررسی میکند که آیا تابع ضرب به درستی ضرب در صفر را مدیریت میکند. انتظار داریم نتیجه 0 باشد هنگام ضرب هر عددی در صفر. این یک مورد حاشیهی مهم برای آزمایش است زیرا برخی از پیادهسازیها ممکن است با ضرب صفر مشکل داشته باشند.
- آزمایش با اعداد بزرگ:
این آزمایش بررسی میکند که آیا تابع ضرب قادر به مدیریت اعداد بسیار بزرگ بدون مشکل راندگی یا دقت است. ما دو میلیون را در یک میلیون ضرب میکنیم و انتظار داریم نتیجه یک تریلیون باشد. این تست بسیار مهم است زیرا حد بالای قابلیت تابع ضرب را بررسی میکند. توجه داشته باشید که این ممکن است شکست بخورد اگر پیادهسازی سرور اعداد بزرگ را به درستی مدیریت نکند، که میتواند به نیاز به کتابخانههای اعداد بزرگ یا یک نوع داده متفاوت اشاره داشته باشد.
- آزمایش ناموفق عمدی:
این آزمایش عمداً برای شکست برنامهریزی شده است. بررسی میکند که آیا ۲ ضربدر ۳ برابر با ۷ است، که نادرست است. هدف از این آزمایش نمایش نحوهی ظاهر شدن یک آزمایش ناموفق در خروجی آزمایش است. این کمک میکند تا درک کنیم چگونه میتوان آزمایشهای ناموفق را شناسایی و عیبزدایی کرد، که یک مهارت ضروری در توسعهی محوری آزمایش و فرآیندهای عیبزدایی است.
با ادراج این موارد حاشیه و یک شکست عمدی، شما نه تنها عملکرد پایهای مسیر ضرب خود را آزمایش میکنید بلکه رفتار آن را تحت شرایط بی سابقه و نحوه گزارش خطاها نیز بررسی میکنید. این رویکرد برای آزمایش به منظور اطمینان از استحکام و قابلیت اطمینان برنامهی ما کمک میکند.
بیایید برای دوباره آزمایش اجرا را امتحان کنیم:
شکستها
# مشتری Flask
client = <FlaskClient <Flask 'app'>><code data-shcb-language-name="python">
</code> def test_multiply_edge_cases(client):
"""آزمون مسیر ضرب با موارد حدی برای نمایش تستهای ناموفق"""
# تست با صفر
response = client.get('/multiply/0/5')
assert response.status_code == 200
assert response.json == {"result": 0}
Code language: Python (python)
# تست با اعداد بزرگ (ممکن است در صورت عدم مدیریت مناسب شکست بخورد)
response = client.get('/multiply/1000000/1000000')
assert response.status_code == 200
assert response.json == {"result": 1000000000000}
Code language: PHP (php)
# تست ناموفق عمدی: نتیجه نادرست
response = client.get('/multiply/2/3')
assert response.status_code == 200
> assert response.json == {"result": 7}, "این تست باید شکست بخورد تا یک مورد شکست را نمایش دهد"
E AssertionError: این تست باید شکست بخورد تا یک مورد شکست را نمایش دهد
E assert {'result': 6} == {'result': 7}
E
E آیتمهای متفاوت:
E {'result': 6} != {'result': 7}
E
E تفاوت کامل:
E {
E - 'result': 7,
Code language: PHP (php)
خروجی کامل کوتاه شد (4 خط پنهان)، برای نمایش ‘-vv’ استفاده کنید.
tests/test_app.py:61: AssertionError
Code language: HTTP (http)
FAILED tests/test_app.py::test_multiply_edge_cases – AssertionError: این تست باید شکست بخورد تا یک مورد شکست را نمایش دهد
خروجی تست
1 ناموفق, 5 موفق در 0.32 ثانیه
شکست عمدی تست
این شکست عمدی برای نمایش نحوهی گزارش خطاهای تست و اطلاعات ارائه شده در پیام خطا مفید است. این پیام دقیقا نشان میدهد که خطای انتظاری در کدام خط رخ داده، مقادیر مورد انتظار و واقعی چه هستند و تفاوت بین آن دو چقدر است.
در یک سناریوی واقعی، شما کد را برای قبولی تست اصلاح میکنید یا در صورت نادرست بودن نتیجه مورد انتظار، تست را تنظیم مجدد میکنید. اما در این مورد، شکست تست برای اهداف آموزشی عمدی بوده است.
نتیجهگیری
در این آموزش، ما نحوه راهاندازی تستهای واحد برای یک برنامه Flask با استفاده از `pytest`، ادغام `pytest` فیکسچرها و نمایش شکلی از شکست یک تست را پوشش دادیم. با پیروی از این مراحل، شما میتوانید اطمینان حاصل کنید که برنامههای Flask شما قابل اعتماد و قابل نگهداری هستند، اشکالات را به حداقل میرسانند و کیفیت کد را افزایش میدهند.
شما میتوانید برای یادگیری بیشتر به مستندات رسمی [Flask](https://flask.palletsprojects.com/en/3.0.x/) و [Pytest](https://docs.pytest.org/en/stable/) مراجعه کنید.
نظرات کاربران