Ачааллж байна...
Бүлэг 7-оос хойш бид олон функц тестэлсэн. Ихэнх нь маш хялбар байсан: add_vat-д тоо өгөөд үр дүнг нь шалгахад нэг мөр хангалттай. Харин өмнөх хичээлд файлтай ажилладаг функцийг тестлэхэд түр фолдер, зам, файл бичих, унших хэрэгтэй болсон. Энэ ялгаа санамсаргүй биш. Зарим код тестлэхэд угаасаа хялбар, зарим нь угаасаа хэцүү байдаг. 7.1 дүгээр хичээлд тестийн ашгуудыг ярихдаа "тест кодын бүтцийн тухай санал өгдөг" гэж дурдаад, энэ сэдвийг 9.2-т нэг практик жишээгээр үзнэ гэж амласан. Одоо тэр амлалтаа биелүүлье.
Энэ хичээл архитектурын тухай урт лекц биш, ганцхан практик санааны тухай: тооцооллыг гадаад ертөнцтэй харилцахаас салга. Эхлээд энэ санааны хоёр нэр томьёог, цэвэр функц ба гаж нөлөөг, энгийн үгээр тайлбарлана. Дараа нь тооцоолол, файл унших, хэвлэх бүгдийг нэг дор хийдэг функцийг бичиж, түүнийг тестлэх нь хэр төвөгтэйг өөрийн нүдээр харна. Тэгээд жижиг өөрчлөлтөөр түүнийг хоёр хэсэгт хувааж, тест хэрхэн энгийн болохыг харьцуулна. Энэ санаа хэзээ хэрэгтэй, хэзээ хэтрүүлэг болохыг шударгаар ярьж дуусна.
Цэвэр функц буюу pure function гэдэг нь хоёр шинжтэй функц юм. Нэгдүгээрт, ижил оролтод үргэлж ижил үр дүн буцаана. Хоёрдугаарт, үр дүнгээ буцаахаас өөр юу ч хийдэггүй: файлд бичдэггүй, дэлгэцэнд хэвлэдэггүй, өгсөн жагсаалтыг өөрчилдөггүй. add_vat бол цэвэр функц: add_vat(1000) өнөөдөр ч, маргааш ч, таны компьютер дээр ч, өөр хүнийх дээр ч 1100.
Гаж нөлөө буюу side effect гэдэг нь функц үр дүнгээ буцаахаас гадна гадаад ертөнцөд хийдэг аливаа зүйл юм. Файл унших, бичих, print(), input()-ээр хэрэглэгчээс асуух, одоогийн цагийг авах, санамсаргүй тоо гаргах, интернэтэд хандах, мөн өгсөн жагсаалтыг append зэргээр өөрчлөх. Эдгээр нь муу зүйл биш: гаж нөлөөгүй программ хэнд ч хэрэггүй, учир нь үр дүнгээ хэнд ч харуулж, хаана ч хадгалж чадахгүй. Гэхдээ гаж нөлөөтэй функцийг тестлэх нь хэцүү, учир нь тест тэр гадаад ертөнцийг бэлдэж, дуурайж, шалгах хэрэгтэй болдог.
Үүнийг тооны машин ба шуудангийн ажилтантай зүйрлэж болно. Тооны машинд 25 үржүүлэх 4 гэж өгвөл үргэлж 100 гэж хариулна. Түүнийг шалгахад тоо оруулж, хариуг харахад л хангалттай. Харин шуудангийн ажилтан илгээмжийн жинг тооцоолоод, үнийг нь бодоод, баримт хэвлэж, илгээмжийг тавиур дээр тавьдаг. Түүний тооцооллыг шалгахыг хүсвэл та жинхэнэ илгээмж авчирч, хэвлэсэн баримтыг уншиж, тавиурыг шалгах хэрэгтэй болно. Хэрвээ ажилтан тооцооллоо тооны машинаар хийж, үр дүнг нь л баримтад бичдэг бол та тооны машиныг тусад нь хялбар шалгаж болно. Энэ хичээлийн санаа бол яг энэ: тооцооллыг тооны машинд, гаж нөлөөг ажилтанд.
Өмнө
Баримтыг файлаас уншиж, дэлгэцэнд хэвлэдэг функц бичье. Ийм функцийг бүх хүн эхлээд байгалийн жамаар ингэж бичдэг. VS Code-д pytest-dadlaga фолдероо нээж, venv идэвхтэй эсэхийг шалгаад, receipt.py-ийн эхэнд import нэмж, төгсгөлд функц нэмнэ үү.
python
from prices import add_vat, format_price
from storage import load_itemspython
def print_receipt(path):
items = load_items(path)
for line in receipt_lines(items):
print(line)
print(f"Нийт: {format_price(receipt_total(items))}")Энэ функц гурван зүйл хийнэ: файлаас бараа уншина, мөр бүрийг хэвлэнэ, нийт дүнг хэвлэнэ. Код ойлгомжтой, богино, ажилладаг. Асуудал нь тестлэх үед л илэрнэ.
Тестлэх оролдлого
Энэ функцийг тестлэхийн тулд юу хэрэгтэй вэ? Функц зам авдаг тул жинхэнэ файл хэрэгтэй: өмнөх хичээлийн tmp_path. Функц юу ч буцаадаггүй, харин хэвлэдэг тул түүний хэвлэсэн зүйлийг барьж авах хэрэгтэй. pytest-д үүнд зориулсан capsys гэдэг бэлэн fixture бий: тест доторх print()-ийн гаралтыг барьж, текст болгон өгдөг. tests\test_receipt.py-ийн import-д print_receipt нэмж, мөн from storage import save_items гэсэн мөр нэмээд, төгсгөлд бичнэ үү.
python
def test_print_receipt(tmp_path, sample_items, capsys):
path = tmp_path / "items.json"
save_items(path, sample_items)
print_receipt(path)
output = capsys.readouterr().out
assert "Нийт: 13,200₮" in outputМөр бүрийг уншъя. Тест гурван fixture хүсэж байна. Эхний хоёр мөр жинхэнэ файл бэлдэнэ: түр зам, бараануудыг хадгалах. Гурав дахь мөр функцийг дуудна. Дөрөв дэх мөр capsys-ээс барьсан гаралтыг авна: readouterr() нь одоог хүртэл барьсан гаралтыг буцааж, “.out” нь ердийн print-ийн хэсэг. Эцэст нь assert гаралтад нийт дүн байгаа эсэхийг шалгана.
Command:
pytest -k print_receipt -vГаралт:
tests/test_receipt.py::test_print_receipt PASSED [100%]Тест ажилладаг. Шударгаар хэлэхэд capsys бол сайн хэрэгсэл бөгөөд хэвлэсэн гаралтыг заримдаа ингэж шалгах шаардлагатай. Гэхдээ энэ тестийн бүтцийг анзаараарай: жинхэнэ шалгалт нь сүүлийн нэг мөр, харин түүнээс өмнөх дөрвөн мөр бүгд гадаад ертөнцийг бэлдэх, барих ажил. Бидний жинхэнэ сонирхсон зүйл, "баримтын текст зөв үү", файлтай ч, дэлгэцтэй ч огт холбоогүй. Гэтэл тест энэ асуултад хариулахын тулд файл бичиж, гаралт барих ёстой болсон. Мөн тест нарийн шалгалт хийж чадахгүй байна: “in” зөвхөн нийт дүнгийн мөр гаралтад хаа нэгтээ байгааг шалгаж байгаа. Бүх мөрийг дарааллаар нь яг шалгах гэвэл гаралтын текстийг хувааж, мөр бүрийг харьцуулах хэрэгтэй болно.
Энэ төвөгшөөл бол тестийн өгч буй санал: "энэ функц хэт олон зүйл нэг дор хийж байна".
Дараа
Санааг хэрэгжүүлье. print_receipt-ийн дотор хоёр өөр ажил холилдсон: баримтын текстийг бүтээх, мөн файл унших болон хэвлэх. Эхнийх нь цэвэр тооцоолол, хоёр дахь нь гаж нөлөө. Тэдгээрийг хоёр функцэд хуваая. receipt.py-д print_receipt-ийг дараах хоёр функцээр солино уу.
python
def receipt_text(items):
lines = receipt_lines(items)
lines.append(f"Нийт: {format_price(receipt_total(items))}")
return "\n".join(lines)
def print_receipt(path):
print(receipt_text(load_items(path)))receipt_text бол тооны машин. Бараануудын жагсаалт авч, баримтын бүтэн текстийг буцаана. Файл уншдаггүй, хэвлэдэггүй, өгсөн жагсаалтыг өөрчилдөггүй: receipt_lines шинэ жагсаалт буцаадаг тул append тэр шинэ жагсаалтад хийгдэнэ. Ижил бараанд үргэлж ижил текст. “\n”.join(lines) нь мөрүүдийг шинэ мөрийн тэмдэгтээр холбож нэг текст болгоно.
print_receipt одоо нэг мөр: файлыг ачаалж, текстийг бүтээж, хэвлэнэ. Энэ бол шуудангийн ажилтан, бүх гаж нөлөө энд төвлөрсөн. Гэхдээ түүнд тооцоолол огт үлдээгүй, зөвхөн бусад функцуудыг холбож байна. Ийм нимгэн функцэд алдаа гарах газар маш бага.
Программын зан төлөв огт өөрчлөгдөөгүй гэдгийг анзаараарай: print_receipt яг өмнөх шигээ ажиллана. Тиймээс өмнө бичсэн test_print_receipt өөрчлөлтгүйгээр давах ёстой. Энэ бол 7.1 дүгээр хичээлд ярьсан "тест өөрчлөх зориг өгдөг" санааны жинхэнэ жишээ: бид кодын бүтцийг өөрчилсөн бөгөөд хуучин тест зан төлөв хадгалагдсаныг баталгаажуулна. Кодын бүтцийг зан төлөвийг нь өөрчлөлгүйгээр сайжруулахыг refactoring гэдэг.
Цэвэр функцийн тест
Одоо receipt_text-ийг тестлэе. tests\test_receipt.py-ийн import-д receipt_text нэмж, төгсгөлд бичнэ үү.
python
def test_receipt_text(sample_items):
assert receipt_text(sample_items) == (
"талх x2: 4,000₮\n"
"сүү x1: 3,000₮\n"
"өндөг x10: 5,000₮\n"
"Нийт: 13,200₮"
)
def test_receipt_text_empty():
assert receipt_text([]) == "Нийт: 0₮"Хоёр тестийг өмнөхтэй харьцуулаарай. Түр файл байхгүй, гаралт барих байхгүй, зөвхөн оролт ба хүлээгдэж буй үр дүн. Эхний тест баримтын бүх мөрийг, тэдгээрийн дарааллыг, нийт дүнг нэг дор яг шалгаж байна: өмнөх тест хийж чадаагүй нарийвчлал. Хашилт доторх урт текстийг хэд хэдэн мөрөнд хувааж бичсэн нь Python-ий онцлог: хаалтан доторх зэрэгцээ текстүүдийг Python автоматаар нэг текст болгон залгадаг тул хүлээгдэж буй баримт яг баримт шиг уншигдаж байна. Хоёр дахь тест хил дээрх тохиолдол: хоосон баримт зөвхөн нийт мөртэй байх ёстой.
Command:
pytest -k receipt -vГаралт (гол мөрүүд):
tests/test_receipt.py::test_print_receipt PASSED
tests/test_receipt.py::test_receipt_text PASSED
tests/test_receipt.py::test_receipt_text_empty PASSEDГурвуулаа давсан, хуучин тест мөн. Одоо receipt_text-ийн алдааг шалгахын тулд файл, дэлгэц хэрэггүй.
Command:
python -c "from receipt import receipt_text; print(receipt_text([('талх', 2000, 2)]))"Гаралт:
талх x2: 4,000₮
Нийт: 4,400₮Цэвэр функцийн бас нэг ашиг: та түүнийг terminal-аас ямар ч бэлтгэлгүйгээр туршиж болно.
test_print_receipt-ийг үлдээх үү?
Одоо test_print_receipt-ийн шалгадаг зүйлийн ихэнхийг test_receipt_text илүү нарийн шалгаж байна. Тэгвэл хуучин тестийг устгах уу? Шударга хариулт: нэгийг үлдээх нь утгатай. receipt_text-ийн тестүүд текст зөв бүтээгдэж байгааг шалгадаг бол test_print_receipt бүх хэсэг, файл ачаалах, текст бүтээх, хэвлэх, хамтдаа холбогдож ажиллаж байгааг шалгадаг. 7.1 дүгээр хичээлийн тестийн пирамидыг санаарай: олон жижиг unit test ба цөөн integration test. Одоо бидэнд яг ийм хослол бий: цэвэр функцийн нарийн тестүүд, мөн бүх зүйлийг холбосон нэг тест. Харин бүх нарийн шалгалтыг capsys-тэй тестэд бичих шаардлагагүй болсон.
Нуугдмал оролт: одоогийн цаг
Гаж нөлөө зөвхөн файл, хэвлэлт биш. Баримтад огноо нэмэх функцийг бодъё. Байгалийн жамаар ингэж бичдэг.
python
from datetime import date
def receipt_header():
return f"Баримт {date.today():%Y-%m-%d}"Энэ функц цэвэр биш, хэдийгээр юу ч хэвлэхгүй, файлд хүрэхгүй ч. Учир нь date.today() өдөр бүр өөр утга буцаадаг: өнөөдөр "Баримт 2026-10-08", маргааш "Баримт 2026-10-09". Ижил оролтод, энд огт оролтгүй, өөр үр дүн. Түүнийг яг текстээр тестлэх боломжгүй: тест маргааш унана.
Ижил санааг хэрэглэе: нуугдмал оролтыг илэрхий параметр болго.
python
def receipt_header(day):
return f"Баримт {day:%Y-%m-%d}"Одоо функц цэвэр: ижил огноонд үргэлж ижил текст. Тест тодорхой огноо өгнө.
python
from datetime import date
def test_receipt_header():
assert receipt_header(date(2026, 10, 8)) == "Баримт 2026-10-08"Харин date.today()-г дууддаг цорын ганц газар бол программын ирмэг дээрх нимгэн функц, жишээ нь print_receipt: receipt_header(date.today()). Санамсаргүй тоо, хэрэглэгчийн оролт гэх мэт бусад нуугдмал оролтод ч яг ижил арга хэрэглэгддэг: тэдгээрийг функцийн доторх нууцаас параметр болгож гаргаж ир.
Өгсөн утгыг өөрчлөх
Өөр нэг нарийн гаж нөлөө бий. 8.2 дугаар хичээлд fixture-ийн жагсаалтад append хийсэн тестийг санаарай. Функц өгсөн жагсаалтыг өөрчилбөл дуудагч тэр өөрчлөлтийг хүлээгээгүй байж болно.
python
def add_item(items, item):
items.append(item)
return itemsЭнэ функц шинэ жагсаалт буцааж байгаа мэт харагдах ч үнэндээ дуудагчийн анхны жагсаалтыг өөрчилдөг. Хэрвээ дуудагч анхны баримтаа хадгалахыг хүссэн бол тэр нь чимээгүйхэн өөрчлөгдсөн байна. Цэвэр хувилбар нь шинэ жагсаалт үүсгэнэ.
python
def add_item(items, item):
return items + [item]“items + [item]” нь анхны жагсаалтыг хөндөлгүйгээр шинэ жагсаалт үүсгэдэг. Энэ хоёр хувилбарын ялгааг тест маш тодорхой барьж чадна: функцийг дуудсаны дараа анхны жагсаалт өөрчлөгдөөгүй эсэхийг assert-ээр шалгахад л хангалттай.
Энэ санааг шударга хүрээнд нь байрлуулъя. Зорилго нь гаж нөлөөг устгах биш, тусгаарлах. Программ бүр файл унших, хэвлэх, хэрэглэгчтэй харилцах ёстой. Сайн бүтэцтэй программд тэр бүх ажил цөөн, нимгэн функцуудад, ихэвчлэн программын ирмэг дээр, жишээ нь main.py-д, төвлөрдөг. Харин тооцоолол, шийдвэр гаргалт, текст бүтээх ажил цэвэр функцуудад байдаг. Програм хангамжийн салбарт энэ санааг янз бүрийн нэрээр, жишээ нь "цэвэр цөм, нимгэн бүрхүүл" гэж нэрлэдэг ч нэр нь чухал биш, санаа нь чухал.
Зарлагын бүртгэлээ санацгаая. Түүний report.py-ийн total, count, average, total_by_category бүгд жагсаалт авч, тоо буцаадаг цэвэр функцууд. formatting.py-ийн format_amount цэвэр. Файл уншдаг газар storage.py, хэвлэдэг газар main.py. Бид энэ бүтцийг ухамсартай төлөвлөөгүй ч алхам бүрт жижиг, нэг ажилтай функцууд бичсэний үр дүнд байгалийн жамаар ийм болсон. Энэ нь дараагийн хичээлийн удирдлагатай бүтээлийг хялбар болгоно: тэр төсөлд бүрэн тест бичихэд бараг юу ч өөрчлөх шаардлагагүй.
Нөгөө талаас хэтрүүлэх хэрэггүй. Хоёр мөртэй, нэг удаа ажилладаг script-ийг хэсэг бүрт нь хуваах шаардлагагүй. Ажиллаж байгаа, тестлэхэд төвөггүй кодыг зөвхөн энэ санааны төлөө дахин бичих шаардлагагүй. Энэ хичээлийн гол сургамж ганцхан: хэрвээ функцийг тестлэх гэж оролдоход төвөгтэй байвал, файл, хэвлэлт, цаг зэрэг гадаад ертөнцийг бэлдэх ажил тестийн ихэнх хэсгийг эзэлж байвал, тооцооллыг тусад нь гаргаж болох эсэхийг асуу. Тестийн төвөгшөөл бол кодын тухай санал.
Хүсвэл өмнөх сургалтын аль нэг төслөөсөө, жишээ нь цаг агаарын программаас, input()-ээр хэрэглэгчээс асууж, тооцоолоод, print()-ээр хариу хэвлэдэг функц олоорой. Түүнийг энэ хичээлийн аргаар хоёр хэсэгт хувааж үзээрэй: input болон print-ийг нимгэн функцэд үлдээж, тооцооллыг утга авч, утга буцаадаг цэвэр функц болгох. Дараа нь цэвэр функцэд хоёр гурван тест бичээрэй. Хуваахаас өмнө ба дараа тест бичих нь хэр ялгаатай байсныг өөрөө мэдрээрэй.
"Цэвэр функц гэдэг нь print огт байхгүй гэсэн үг." Үгүй. Программд print байх ёстой, гагцхүү тооцооллын функцуудын дотор биш. print-ийг программын ирмэг дээрх цөөн функцэд төвлөрүүлж, бусад функцууд утгаа буцааж, тэр утгыг хэвлэдэг функцэд дамжуулдаг. Хэвлэх зүйл нь хэвээр, хэвлэдэг газар нь л цэгцтэй болно.
“capsys ба tmp_path ашиглах нь буруу.” Үгүй. Эдгээр нь pytest-ийн сайн хэрэгслүүд бөгөөд гаж нөлөөг өөрийг нь шалгах шаардлагатай үед, өмнөх хичээлийн хадгалах ба ачаалах функц шиг, яг тохиромжтой. Энэ хичээлийн санаа нь тэдгээрийг цэвэр тооцооллыг шалгахын тулд ашиглах шаардлагагүй болгох явдал. Гаж нөлөөг тестлэх цөөн тест, тооцооллыг тестлэх олон энгийн тест.
Тестгүйгээр refactor хийх. Энэ хичээлд бид print_receipt-ийг хуваахаас өмнө түүний тестийг бичсэн, тэгээд хуваасны дараа тэр тест давсаар байгааг шалгасан. Энэ дараалал санамсаргүй биш. Кодын бүтцийг өөрчлөхийн өмнө зан төлөвийг нь баталгаажуулдаг тест байх нь өөрчлөлтийн үеэр юу ч эвдрээгүйг мэдэх цорын ганц найдвартай арга. Тестгүй refactor бол Бүлэг 1-д ярьсан "эвдэхээс айх" нөхцөл рүү буцах хэрэг.
Бүх кодыг нэг дор өөрчлөх. Энэ санааг сурсны дараа бүх төслөө дахин бичих хүсэл төрж магадгүй. Гэхдээ том өөрчлөлт том эрсдэл. Хамгийн сайн арга бол тестлэхэд төвөгтэй функцтай тулгарах үед, тэр нэг функцэд энэ санааг хэрэглэх явдал. Жижиг, алхам алхмын сайжруулалт, тус бүрийн өмнө ба дараа тест ажиллуулах.
Цэвэр функц ижил оролтод үргэлж ижил үр дүн буцаадаг бөгөөд үр дүнгээ буцаахаас өөр юу ч хийдэггүй. Гаж нөлөө бол файл унших бичих, хэвлэх, input(), одоогийн цаг, санамсаргүй тоо, өгсөн утгыг өөрчлөх зэрэг функцийн гадаад ертөнцөд хийдэг аливаа зүйл. Гаж нөлөө зайлшгүй хэрэгтэй ч түүнтэй холилдсон тооцооллыг тестлэхэд tmp_path, capsys зэргээр гадаад ертөнцийг бэлдэж, барих шаардлагатай болдог. Практик санаа ганцхан: тооцооллыг гаж нөлөөнөөс салга. Тооцооллыг утга авч, утга буцаадаг цэвэр функцэд гаргаж, гаж нөлөөг ирмэг дээрх нимгэн функцэд үлдээ. Нуугдмал оролтыг, жишээ нь date.today()-г, параметр болгож гаргана, өгсөн жагсаалтыг өөрчлөхийн оронд шинэ жагсаалт буцаана. Кодын бүтцийг зан төлөвийг нь өөрчлөлгүйгээр сайжруулахыг refactoring гэдэг бөгөөд түүний өмнө тест байх ёстой. Энэ санааг тестлэхэд төвөгтэй функцтай тулгарах үед хэрэглэнэ, бүх зүйлд хэтрүүлэхгүй: тестийн төвөгшөөл бол кодын тухай санал.
Дараагийн хичээл бол Түвшин 3-ын удирдлагатай бүтээл. Бүлэг 7-9-д сурсан бүх зүйлийг зарлагын бүртгэлд хэрэглэнэ: pytest суулгах, tests фолдер ба pytest.ini, fixture, parametrize, raises, tmp_path. Энэ бүх ажлыг Түвшин 1, 2-ын дадлаар хийнэ: issue, салаа, жижиг commit-ууд, pull request, merge.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.