Ачааллж байна...
Одоог хүртэл бидний бүх тест нэг төрлийн асуулт асуусан: "энэ оролтод зөв үр дүн гарах уу?" Гэхдээ функцийн гэрээний нөгөө тал бий: буруу оролт ирэхэд юу болох ёстой вэ? Хөнгөлөлтийн функцэд 150 хувь гэж өгвөл үнэ сөрөг болох уу, эсвэл функц "энэ боломжгүй" гэж эсэргүүцэх ёстой юу? Өмнөх сургалтын raise-ийн хичээлээс санаж байгаачлан сайн функц буруу оролтыг чимээгүй хүлээж авдаггүй, харин тодорхой алдаа гаргадаг. Хэрвээ тэр алдаа таны функцийн санаатай зан төлөв бол тэр ч бас тестлэгдэх ёстой: "энэ оролтод яг энэ алдаа гарах ёстой".
Энэ хичээлд бид pytest.raises-ийг сурна. Эхлээд prices модульд оролтоо шалгадаг хөнгөлөлтийн функц бичнэ. Дараа нь pytest.raises-ээр алдаа гарч байгааг шалгаж, алдаа гараагүй үед тест хэрхэн унадгийг харна. Алдааны мессежийг match-ээр шалгаж, буруу оролтуудыг parametrize-тэй хослуулна. Эцэст нь хэт ерөнхий алдааны шалгалтын аюул болон ямар алдааг тестлэх нь утгатай тухай шударга ярина. Бид pytest-dadlaga фолдер дээрээ Бүлэг 8-ыг дуусгана.
Барилгын галын дохиолол ердийн өдөр чимээгүй байдаг бөгөөд энэ нь зөв. Харин утаа гарахад заавал дуугарах ёстой. Аюулгүй ажиллагааны байцаагч дохиоллыг шалгахдаа хоёр зүйлийг шалгадаг: утаагүй үед чимээгүй байна уу, утаа гарахад дуугарна уу. Хоёр дахь шалгалт илүү чухал: дуугардаггүй дохиолол хэнийг ч хамгаалахгүй, тэгсэн ч тэр ердийн өдөр яг адилхан чимээгүй харагдана. Тиймээс байцаагч утааны тусгай хэрэгслээр зориуд утаа үүсгэж, дохиолол дуугарч байгааг өөрийн чихээр сонсдог.
pytest.raises бол тэр байцаагчийн утааны хэрэгсэл. Таны функц буруу оролтод алдаа гаргах ёстой гэж байгаа бол тест зориуд буруу оролт өгч, алдаа үнэхээр гарч байгааг шалгана. Хэрвээ хэн нэгэн хожим функцийн шалгалтыг санамсаргүй устгавал функц ердийн оролтод хэвийн ажилласаар байх тул бусад бүх тест давна. Зөвхөн энэ тест л "дохиолол дуугарсангүй" гэж мэдэгдэнэ.
Алхам 1: Оролтоо шалгадаг функц
VS Code-д pytest-dadlaga фолдероо нээж, venv идэвхтэй эсэхийг шалгаад, prices.py-ийн төгсгөлд дараах функцийг нэмнэ үү. Хэрвээ та өмнөх хичээлүүдийн "туршаад үзээрэй" хэсгүүдэд apply_discount бичсэн бол түүнийг энэ хувилбараар солино уу.
python
def apply_discount(price, percent):
if percent < 0 or percent > 100:
raise ValueError(f"Discount must be between 0 and 100, got {percent}")
return price - price * percent // 100Функц үнээс хувийн хөнгөлөлт хасна. add_vat-ийн нэгэн адил бүхэл хуваалт ашиглаж, үр дүнг бүхэл төгрөг байлгана. Гэхдээ эхний хоёр мөр шинэ: хэрвээ хувь тэгээс бага эсвэл зуунаас их бол функц ValueError гаргана. Энэ бол өмнөх сургалтын raise: "энэ утга буруу, үргэлжлүүлэх боломжгүй" гэж дуудагчид мэдэгдэх арга. Мессеж нь юу буруу болсон, ямар утга ирснийг хэлдэг.
Яагаад энэ шалгалт хэрэгтэй вэ? Шалгалтгүй бол юу болохыг харцгаая. Шалгалтын хоёр мөрийг түр тайлбар болгоод, өөрөөр хэлбэл мөр бүрийн эхэнд “#” тавиад хадгалж, дараах command-ыг ажиллуулна уу.
Command:
python -c "from prices import apply_discount; print(apply_discount(1000, 150))"Гаралт:
-500Мянган төгрөгийн бараа 150 хувийн хөнгөлөлттэй болоход дэлгүүр худалдан авагчид 500 төгрөг өгөх ёстой болжээ. Функц ямар ч эсэргүүцэлгүйгээр утгагүй үр дүн буцаасан. Энэ бол хамгийн аюултай төрлийн алдаа: программ унахгүй, гэхдээ буруу тоо чимээгүйхэн цааш дамжиж, тайлан, баримт, тооцоонд орно. Чанга алдаа чимээгүй буруу утгаас хавьгүй дээр, учир нь алдааг та тэр дороо харна. Хоёр мөрийн “#” тэмдгийг устгаж, шалгалтыг буцааж идэвхжүүлээд хадгална уу.
Command:
python -c "from prices import apply_discount; print(apply_discount(1000, 150))"Гаралт:
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "C:\Users\Bat\pytest-dadlaga\prices.py", line 26, in apply_discount
raise ValueError(f"Discount must be between 0 and 100, got {percent}")
ValueError: Discount must be between 0 and 100, got 150Одоо функц эсэргүүцэж байна. Доороос нь уншвал: ValueError, мессеж нь "хөнгөлөлт 0-ээс 100-ийн хооронд байх ёстой, 150 ирсэн". Хэн ч энэ мессежийг хараад асуудлыг шууд ойлгоно. Таны мөрийн дугаар prices.py-ийн бүтцээс хамаараад өөр байж болно.
Алхам 2: Ердийн зан төлөвийг тестлэх
Эхлээд функцийн ердийн ажлыг 8.3 дугаар хичээлийн хүснэгтээр тестлэе. test_prices.py-ийн import-д apply_discount нэмээд, файлын төгсгөлд бичнэ үү.
python
@pytest.mark.parametrize(
"price, percent, expected",
[
(1000, 0, 1000),
(1000, 10, 900),
(999, 10, 900),
(1000, 100, 0),
],
)
def test_apply_discount(price, percent, expected):
assert apply_discount(price, percent) == expectedХүснэгтийг толгойдоо шалгая. Тэг хувь үнийг өөрчлөхгүй. Арван хувь мянгаас зууг хасна. 999-ийн арван хувь 99.9, доош тайрвал 99, хасвал 900. Зуун хувь үнийг тэг болгоно. Тэг ба зуу бол зөвшөөрөгдөх хамгийн бага, хамгийн их утга, хил дээрх тохиолдлууд.
Алхам 3: Алдааг тестлэх: pytest.raises
Одоо галын дохиоллын шалгалт. Файлын төгсгөлд нэмнэ үү.
python
def test_apply_discount_rejects_over_100():
with pytest.raises(ValueError):
apply_discount(1000, 150)Энэ тестэд assert байхгүй. Оронд нь “with pytest.raises(ValueError):” мөр бий. with түлхүүр үгийг та өмнөх сургалтын файлтай ажиллах хичээлээс with open(...) хэлбэрээр таньж байгаа: тэр нь доорх догол мөртэй блокийн эргэн тойронд тусгай зан төлөв нэмдэг. Энд pytest.raises нь "доорх блок ValueError гаргах ёстой" гэсэн хүлээлтийг тогтоодог. Блокийн дотор apply_discount-ийг 150 хувиар дуудна.
Гурван боломжит үр дүн бий. Нэгдүгээрт, блок ValueError гаргавал pytest.raises тэр алдааг барьж, тест давна: дохиолол дуугарсан, бүх зүйл зөв. Хоёрдугаарт, блок ямар ч алдаагүй дуусвал тест унана: дохиолол дуугарах ёстой байсан ч дуугарсангүй. Гуравдугаарт, блок өөр төрлийн алдаа, жишээ нь TypeError гаргавал pytest.raises түүнийг барихгүй, тэр алдаа цааш дамжиж тест унана: дохиолол дуугарсан ч буруу дохиогоор.
Command:
pytest -k apply_discount -vГаралт:
collected 37 items / 32 deselected / 5 selected
tests/test_prices.py::test_apply_discount[1000-0-1000] PASSED [ 20%]
tests/test_prices.py::test_apply_discount[1000-10-900] PASSED [ 40%]
tests/test_prices.py::test_apply_discount[999-10-900] PASSED [ 60%]
tests/test_prices.py::test_apply_discount[1000-100-0] PASSED [ 80%]
tests/test_prices.py::test_apply_discount_rejects_over_100 PASSED [100%]
====================== 5 passed, 32 deselected in 0.03s =======================Бүгд давсан. Таны нийт тестийн тоо өөр байж болно. Хүснэгтийн мөр бүрт гурван утгатай ID гарсныг анзаараарай: price, percent, expected.
Алхам 4: Дохиолол дуугарахгүй бол
Тест үнэхээр ажиллаж байгааг батлахын тулд дохиоллыг зориуд эвдье. prices.py-д шалгалтын хоёр мөрийг дахин “#”-ээр тайлбар болгоод хадгалж, ажиллуулна уу.
Command:
pytest -k rejectsГаралт:
_____________________ test_apply_discount_rejects_over_100 _____________________
def test_apply_discount_rejects_over_100():
> with pytest.raises(ValueError):
E Failed: DID NOT RAISE <class 'ValueError'>
tests\test_prices.py:88: Failed
=========================== short test summary info ===========================
FAILED tests/test_prices.py::test_apply_discount_rejects_over_100 - Failed: DID NOT RAISE <class 'ValueError'>
======================= 1 failed, 36 deselected in 0.04s ======================Уналтын блокийг уншъя. “>” тэмдэг with мөрийг заасан. “E Failed: DID NOT RAISE <class ‘ValueError’>” нь "амжилтгүй: ValueError гарсангүй" гэсэн утгатай. “<class ‘ValueError’>” нь хүлээж байсан алдааны төрөл. Сүүлийн мөрөнд алдааны төрөл “AssertionError” биш, “Failed” гэж байгааг анзаараарай: pytest өөрөө тестийг амжилтгүй гэж зарласан. Таны мөрийн дугаар өөр байж болно.
Энэ тайлан яг бидний хүссэн зүйлийг хийсэн: хэн нэгэн хамгаалалтын шалгалтыг устгасныг барьсан. Ердийн зан төлөвийн тестүүд, Алхам 2-ын хүснэгт, энэ өөрчлөлтийг огт анзаарахгүй байсан, учир нь тэдгээрийн бүх оролт зөв. pytest -k apply_discount ажиллуулбал хүснэгтийн дөрвөн тест давж, зөвхөн энэ тест унана. Шалгалтын хоёр мөрийг буцааж идэвхжүүлээд хадгална уу.
Алдааны мессежийг шалгах: match
pytest.raises анхдагчаар зөвхөн алдааны төрлийг шалгадаг. Гэхдээ заримдаа нэг функц өөр өөр шалтгаанаар ижил төрлийн алдаа гаргадаг бөгөөд зөв шалтгаанаар гарч байгааг шалгах нь чухал. Үүнд match тохиргоо бий.
python
def test_apply_discount_error_message():
with pytest.raises(ValueError, match="between 0 and 100"):
apply_discount(1000, 150)match-д өгсөн текст алдааны мессежийн дотор хаа нэгтээ байх ёстой. Манай мессеж “Discount must be between 0 and 100, got 150” тул “between 0 and 100” олдож, тест давна. Хэрвээ хожим функц огт өөр шалтгаанаар ValueError гаргаж эхэлбэл, жишээ нь “price must be positive”, энэ тест унаж, мессеж өөрчлөгдсөнийг мэдэгдэнэ.
Нэг шударга тэмдэглэл: match-ийн текстийг pytest regular expression буюу тогтмол илэрхийлэл гэж ойлгодог. Энэ нь текст хайх тусгай хэл бөгөөд цэг, хаалт, од зэрэг зарим тэмдэгт тусгай утгатай. Энгийн үг, тоо, зай ашиглавал ямар ч асуудалгүй. Харин мессежийн хаалт эсвэл цэг агуулсан хэсгийг шалгахыг хүсвэл тэдгээр тэмдэгт таны бодсоноор ажиллахгүй байж магадгүй. Тогтмол илэрхийллийг энэ сургалтад судлахгүй тул match-д тусгай тэмдэгтгүй энгийн хэсэг сонгох нь хамгийн найдвартай.
Олон буруу оролт: parametrize ба raises
Буруу оролт ганц биш. Хүснэгтийн сэтгэлгээг энд ч хэрэглэе.
python
@pytest.mark.parametrize("percent", [-50, -1, 101, 150])
def test_apply_discount_rejects_invalid_percent(percent):
with pytest.raises(ValueError):
apply_discount(1000, percent)Хүснэгт хоёр хилийн хоёр талыг хамарч байна: тэгийн доорх -1 ба -50, зуунаас дээших 101 ба 150. Ялангуяа -1 ба 101 чухал: яг хилийн гадна талын утгууд. Алхам 2-ын хүснэгтэд 0 ба 100, хилийн дотор талын утгууд, давж байгааг шалгасан. Хоёр хүснэгт хамтдаа хилийг хоёр талаас нь бүрэн хамгаалдаг: 0 зөвшөөрөгдөнө, -1 зөвшөөрөгдөхгүй, 100 зөвшөөрөгдөнө, 101 зөвшөөрөгдөхгүй. Хэрвээ хэн нэгэн шалгалтын нөхцөлийг “>” биш “>=” болгож андуурвал энэ хил дээрх тестүүд шууд барина. Энэ тест өмнөх test_apply_discount_rejects_over_100-ийг агуулж байгаа тул түүнийг устгаж болно.
Алдааны дэлгэрэнгүйг шалгах
Заримдаа алдааны мессежийн нарийн агуулгыг assert-ээр шалгах хэрэгтэй болно. pytest.raises барьсан алдаагаа хадгалах боломжтой.
python
def test_apply_discount_error_shows_value():
with pytest.raises(ValueError) as exc_info:
apply_discount(1000, 150)
assert "150" in str(exc_info.value)“as exc_info” нь барьсан алдааны мэдээллийг exc_info нэртэй хувьсагчид хадгална. Блок дууссаны дараа, with-ийн догол мөрөөс гадна, exc_info.value нь алдааны объект өөрөө бөгөөд str() нь түүний мессежийг буцаана. Тэгээд 7.3 дугаар хичээлийн in-ээр мессежид буруу утга дурдагдсан эсэхийг шалгана. Энэ нь сайн алдааны мессеж хэрэглэгчид ямар утга асуудал үүсгэснийг хэлдэг гэсэн гэрээг баталгаажуулна. Ихэнх тохиолдолд match хангалттай бөгөөд энэ хэлбэрийг илүү нарийн шалгалт хэрэгтэй үед л ашиглана.
Аюул: хэт ерөнхий алдаа
pytest.raises-д ValueError-ийн оронд бүх алдааны эх болох Exception-ийг өгөх уруу таталт бий: "ямар ч алдаа гарвал болох нь". Энэ нь аюултай. Дараах тестийг уншаарай, бичих шаардлагагүй.
python
def test_apply_discount_rejects_bad_percent_too_broad():
with pytest.raises(Exception):
apply_dicsount(1000, 150)Функцийн нэрэнд үсгийн алдаа байна: “apply_dicsount”. Python энэ нэрийг олохгүй тул NameError гаргана. NameError бол Exception-ийн нэг төрөл тул pytest.raises түүнийг барьж, тест давна. Тест ногоон, гэтэл apply_discount хэзээ ч дуудагдаагүй. Хөнгөлөлтийн шалгалтыг бүрмөсөн устгасан ч энэ тест давсаар байх болно. Харуул дохиоллыг шалгах гэж ирээд өөрийн хөлөө гишгэснээ "дохиолол дуугарлаа" гэж тэмдэглэсэнтэй адил. Тиймээс pytest.raises-д үргэлж таны функцийн гаргах ёстой яг тэр алдааны төрлийг өгөөрэй.
Мөн with блокийн дотор зөвхөн алдаа гаргах ёстой нэг дуудлагыг бичих нь чухал. Хэрвээ блокт хэд хэдэн мөр байвал аль нь алдаа гаргасан нь тодорхойгүй болно. Мөн алдаа гарсан мөрөөс хойших мөрүүд хэзээ ч ажиллахгүй, учир нь алдаа гармагц блок дуусдаг. Хэрвээ блокийн дотор алдааны дараа assert бичсэн бол тэр assert хэзээ ч шалгагдахгүй бөгөөд тест худал итгэл төрүүлнэ.
Ямар алдааг тестлэх вэ
7.4 дүгээр хичээлд format_price-д текст өгөхөд ValueError гарахыг харсан бөгөөд "үүнийг 8.4-д зөв бичиж сурна" гэж хэлсэн. Одоо шударга асуулт тавья: тэр алдааг тестлэх ёстой юу? Тэр ValueError-ийг бид санаатайгаар бичээгүй: энэ нь Python-ий f-string текстэд таслал тавих боломжгүй учраас санамсаргүй гарсан. Мессеж нь “Cannot specify ‘,’ with ‘s’” гэх мэт ойлгомжгүй. Хэрвээ бид үүнийг тестэд түгжвэл кодыг сайжруулах, жишээ нь илүү ойлгомжтой мессеж нэмэх бүрт тест эвдэрнэ.
Дүрэм ийм: pytest.raises-ээр зөвхөн таны функцийн гэрээний нэг хэсэг болох, санаатайгаар бичсэн алдааг тестлэ. apply_discount-ийн ValueError бол гэрээ: бид түүнийг зориуд бичиж, ойлгомжтой мессеж өгсөн. Хэрвээ format_price текстийг хүлээж авахгүй байх нь гэрээний хэсэг гэж та шийдвэл эхлээд функцэд тодорхой шалгалт, жишээ нь if not isinstance(amount, int): raise TypeError(...), бичиж, дараа нь түүнийг тестлэ. Санамсаргүй алдааг биш, санаатай шийдвэрийг тестлэ.
Хүсвэл receipt модульд оролтын шалгалт нэмж үзээрэй. Барааны тоо ширхэг тэг эсвэл сөрөг байх нь утгагүй. receipt_total-ийн эхэнд бараа бүрийг шалгаж, qty нь тэгээс бага эсвэл тэнцүү бол ValueError гаргадаг болгоорой, ойлгомжтой мессежтэй. Дараа нь test_receipt.py-д буруу тоо ширхэгтэй баримтуудын хүснэгт бичиж, pytest.raises ба match-ээр шалгаарай. Мөн 8.3 дугаар хичээлийн ердийн хүснэгт давсаар байгаа эсэхийг баталгаажуулаарай. Шалгалтыг түр “#”-ээр унтрааж, шинэ тестүүд “DID NOT RAISE”-ээр унаж байгааг харвал тестүүд тань үнэхээр ажиллаж байна.
DID NOT RAISE.
E Failed: DID NOT RAISE <class 'ValueError'>Функц хүлээгдэж буй алдааг гаргасангүй. 7.4 дүгээр хичээлийн "код уу, тест үү" асуултыг тавь. Хэрвээ функц энэ оролтод алдаа гаргах ёстой бол функцийн шалгалт дутуу эсвэл буруу: нөхцлөө шалга, ялангуяа “<” ба “<=” хоёрыг. Хэрвээ энэ оролт үнэндээ зөвшөөрөгдөх ёстой бол, жишээ нь яг 100 хувь, тест буруу утга сонгосон байна. Хилийн утгуудыг гараар дахин бод.
Өөр төрлийн алдаа гарах. Хэрвээ тест ValueError хүлээж байхад функц TypeError гаргавал pytest.raises түүнийг барихгүй бөгөөд тайланд 7.4 дүгээр хичээлийн "код огт унах" хэлбэрийн блок гарна: тасархай шугам, функц доторх “>” тэмдэг, сүүлийн мөрөнд “TypeError”. Энэ нь хоёр боломжтой: функц буруу төрлийн алдаа гаргаж байна, эсвэл тестийн оролт таны бодсоноос өөр шалтгаанаар, жишээ нь буруу төрлийн аргументаар, унаж байна. Тайлангийн параметрүүдийн утга болон “>” тэмдгийн заасан мөрийг уншиж шалтгааныг ол.
match таарахгүй байх.
E AssertionError: Regex pattern did not match.
E Expected regex: 'between 1 and 100'
E Actual message: 'Discount must be between 0 and 100, got 150'"Тогтмол илэрхийллийн загвар таарсангүй" гэсэн утгатай. “Expected regex” нь таны match-д өгсөн текст, “Actual message” нь жинхэнэ алдааны мессеж. Таны pytest-ийн хувилбараас хамаараад эдгээр мөрийн шошго бага зэрэг өөр байж болох ч бүтэц нь ижил. Хоёр мөрийг харьцуулбал ялгаа шууд харагдана: энд “1” ба “0”. Алдаа нь зөв төрлийн, гэхдээ мессеж нь таны хүлээснээс өөр. Хэрвээ мессеж санаатайгаар өөрчлөгдсөн бол тестийг шинэчил, санамсаргүй бол кодыг шалга. Хэрвээ мессеж нь хаалт, цэг агуулсан бол match-ийн тусгай тэмдэгтийн асуудал байж болно: тэр хэсгийг тусгай тэмдэгтгүй хэсгээр солиорой.
with блокийн догол мөрийг мартах. Хэрвээ дуудлагыг with-ийн доор догол мөргүйгээр бичвэл, өөрөөр хэлбэл блокийн гадна, Python IndentationError гаргана эсвэл дуудлага pytest.raises-ийн хамгаалалтаас гадуур ажиллана. Хоёр дахь тохиолдолд ValueError барьцгүй гарч, тест “ValueError: Discount must be...” гэсэн уналтын блоктой унана. VS Code-д дуудлагын мөр with мөрөөс нэг догол мөрөөр дотогш байгаа эсэхийг шалгаарай.
import pytest-ийг мартах. pytest.raises ч pytest.approx шиг pytest модулийн хэсэг тул файлын эхэнд import pytest шаардлагатай. Мартвал 7.3 дугаар хичээлийн “NameError: name ‘pytest’ is not defined” алдаа гарна.
Функцийн гэрээ зөвхөн зөв оролтод зөв үр дүн гаргахаас гадна буруу оролтыг тодорхой алдаагаар эсэргүүцэхийг багтаадаг, учир нь чимээгүй буруу утга чанга алдаанаас хавьгүй аюултай. with pytest.raises(АлдааныТөрөл): блок тухайн алдааг гаргах ёстойг шалгадаг: алдаа гарвал тест давна, гараагүй бол “DID NOT RAISE”-ээр унана, өөр төрлийн алдаа гарвал тэр алдаа цааш дамжиж тест унана. match="текст" алдааны мессежийг шалгадаг бөгөөд текстийг тогтмол илэрхийлэл гэж ойлгодог тул тусгай тэмдэгтгүй энгийн хэсэг сонгоно. parametrize-тэй хослуулж хилийн хоёр талыг бүрэн шалгана: зөвшөөрөгдөх хамгийн бага ба их утгууд, мөн тэдгээрийн яг гадна талын утгууд. as exc_info барьсан алдааг хадгалж, exc_info.value-ээр нарийн шалгалт хийх боломж олгоно. Exception шиг хэт ерөнхий төрөл үсгийн алдааг ч барьж, худал итгэл төрүүлдэг тул яг тодорхой төрлийг өгнө. with блокт зөвхөн алдаа гаргах ёстой нэг дуудлагыг бичнэ. Зөвхөн таны гэрээний нэг хэсэг болох, санаатайгаар бичсэн алдааг тестлэнэ.
Ингээд Бүлэг 8 дууслаа. Та тестүүдээ зохион байгуулж, fixture, parametrize, raises гэсэн pytest-ийн үндсэн гурван хэрэгслийг эзэмшлээ. Дараагийн Бүлэгт эдгээрийг бодит кодонд хэрэглэнэ. Бүлэг 9-ийн эхний хичээлд файл ба JSON-той ажилладаг кодыг тестлэх асуудлыг шийднэ: таны жинхэнэ өгөгдлийн файлд хүрэлгүйгээр хадгалах, ачаалах функцуудыг хэрхэн тестлэх вэ? Үүний хариу нь pytest-ийн бэлэн fixture tmp_path юм.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.