از طریق منوی جستجو مطلب مورد نظر خود در وبلاگ را به سرعت پیدا کنید
توسعه تست محور با pytest
سرفصلهای مطلب
معرفی
نرم افزار خوب نرم افزار تست شده است. آزمایش کد ما می تواند به ما کمک کند تا اشکالات یا رفتارهای ناخواسته را پیدا کنیم.
توسعه آزمایش محور (TDD) یک روش توسعه نرمافزار است که از ما میخواهد برای ویژگیهایی که میخواهیم اضافه کنیم، بهصورت تدریجی آزمایش بنویسیم. از مجموعه های تست خودکار، مانند pytest – یک چارچوب آزمایشی برای برنامه های پایتون.
تست خودکار
توسعه دهندگان معمولا کد می نویسند، در صورت لزوم آن را کامپایل می کنند و سپس کد را اجرا می کنند تا ببینند کار می کند یا خیر. این یک نمونه از تست دستی. در این روش به بررسی ویژگی های برنامه می پردازیم. اگر می خواهید آزمایش خود را کامل انجام دهید، باید به یاد داشته باشید که چگونه نتایج مختلف هر ویژگی را آزمایش کنید.
اگر یک توسعهدهنده جدید شروع به اضافه کردن ویژگیها به پروژه کند، باید ویژگیهای آنها را هم یاد بگیرید تا آن را آزمایش کنید؟ ویژگیهای جدید گاهی اوقات روی ویژگیهای قدیمیتر تأثیر میگذارند، آیا میخواهید به صورت دستی بررسی کنید که وقتی یک ویژگی جدید اضافه میکنید، همه ویژگیهای قبلی همچنان کار میکنند؟
آزمایش دستی می تواند به ما اعتماد به نفس را برای ادامه توسعه افزایش دهد. با این حال، همانطور که برنامه ما رشد می کند، آزمایش دستی پایه کد به طور مداوم سخت تر و خسته کننده تر می شود.
آزمایش خودکار بار آزمایش کد و پیگیری نتایج را به سمت حفظ اسکریپت هایی که این کار را برای ما انجام می دهند، منتقل می کند. اسکریپت ها ماژول های کد را با ورودی های تعریف شده توسط توسعه دهنده اجرا می کنند و خروجی را با انتظارات تعریف شده توسط توسعه دهنده مقایسه می کنند.
این pytest
مدول
کتابخانه استاندارد پایتون با یک چارچوب تست خودکار ارائه می شود – the واحد آزمایش کتابخانه در حالی که unittest
کتابخانه دارای ویژگی های غنی است و در وظایف خود موثر است، ما از آن استفاده خواهیم کرد pytest
به عنوان سلاح انتخابی ما در این مقاله.
اکثر توسعه دهندگان پیدا می کنند pytest
استفاده آسان تر از unittest
. یک دلیل ساده این است که pytest
برای نوشتن تست فقط به توابع نیاز دارد، در حالی که unittest
ماژول نیاز به کلاس دارد.
برای بسیاری از توسعه دهندگان جدید، نیاز به کلاس برای تست ها می تواند کمی آزاردهنده باشد. pytest
همچنین شامل بسیاری از ویژگی های دیگر است که بعداً در این آموزش از آنها استفاده خواهیم کرد که در آن وجود ندارند unittest
مدول.
توسعه تست محور چیست؟
Test-Driven Development یک روش ساده توسعه نرم افزار است که به شما یا تیمی از کدنویسان دستور می دهد تا این سه مرحله را برای ایجاد نرم افزار دنبال کنید:
- تستی برای ویژگی ای که ناموفق است بنویسید
- برای قبولی در آزمون کد بنویسید
- در صورت نیاز کد را دوباره فاکتور کنید
این process معمولاً به عنوان قرمز-سبز-رفاکتور چرخه:
- شما یک تست خودکار برای روش رفتار کد جدید می نویسید و می بینید که شکست می خورد – قرمز
- کد را در برنامه بنویسید تا زمانی که آزمون شما قبول شود – سبز
- Refactor کدی که آن را خوانا و کارآمد می کند. نیازی نیست نگران باشید که ریفکتورینگ شما ویژگی جدید را از بین میبرد، فقط باید تست را دوباره اجرا کنید و از موفقیت آن اطمینان حاصل کنید.
یک ویژگی زمانی کامل می شود که دیگر نیازی به نوشتن کد برای گذراندن تست های آن نداشته باشیم.
چرا از TDD برای ایجاد برنامه ها استفاده کنیم؟
شکایت رایج استفاده از TDD این است که زمان زیادی می برد.
همانطور که با نوشتن تستها کارآمدتر میشوید، زمان مورد نیاز برای حفظ آنها کاهش مییابد. علاوه بر این، TDD مزایای زیر را ارائه می دهد، که می توانید ارزش معاوضه زمانی را داشته باشید:
- تستهای نوشتن مستلزم این هستند که ورودیها و خروجیها را بشناسید تا ویژگی کار کند – TDD ما را مجبور میکند قبل از شروع کدنویسی در مورد رابط برنامه فکر کنیم.
- افزایش اعتماد به پایگاه کد – با انجام تستهای خودکار برای همه ویژگیها، توسعهدهندگان در هنگام توسعه ویژگیهای جدید اعتماد به نفس بیشتری پیدا میکنند. آزمایش کل سیستم برای دیدن اینکه آیا تغییرات جدید آنچه قبلا وجود داشت را شکسته است، امری بی اهمیت می شود.
- TDD همه باگها را برطرف نمیکند، اما احتمال مواجهه با آنها کمتر است – هنگام تلاش برای رفع یک باگ، میتوانید یک آزمایش برای آن بنویسید تا از رفع آن هنگام کدنویسی مطمئن شوید.
- از آزمون ها می توان به عنوان مستندات بیشتر استفاده کرد. همانطور که ورودی ها و خروجی های یک ویژگی را می نویسیم، یک توسعه دهنده می تواند به آزمایش نگاه کند و ببیند که رابط کد چگونه قرار است استفاده شود.
پوشش کد
پوشش کد معیاری است که میزان کد منبع تحت پوشش طرح آزمایشی شما را اندازه گیری می کند.
پوشش 100٪ کد به این معنی است که تمام کدهایی که نوشته اید توسط برخی از آزمون ها استفاده شده است. ابزارها پوشش کد را به روش های مختلف اندازه گیری می کنند، در اینجا چند معیار رایج وجود دارد:
- خطوط کد تست شده
- چند تابع تعریف شده تست شده است
- چند شاخه (
if
عبارات به عنوان مثال) آزمایش می شوند
مهم است که بدانید ابزار پوشش کد شما از چه معیارهایی استفاده می کند.
همانطور که ما به شدت از آن استفاده می کنیم pytest
، از محبوب استفاده خواهیم کرد pytest-cov افزونه برای دریافت پوشش کد.
پوشش بالای کد به این معنی نیست که برنامه شما هیچ باگ نخواهد داشت. به احتمال زیاد کد برای آن آزمایش نشده است هر ممکن سناریو.
تست واحد در مقابل تست های ادغام
تست های واحد برای اطمینان از رفتار یک ماژول فردی همانطور که انتظار می رود استفاده می شود، در حالی که تست های ادغام اطمینان حاصل کنید که مجموعه ای از ماژول ها همانطور که ما از آنها انتظار داریم با هم کار می کنند.
همانطور که ما برنامه های بزرگتر را توسعه می دهیم، باید اجزای زیادی را توسعه دهیم. در حالی که این اجزای جداگانه ممکن است هر کدام مربوط به خود را داشته باشند تست های واحد، ما همچنین راهی می خواهیم که اطمینان حاصل کنیم که این اجزای متعدد وقتی با هم استفاده می شوند انتظارات ما را برآورده می کنند.
TDD مستلزم آن است که ما با نوشتن یک تست منفرد که با کد پایه فعلی ناموفق است شروع کنیم، سپس برای تکمیل آن کار کنیم. مشخص نمی کند که باید یک آزمون واحد باشد، در صورت تمایل، اولین آزمون شما می تواند یک آزمون یکپارچه سازی باشد.
هنگامی که اولین آزمون ادغام ناموفق شما نوشته شد، میتوانیم شروع به توسعه هر جزء جداگانه کنیم.
تست ادغام تا زمانی که هر کامپوننت ساخته نشود و تست های خود را بگذراند ناموفق خواهد بود. وقتی تست یکپارچهسازی رد میشود، اگر به درستی ساخته میشد، یک نیاز کاربر را برای سیستم خود برآورده میکردیم.
بهترین راه برای درک TDD، عملی کردن آن است. ما با نوشتن یک برنامه پایتون شروع می کنیم که مجموع تمام اعداد در یک دنباله که اعداد اول هستند را برمی گرداند.
ما دو تابع برای انجام این کار ایجاد می کنیم، یکی که تعیین می کند یک عدد اول است یا نه و دیگری که اعداد اول را از یک دنباله اعداد معین جمع می کند.
یک دایرکتوری به نام ایجاد کنید primes
در فضای کاری دلخواه شما حالا دو فایل اضافه کنید: primes.py
، test_primes.py
. فایل اول جایی است که ما کد برنامه خود را می نویسیم، فایل دوم جایی است که تست های ما در آن خواهد بود.
pytest
مستلزم آن است که فایل های آزمایشی ما یا با “test_” شروع شوند یا با “_test” ختم شوند.py(بنابراین، میتوانستیم فایل آزمایشی خود را نیز نامگذاری کنیم primes_test.py
).
در حال حاضر در ما primes
دایرکتوری، بیایید محیط مجازی خود را راه اندازی کنیم:
$ python3 -m venv env
$ . env/bin/activate
$ pip install --upgrade pip
$ pip install pytest
تست تابع ()is_prime
عدد اول هر عدد طبیعی بزرگتر از 1 است که فقط بر 1 و خودش بخش پذیر باشد.
تابع ما باید یک عدد بگیرد و برگردد True
اگر اول باشد و False
در غیر این صورت.
در ما test_primes.py
، بیایید اولین مورد آزمایشی خود را اضافه کنیم:
def test_prime_low_number():
assert is_prime(1) == False
این assert()
بیانیه یک کلمه کلیدی در پایتون (و در بسیاری از زبان های دیگر) است که در صورت عدم موفقیت یک شرط فوراً خطا می دهد. این کلمه کلیدی هنگام نوشتن تست ها مفید است زیرا دقیقاً نشان می دهد که چه شرایطی شکست خورده است.
اگر وارد شویم
1
یا عددی کوچکتر از1
، پس نمی تواند اول باشد.
حالا بیایید تست خود را اجرا کنیم. موارد زیر را در خط فرمان خود وارد کنید:
$ pytest
برای خروجی پرمخاطب می توانید اجرا کنید pytest -v
. مطمئن شوید که محیط مجازی شما هنوز فعال است (باید ببینید (env)
در ابتدای خط در شما terminal).
باید به خروجی های زیر توجه کنید:
def test_prime_low_number():
> assert is_prime(1) == False
E NameError: name 'is_prime' is not defined
test_primes.py:2: NameError
========================================================= 1 failed in 0.12 seconds =========================================================
منطقی است که a NameError
، ما هنوز تابع خود را ایجاد نکرده ایم. این جنبه “قرمز” چرخه قرمز-سبز- فاکتور است.
pytest
حتی اگر پوسته شما برای نمایش رنگ ها پیکربندی شده باشد، تست های ناموفق را با رنگ قرمز ثبت می کند. حالا بیایید کد را در ما اضافه کنیم primes.py
فایل برای قبولی در این آزمون:
def is_prime(num):
if num == 1:
return False
توجه داشته باشید: به طور کلی تمرین خوبی است که تست های خود را در فایل های جداگانه از کد خود نگه دارید. جدای از بهبود خوانایی و تفکیک نگرانی ها با رشد پایگاه کد شما، توسعه دهنده آزمون را نیز از عملکرد داخلی کد دور نگه می دارد. بنابراین، آزمایشها از رابطهای برنامه به همان شیوهای استفاده میکنند که توسعهدهنده دیگری از آن استفاده میکند.
حالا بیایید فرار کنیم pytest
یکبار دیگر. اکنون باید خروجی را مانند این ببینیم:
=========================================================== test session starts ============================================================
platform darwin -- Python 3.7.3, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
rootdir: /Users/marcus/rasanegar/test-driven-development-with-pytest/primes
plugins: cov-2.6.1
collected 1 item
test_primes.py . (100%)
========================================================= 1 passed in 0.04 seconds =========================================================
اولین آزمون ما قبول شد! می دانیم که 1 اول نیست، اما طبق تعریف 0 اول نیست و هیچ عدد منفی نیز وجود ندارد.
ما باید برنامه خود را اصلاح کنیم تا آن را منعکس کنیم و تغییر دهیم is_prime()
به:
def is_prime(num):
if num < 2:
return False
اگر اجرا کنیم pytest
باز هم، آزمون های ما همچنان با موفقیت پشت سر می گذاشتند.
حالا بیایید یک مورد آزمایشی برای عدد اول اضافه کنیم test_primes.py
موارد زیر را بعد از اولین مورد آزمایشی خود اضافه کنید:
def test_prime_prime_number():
assert is_prime(29)
و بیایید فرار کنیم pytest
برای دیدن این خروجی:
def test_prime_prime_number():
> assert is_prime(29)
E assert None
E + where None = is_prime(29)
test_primes.py:9: AssertionError
============================================================= warnings summary =============================================================
test_primes.py::test_prime_prime_number
/Users/marcus/rasanegar/test-driven-development-with-pytest/primes/test_primes.py:9: PytestWarning: asserting the value None, please use "assert is None"
assert is_prime(29)
-- Docs: https://docs.pytest.org/en/latest/warnings.html
============================================== 1 failed, 1 passed, 1 warnings in 0.12 seconds ==============================================
توجه داشته باشید که pytest
دستور اکنون دو تستی را که نوشته ایم اجرا می کند.
مورد جدید با شکست مواجه میشود، زیرا ما واقعاً اول بودن یا نبودن یک عدد را محاسبه نمیکنیم. این is_prime()
تابع برمی گردد None
همانطور که سایر توابع به طور پیش فرض برای هر عدد بزرگتر از 1 انجام می دهند.
خروجی همچنان از کار می افتد یا از خروجی قرمز می بینیم.
بیایید به این فکر کنیم که چگونه تعیین کنیم یک عدد کجا اول است یا خیر. سادهترین روش این است که از 2 تا یک عدد کمتر از عدد حلقه بزنید و عدد را بر مقدار فعلی تکرار تقسیم کنید.
برای کارآمدتر کردن این کار، میتوانیم با تقسیم اعداد بین 2 و مربع بررسی کنیم root از تعداد
اگر از تقسیم باقی نمانده باشد، مقسومکنندهای دارد که نه 1 است و نه خودش، و بنابراین اول نیست. اگر مقسوم علیه در حلقه پیدا نکرد، پس باید اول باشد.
بیایید به روز کنیم is_prime()
با منطق جدید ما:
import math
def is_prime(num):
if num < 2:
return False
for n in range(2, math.floor(math.sqrt(num) + 1)):
if num % n == 0:
return False
return True
حالا می دویم pytest
برای دیدن اینکه آیا آزمون ما قبول می شود:
=========================================================== test session starts ============================================================
platform darwin -- Python 3.7.3, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
rootdir: /Users/marcus/rasanegar/test-driven-development-with-pytest/primes
plugins: cov-2.6.1
collected 2 items
test_primes.py .. (100%)
========================================================= 2 passed in 0.04 seconds =========================================================
می گذرد. می دانیم که این تابع می تواند یک عدد اول و یک عدد پایین را دریافت کند. بیایید یک آزمایش اضافه کنیم تا از بازگشت آن اطمینان حاصل کنیم False
برای عدد مرکب بزرگتر از 1.
که در test_primes.py
مورد آزمایشی زیر را در زیر اضافه کنید:
def test_prime_composite_number():
assert is_prime(15) == False
اگر اجرا کنیم pytest
خروجی زیر را خواهیم دید:
=========================================================== test session starts ============================================================
platform darwin -- Python 3.7.3, pytest-4.4.1, py-1.8.0, pluggy-0.9.0
rootdir: /Users/marcus/rasanegar/test-driven-development-with-pytest/primes
plugins: cov-2.6.1
collected 3 items
test_primes.py ... (100%)
========================================================= 3 passed in 0.04 seconds =========================================================
آزمایش sum_of_primes()
همانطور که با is_prime()
، بیایید در مورد نتایج این تابع فکر کنیم. اگر به تابع یک لیست خالی داده شود، مجموع آن باید صفر باشد.
این تضمین می کند که تابع ما همیشه باید مقداری با ورودی معتبر برگرداند. پس از آن، ما می خواهیم آزمایش کنیم که فقط اعداد اول را در لیستی از اعداد اضافه می کند.
بیایید اولین تست شکست خورده خود را بنویسیم، کد زیر را در انتهای آن اضافه کنید test_primes.py
:
def test_sum_of_primes_empty_list():
assert sum_of_primes(()) == 0
اگر اجرا کنیم pytest
ما آشنا را دریافت خواهیم کرد NameError
شکست تست، زیرا ما هنوز تابع را تعریف نکرده ایم. در ما primes.py
فایل بیایید تابع جدید خود را اضافه کنیم که به سادگی مجموع یک لیست داده شده را برمی گرداند:
def sum_of_primes(nums):
return sum(nums)
اکنون در حال اجرا pytest
نشان می دهد که همه آزمون ها قبول می شوند. آزمایش بعدی ما باید اطمینان حاصل کند که فقط اعداد اول اضافه می شوند.
ما اعداد اول و مرکب را با هم ترکیب می کنیم و انتظار داریم که تابع فقط اعداد اول را اضافه کند:
def test_sum_of_primes_mixed_list():
assert sum_of_primes((11, 15, 17, 18, 20, 100)) == 28
اعداد اول در لیستی که ما آزمایش می کنیم 11 و 17 هستند که جمع آنها به 28 می رسد.
در حال دویدن pytest
برای تأیید عدم موفقیت آزمون جدید. حالا بیایید خودمان را اصلاح کنیم sum_of_primes()
به طوری که فقط اعداد اول اضافه می شوند.
ما اعداد اول را با درک لیست فیلتر می کنیم:
def sum_of_primes(nums):
return sum((x for x in nums if is_prime(x)))
طبق روال معمول، می دویم pytest
برای تأیید، ما تست شکست را برطرف کردیم – همه چیز قبول می شود.
پس از تکمیل، بیایید پوشش کد خود را بررسی کنیم:
$ pytest --cov=primes
برای این بسته، پوشش کد ما 100٪ است! اگر اینطور نبود، میتوانیم مدتی را صرف اضافه کردن چند آزمایش دیگر به کد خود کنیم تا مطمئن شویم که طرح آزمایشی ما کامل است.
به عنوان مثال، اگر ما is_prime()
تابع یک مقدار float داده شد، آیا خطا می دهد؟ ما is_prime()
روش این قانون را اجرا نمی کند که یک عدد اول باید یک عدد طبیعی باشد، فقط بررسی می کند که بزرگتر از 1 باشد.
حتی اگر پوشش کد کلی داریم، ویژگی در حال پیاده سازی ممکن است در همه شرایط به درستی کار نکند.
مثال پیشرفته: نوشتن یک مدیر موجودی
اکنون که اصول TDD را درک کردهایم، بیایید به برخی از ویژگیهای مفید آن بپردازیم pytest
که ما را قادر می سازد در نوشتن تست ها کارآمدتر شویم.
درست مانند قبل در مثال اصلی ما، inventory.py
و یک فایل آزمایشی test_inventory.py
، دو فایل اصلی ما خواهند بود.
ویژگی ها و برنامه ریزی آزمون
یک فروشگاه پوشاک و کفش میخواهد مدیریت اقلام خود را از کاغذ به رایانه جدیدی که مالک آن خریده است منتقل کند.
در حالی که مالک ویژگیهای زیادی را میخواهد، او از نرمافزاری راضی است که میتواند وظایف بعدی زیر را بلافاصله انجام دهد.
- 10 کفش ورزشی نایکی که اخیرا خریده است را ضبط کنید. ارزش هر کدام 50.00 دلار است.
- 5 شلوار ورزشی آدیداس اضافه کنید که هر کدام 70 دلار قیمت دارند.
- او انتظار دارد یک مشتری 2 کفش ورزشی نایک بخرد
- او انتظار دارد مشتری دیگری 1 شلوار ورزشی را بخرد.
ما می توانیم از این الزامات برای ایجاد اولین آزمون یکپارچه سازی خود استفاده کنیم. قبل از اینکه به نوشتن آن بپردازیم، اجازه دهید اجزای کوچکتر را کمی توضیح دهیم تا بفهمیم ورودی و خروجی ما، امضاهای عملکرد و سایر عناصر طراحی سیستم چه خواهد بود.
هر قلم از سهام دارای نام، قیمت و مقدار خواهد بود. ما میتوانیم اقلام جدید اضافه کنیم، سهام را به اقلام موجود اضافه کنیم و البته موجودی را حذف کنیم.
وقتی یک نمونه را مثال می زنیم Inventory
شی، ما از کاربر می خواهیم که a را ارائه دهد limit
. این limit
یک مقدار پیش فرض 100 خواهد داشت. اولین آزمایش ما بررسی این خواهد بود limit
هنگام نمونه سازی یک شی برای اطمینان از اینکه از حد خود فراتر نمی رویم، باید آن را پیگیری کنیم total_items
پیشخوان. وقتی مقداردهی اولیه شد، این باید 0 باشد.
ما باید 10 کفش ورزشی نایک و 5 شلوار ورزشی آدیداس را به سیستم اضافه کنیم. ما می توانیم ایجاد کنیم add_new_stock()
روشی که الف را می پذیرد name
، price
، و quantity
.
باید آزمایش کنیم که بتوانیم یک آیتم را به شی موجودی خود اضافه کنیم. ما نباید بتوانیم یک مورد با کمیت منفی اضافه کنیم، روش باید یک استثنا ایجاد کند. همچنین اگر در حد خود هستیم، نباید بتوانیم موارد دیگری را اضافه کنیم، این نیز باید یک استثنا ایجاد کند.
مشتریان به زودی پس از ورود این اقلام را خریداری خواهند کرد، بنابراین ما به یک نیاز داریم remove_stock()
روش نیز این تابع به name
از سهام و quantity
موارد در حال حذف اگر مقدار حذف شده منفی باشد یا اگر مقدار کل موجودی را به زیر 0 برساند، روش باید یک استثنا ایجاد کند. به علاوه، اگر name
به شرطی که در موجودی ما یافت نشود، روش باید یک استثنا ایجاد کند.
اولین تست ها
آماده شدن برای انجام آزمایشات اولیه به ما در طراحی سیستم کمک کرده است. بیایید با ایجاد اولین تست ادغام خود شروع کنیم:
def test_buy_and_sell_nikes_adidas():
inventory = Inventory()
assert inventory.limit == 100
assert inventory.total_items == 0
inventory.add_new_stock('Nike Sneakers', 50.00, 10)
assert inventory.total_items == 10
inventory.add_new_stock('Adidas Sweatpants', 70.00, 5)
assert inventory.total_items == 15
inventory.remove_stock('Nike Sneakers', 2)
assert inventory.total_items == 13
inventory.remove_stock('Adidas Sweatpants', 1)
assert inventory.total_items == 12
در هر اقدامی ما یک ادعا انجام می دهیم روی وضعیت موجودی بهتر است بعد از انجام یک اقدام ادعا کنید، بنابراین وقتی در حال رفع اشکال هستید، آخرین مرحلهای را که برداشتهاید، میدانید.
اجرا کن pytest
و باید با a شکست بخورد NameError
به عنوان نه Inventory
کلاس تعریف شده است.
بیایید خود را ایجاد کنیم Inventory
کلاس، با یک پارامتر محدود که به طور پیش فرض 100 است، با تست های واحد شروع می شود:
def test_default_inventory():
"""Test that the default limit is 100"""
inventory = Inventory()
assert inventory.limit == 100
assert inventory.total_items == 0
و حالا خود کلاس:
class Inventory:
def __init__(self, limit=100):
self.limit = limit
self.total_items = 0
قبل از اینکه حرکت کنیم روی به متدها، میخواهیم مطمئن شویم که شی ما میتواند با یک محدودیت سفارشی مقداردهی اولیه شود و باید به درستی تنظیم شود:
def test_custom_inventory_limit():
"""Test that we can set a custom limit"""
inventory = Inventory(limit=25)
assert inventory.limit == 25
assert inventory.total_items == 0
ادغام همچنان با شکست مواجه می شود، اما این آزمون با موفقیت انجام می شود.
وسایل
دو آزمایش اول ما را ملزم به نمونه سازی یک نمونه می کرد Inventory
قبل از اینکه بتوانیم شروع کنیم، مخالفت کنیم. به احتمال زیاد ما باید همین کار را برای تمام آزمایشات بعدی انجام دهیم. این یک مقدار تکراری است.
ما میتوانیم استفاده کنیم وسایل برای کمک به حل این مشکل. فیکسچر یک حالت شناخته شده و ثابت است که تست ها برای اطمینان از تکرارپذیری نتایج انجام می شود.
این تمرین خوبی است که آزمون ها جدا از یکدیگر اجرا شوند. نتایج یک مورد آزمایشی نباید بر نتایج یک مورد آزمایشی دیگر تأثیر بگذارد.
بیایید اولین وسیله خود را بسازیم، یک Inventory
شی بدون سهام
test_inventory.py
:
import pytest
@pytest.fixture
def no_stock_inventory():
"""Returns an empty inventory that can store 10 items"""
return Inventory(10)
به استفاده از pytest.fixture
دکوراتور برای اهداف آزمایشی میتوانیم محدودیت موجودی را به 10 کاهش دهیم.
بیایید از این فیکسچر برای اضافه کردن یک آزمایش برای آن استفاده کنیم add_new_stock()
روش:
def test_add_new_stock_success(no_stock_inventory):
no_stock_inventory.add_new_stock('Test Jacket', 10.00, 5)
assert no_stock_inventory.total_items == 5
assert no_stock_inventory.stocks('Test Jacket')('price') == 10.00
assert no_stock_inventory.stocks('Test Jacket')('quantity') == 5
توجه داشته باشید که نام تابع آرگومان آزمون است، برای اعمال فیکسچر باید هم نام باشند. در غیر این صورت، از آن مانند یک شی معمولی استفاده می کنید.
برای اطمینان از اضافه شدن موجودی، باید کمی بیشتر از کل اقلام ذخیره شده تا کنون تست کنیم. نوشتن این تست ما را مجبور کرده است که روش نمایش قیمت و مقدار باقیمانده سهام را در نظر بگیریم.
اجرا کن pytest
مشاهده کنید که اکنون 2 شکست و 2 پاس وجود دارد. اکنون آن را اضافه می کنیم add_new_stock()
روش:
class Inventory:
def __init__(self, limit=100):
self.limit = limit
self.total_items = 0
self.stocks = {}
def add_new_stock(self, name, price, quantity):
self.stocks(name) = {
'price': price,
'quantity': quantity
}
self.total_items += quantity
متوجه خواهید شد که یک شی سهام در قسمت مقداردهی اولیه شده است __init__
تابع. باز هم فرار کن pytest
برای تایید قبولی آزمون
تست های پارامترسازی
قبلا ذکر کردیم که add_new_stock()
روش اعتبار سنجی ورودی را انجام می دهد – اگر کمیت صفر یا منفی باشد، یا اگر ما را از حد موجودی ما فراتر می برد، استثنا می کنیم.
ما به راحتی می توانیم موارد تست بیشتری را اضافه کنیم، از try/except برای گرفتن هر استثنا استفاده کنیم. این نیز تکراری است.
Pytest فراهم می کند توابع پارامتری شده که به ما امکان می دهد چندین سناریو را با استفاده از یک تابع آزمایش کنیم. بیایید یک تابع تست پارامتری بنویسیم تا مطمئن شویم اعتبار سنجی ورودی ما کار می کند:
@pytest.mark.parametrize('name,price,quantity,exception', (
('Test Jacket', 10.00, 0, InvalidQuantityException(
'Cannot add a quantity of 0. All new stocks must have at least 1 item'))
))
def test_add_new_stock_bad_input(name, price, quantity, exception):
inventory = Inventory(10)
try:
inventory.add_new_stock(name, price, quantity)
except InvalidQuantityException as inst:
assert isinstance(inst, type(exception))
assert inst.args == exception.args
else:
pytest.fail("Expected error but found none")
این تست سعی میکند سهامی را اضافه کند، استثنا را دریافت میکند و سپس بررسی میکند که استثنا درست است. اگر استثنا نگیریم، در آزمون مردود می شویم. این else
بند در این سناریو بسیار مهم است. بدون آن، استثنایی که پرتاب نمی شد به عنوان یک پاس به حساب می آمد. بنابراین آزمایش ما مثبت کاذب خواهد داشت.
ما استفاده می کنیم pytest
دکوراتورها برای اضافه کردن یک پارامتر به عملکرد. آرگومان اول شامل یک رشته از نام تمام پارامترها است. آرگومان دوم لیستی از تاپل ها است که هر تاپل یک مورد آزمایشی است.
اجرا کن pytest
برای دیدن شکست تست ما به عنوان InvalidQuantityException
تعریف نشده است. برگشت داخل inventory.py
بیایید یک استثنای جدید در بالای آن ایجاد کنیم Inventory
کلاس:
class InvalidQuantityException(Exception):
pass
و تغییر دهید add_new_stock()
روش:
def add_new_stock(self, name, price, quantity):
if quantity <= 0:
raise InvalidQuantityException(
'Cannot add a quantity of {}. All new stocks must have at least 1 item'.format(quantity))
self.stocks(name) = {
'price': price,
'quantity': quantity
}
self.total_items += quantity
اجرا کن pytest
تا ببینیم که آخرین آزمایش ما اکنون با موفقیت انجام می شود. حالا بیایید دومین مورد تست خطا را اضافه کنیم، اگر موجودی ما نتواند آن را ذخیره کند، یک استثنا ایجاد می شود. تست را به صورت زیر تغییر دهید:
@pytest.mark.parametrize('name,price,quantity,exception', (
('Test Jacket', 10.00, 0, InvalidQuantityException(
'Cannot add a quantity of 0. All new stocks must have at least 1 item')),
('Test Jacket', 10.00, 25, NoSpaceException(
'Cannot add these 25 items. Only 10 more items can be stored'))
))
def test_add_new_stock_bad_input(name, price, quantity, exception):
inventory = Inventory(10)
try:
inventory.add_new_stock(name, price, quantity)
except (InvalidQuantityException, NoSpaceException) as inst:
assert isinstance(inst, type(exception))
assert inst.args == exception.args
else:
pytest.fail("Expected error but found none")
به جای ایجاد یک تابع کاملاً جدید، این یکی را کمی تغییر می دهیم تا استثنای جدید خود را انتخاب کنیم و یک تاپل دیگر به دکوراتور اضافه کنیم! اکنون دو تست اجرا می شود روی یک تابع واحد
توابع پارامتری شده کاهش می یابد روی زمان لازم برای افزودن موارد آزمایشی جدید
که در inventory.py
، ابتدا استثنای جدید خود را در زیر اضافه می کنیم InvalidQuantityException
:
class NoSpaceException(Exception):
pass
و تغییر دهید add_new_stock()
روش:
def add_new_stock(self, name, price, quantity):
if quantity <= 0:
raise InvalidQuantityException(
'Cannot add a quantity of {}. All new stocks must have at least 1 item'.format(quantity))
if self.total_items + quantity > self.limit:
remaining_space = self.limit - self.total_items
raise NoSpaceException(
'Cannot add these {} items. Only {} more items can be stored'.format(quantity, remaining_space))
self.stocks(name) = {
'price': price,
'quantity': quantity
}
self.total_items += quantity
اجرا کن pytest
تا ببینید که تست جدید شما نیز قبول می شود.
ما می توانیم از فیکسچرها با تابع پارامتری خود استفاده کنیم. بیایید آزمایش خود را برای استفاده از موجودی خالی ثابت کنیم:
def test_add_new_stock_bad_input(no_stock_inventory, name, price, quantity, exception):
try:
no_stock_inventory.add_new_stock(name, price, quantity)
except (InvalidQuantityException, NoSpaceException) as inst:
assert isinstance(inst, type(exception))
assert inst.args == exception.args
else:
pytest.fail("Expected error but found none")
مانند قبل، این فقط یک آرگومان دیگر است که از نام یک تابع استفاده می کند. نکته کلیدی این است که آن را در دکوراتور پارامتری حذف کنید.
با نگاهی بیشتر به کد، دلیلی وجود ندارد که دو روش برای اضافه کردن سهام جدید وجود داشته باشد. ما می توانیم خطاها را آزمایش کنیم و موفقیت در یک عملکرد
حذف test_add_new_stock_bad_input()
و test_add_new_stock_success()
و اجازه دهید یک تابع جدید اضافه کنیم:
@pytest.mark.parametrize('name,price,quantity,exception', (
('Test Jacket', 10.00, 0, InvalidQuantityException(
'Cannot add a quantity of 0. All new stocks must have at least 1 item')),
('Test Jacket', 10.00, 25, NoSpaceException(
'Cannot add these 25 items. Only 10 more items can be stored')),
('Test Jacket', 10.00, 5, None)
))
def test_add_new_stock(no_stock_inventory, name, price, quantity, exception):
try:
no_stock_inventory.add_new_stock(name, price, quantity)
except (InvalidQuantityException, NoSpaceException) as inst:
assert isinstance(inst, type(exception))
assert inst.args == exception.args
else:
assert no_stock_inventory.total_items == quantity
assert no_stock_inventory.stocks(name)('price') == price
assert no_stock_inventory.stocks(name)('quantity') == quantity
این یک تابع تست ابتدا استثناهای شناخته شده را بررسی می کند، اگر هیچ موردی یافت نشد، مطمئن می شویم که اضافه با انتظارات ما مطابقت دارد. جدا test_add_new_stock_success()
تابع اکنون فقط از طریق یک پارامتر tuple-d اجرا می شود. از آنجایی که انتظار نداریم در مورد موفق استثنایی ایجاد شود، مشخص می کنیم None
به عنوان استثنای ما
جمع بندی مدیر موجودی ما
با پیشرفته تر ما pytest
استفاده، ما به سرعت می توانیم توسعه دهیم remove_stock
عملکرد با TDD که در inventory_test.py
:
from inventory import Inventory, InvalidQuantityException, NoSpaceException, ItemNotFoundException
@pytest.fixture
def ten_stock_inventory():
"""Returns an inventory with some test stock items"""
inventory = Inventory(20)
inventory.add_new_stock('Puma Test', 100.00, 8)
inventory.add_new_stock('Reebok Test', 25.50, 2)
return inventory
@pytest.mark.parametrize('name,quantity,exception,new_quantity,new_total', (
('Puma Test', 0,
InvalidQuantityException(
'Cannot remove a quantity of 0. Must remove at least 1 item'),
0, 0),
('Not Here', 5,
ItemNotFoundException(
'Could not find Not Here in our stocks. Cannot remove non-existing stock'),
0, 0),
('Puma Test', 25,
InvalidQuantityException(
'Cannot remove these 25 items. Only 8 items are in stock'),
0, 0),
('Puma Test', 5, None, 3, 5)
))
def test_remove_stock(ten_stock_inventory, name, quantity, exception,
new_quantity, new_total):
try:
ten_stock_inventory.remove_stock(name, quantity)
except (InvalidQuantityException, NoSpaceException, ItemNotFoundException) as inst:
assert isinstance(inst, type(exception))
assert inst.args == exception.args
else:
assert ten_stock_inventory.stocks(name)('quantity') == new_quantity
assert ten_stock_inventory.total_items == new_total
و در ما inventory.py
ابتدا برای زمانی که کاربران سعی می کنند سهامی را که وجود ندارد تغییر دهند، استثنا جدید ایجاد می کنیم:
class ItemNotFoundException(Exception):
pass
و سپس این روش را به خود اضافه می کنیم Inventory
کلاس:
def remove_stock(self, name, quantity):
if quantity <= 0:
raise InvalidQuantityException(
'Cannot remove a quantity of {}. Must remove at least 1 item'.format(quantity))
if name not in self.stocks:
raise ItemNotFoundException(
'Could not find {} in our stocks. Cannot remove non-existing stock'.format(name))
if self.stocks(name)('quantity') - quantity <= 0:
raise InvalidQuantityException(
'Cannot remove these {} items. Only {} items are in stock'.format(
quantity, self.stocks(name)('quantity')))
self.stocks(name)('quantity') -= quantity
self.total_items -= quantity
وقتی می دوی pytest
باید ببینید که آزمون ادغام و بقیه قبول می شوند!
نتیجه
Test-Driven Development یک توسعه نرم افزاری است process که در آن از تست ها برای هدایت طراحی یک سیستم استفاده می شود. TDD الزام میکند که برای هر ویژگی که باید پیادهسازی کنیم، آزمایشی بنویسیم که ناموفق باشد، کمترین کد را برای موفقیت در آزمون اضافه کنیم و در نهایت آن کد را اصلاح کنیم تا پاکتر شود.
برای ساختن این process ممکن و کارآمد، ما از آن استفاده کردیم pytest
– یک ابزار تست خودکار با pytest
ما می توانیم تست های اسکریپت را انجام دهیم و در زمان ما از آزمایش دستی کد هر تغییر صرفه جویی کنیم.
تستهای واحد برای اطمینان از رفتار یک ماژول منفرد همانطور که انتظار میرود استفاده میشود، در حالی که تستهای یکپارچهسازی اطمینان میدهند که مجموعهای از ماژولها همانطور که ما انتظار داریم با هم کار میکنند. هر دو pytest
ابزار و روش TDD امکان استفاده از هر دو نوع تست را فراهم می کند و توسعه دهندگان تشویق می شوند از هر دو استفاده کنند.
با TDD، ما مجبوریم به ورودی ها و خروجی های سیستم خود و در نتیجه طراحی کلی آن فکر کنیم. نوشتن تستها مزایای دیگری مانند افزایش اطمینان در عملکرد برنامه ما پس از تغییرات را فراهم میکند. TDD به شدت تکراری است process که می تواند با استفاده از یک مجموعه تست خودکار مانند کارآمد باشد pytest
. با ویژگیهایی مانند وسایل و عملکردهای پارامتری، میتوانیم بهسرعت موارد تست را بر اساس نیازهای خود بنویسیم.
(برچسبها به ترجمه)# python
منتشر شده در 1403-01-23 03:50:04