16-bo‘lim
Eski kodni testlash
Testi yo'q kodga qanday yondashish, xarakteristik testlar, oltin nusxa usuli, tikish joylari va xavfsiz refaktoring tartibi.
Ushbu bo‘lim mundarijasi
Michael Feathers ning 2004-yilda chiqqan *"Working Effectively with Legacy Code"* kitobida mashhur ta'rif bor: eski kod - testi yo'q kod. Yoshi ahamiyatsiz; kecha yozilgan bo'lsa ham, testi bo'lmasa - eski.
Bunday kod bilan ishlash tuzoqqa o'xshaydi: uni xavfsiz o'zgartirish uchun test kerak, lekin test yozish uchun uni o'zgartirish kerak.
Boshlang'ich holat #
Mana shunday funksiya - bir necha yil davomida qoidalar qo'shilib borgan:
def hisobot(buyurtmalar):
natija = ""
jami = 0
for b in buyurtmalar:
s = b["soni"] * b["narx"]
if b["soni"] > 10:
s = s * 0.95
if b["turkum"] == "kitob":
s = s * 0.9
jami += s
natija += b["nom"] + ": " + str(int(s)) + "\n"
if jami > 1_000_000:
natija += "CHEGIRMA: 50000\n"
jami -= 50_000
natija += "JAMI: " + str(int(jami))
return natija
Uni refaktoring qilish kerak, lekin hech qanday test yo'q. Uni o'zgartirsak, nimani buzganimizni bilmaymiz.
Xarakteristik test #
Birinchi qadam - kodning hozir nima qilayotganini yozib olish. E'tibor bering: to'g'ri qilayotganini emas, nima qilayotganini.
Shuning uchun bunday testlar xarakteristik test (characterisation test) deb ataladi: ular kodning xarakterini tasvirlaydi.
Avval chiqishni ko'ramiz:
from eski import hisobot
BUYURTMA = [
{"nom": "Daftar", "turkum": "kantselyariya", "soni": 12, "narx": 8000},
{"nom": "O'tkan kunlar", "turkum": "kitob", "soni": 3, "narx": 65000},
{"nom": "Ruchka", "turkum": "kantselyariya", "soni": 5, "narx": 4000},
]
print(hisobot(BUYURTMA))
python3 korish.py
Daftar: 91200
O'tkan kunlar: 175500
Ruchka: 20000
JAMI: 286700
Endi shu natijani aynan shundayligicha testga ko'chiramiz:
from eski import hisobot
BUYURTMA = [
{"nom": "Daftar", "turkum": "kantselyariya", "soni": 12, "narx": 8000},
{"nom": "O'tkan kunlar", "turkum": "kitob", "soni": 3, "narx": 65000},
{"nom": "Ruchka", "turkum": "kantselyariya", "soni": 5, "narx": 4000},
]
def test_hozirgi_xatti_harakat():
natija = hisobot(BUYURTMA)
assert natija == (
"Daftar: 91200\n"
"O'tkan kunlar: 175500\n"
"Ruchka: 20000\n"
"JAMI: 286700"
)
def test_bosh_royxat():
assert hisobot([]) == "JAMI: 0"
def test_katta_summada_qoshimcha_chegirma():
katta = [{"nom": "Noutbuk", "turkum": "texnika", "soni": 1, "narx": 12_000_000}]
natija = hisobot(katta)
assert "CHEGIRMA: 50000" in natija
assert natija.endswith("JAMI: 11950000")
pytest -q -p no:randomly test_xarakteristik.py
... [100%]
3 passed in 0.01s
Yuqoridagi test 91200 sonini kutadi. Bu son to'g'rimi? Bilmaymiz.
Balki 12 dona daftarga 5% chegirma berilmasligi kerakdir. Balki chegirmalar boshqa tartibda qo'llanishi kerakdir.
Xarakteristik testning vazifasi boshqa: u refaktoring paytida xatti-harakat o'zgarmaganini kafolatlaydi. Agar keyinchalik qoida noto'g'ri ekani aniqlansa, siz uni ongli ravishda o'zgartirasiz va testni ham o'zgartirasiz.
Farq muhim: tasodifan buzish va ataylab o'zgartirish - ikki xil narsa.
Tikish joyi #
Eski kodni testlashda asosiy qiyinchilik - u tashqi dunyoga qattiq bog'langan bo'ladi. Tikish joyi (seam, Feathers atamasi) - kodni o'zgartirmasdan xatti-harakatini almashtirish mumkin bo'lgan nuqta.
Kodga sana qo'shilgan deylik:
from datetime import date
def sarlavha():
return f"Hisobot sanasi: {date.today().isoformat()}"
def hisobot(buyurtmalar, sarlavha_beruvchi=sarlavha):
natija = sarlavha_beruvchi() + "\n"
jami = 0
for b in buyurtmalar:
s = b["soni"] * b["narx"]
if b["soni"] > 10:
s = s * 0.95
if b["turkum"] == "kitob":
s = s * 0.9
jami += s
natija += b["nom"] + ": " + str(int(s)) + "\n"
if jami > 1_000_000:
natija += "CHEGIRMA: 50000\n"
jami -= 50_000
natija += "JAMI: " + str(int(jami))
return natija
sarlavha_beruvchi - tikish joyi. Mavjud chaqiruvlarning birortasi
o'zgarmadi (argument sukut qiymatga ega), lekin test endi sanani
boshqara oladi:
from eski import hisobot
def test_sarlavha_almashtiriladi():
natija = hisobot([], sarlavha_beruvchi=lambda: "Hisobot sanasi: 2026-01-01")
assert natija == "Hisobot sanasi: 2026-01-01\nJAMI: 0"
pytest -q -p no:randomly test_tikish.py
. [100%]
1 passed in 0.00s
| Tikish turi | Qanday qo'yiladi |
|---|---|
| Argument | Sukut qiymatli yangi parametr qo'shish |
| Obyekt | Bog'liqlikni konstruktorga ko'chirish |
| Modul | monkeypatch bilan nomni almashtirish (8-bo'lim) |
| Meros | Metodni ost sinfda qayta aniqlash |
| Sozlama | Muhit o'zgaruvchisi yoki konfiguratsiya fayli |
Bu o'zgarish xarakteristik testni ataylab buzadi - chiqishga yangi qator qo'shildi:
pytest -q -p no:randomly --tb=no test_xarakteristik.py
FF. [100%]
======================= short test summary info ========================
FAILED test_xarakteristik.py::test_hozirgi_xatti_harakat - AssertionE...
FAILED test_xarakteristik.py::test_bosh_royxat - AssertionError: asse...
2 failed, 1 passed in 0.01s
Aynan shu kerak edi. Test bizga "chiqish o'zgardi" deb aytdi, biz esa buni bilib turibmiz. Endi testni yangi holatga moslaymiz - sarlavhani har test o'zi belgilaydi:
from eski import hisobot
BUYURTMA = [
{"nom": "Daftar", "turkum": "kantselyariya", "soni": 12, "narx": 8000},
{"nom": "O'tkan kunlar", "turkum": "kitob", "soni": 3, "narx": 65000},
{"nom": "Ruchka", "turkum": "kantselyariya", "soni": 5, "narx": 4000},
]
SARLAVHA = lambda: "Hisobot sanasi: 2026-01-01"
def test_hozirgi_xatti_harakat():
natija = hisobot(BUYURTMA, sarlavha_beruvchi=SARLAVHA)
assert natija == (
"Hisobot sanasi: 2026-01-01\n"
"Daftar: 91200\n"
"O'tkan kunlar: 175500\n"
"Ruchka: 20000\n"
"JAMI: 286700"
)
def test_bosh_royxat():
assert hisobot([], sarlavha_beruvchi=SARLAVHA) == (
"Hisobot sanasi: 2026-01-01\nJAMI: 0"
)
def test_katta_summada_qoshimcha_chegirma():
katta = [{"nom": "Noutbuk", "turkum": "texnika", "soni": 1, "narx": 12_000_000}]
natija = hisobot(katta, sarlavha_beruvchi=SARLAVHA)
assert "CHEGIRMA: 50000" in natija
assert natija.endswith("JAMI: 11950000")
pytest -q -p no:randomly test_xarakteristik.py
... [100%]
3 passed in 0.01s
Oltin nusxa #
Chiqish uzun bo'lsa, uni testga ko'chirib yozish noqulay. Shunda oltin nusxa (golden master) usuli ishlatiladi: natija faylga saqlanadi va har ishga tushirishda solishtiriladi.
from pathlib import Path
from eski import hisobot
OLTIN = Path(__file__).parent / "oltin"
BUYURTMA = [
{"nom": "Daftar", "turkum": "kantselyariya", "soni": 12, "narx": 8000},
{"nom": "O'tkan kunlar", "turkum": "kitob", "soni": 3, "narx": 65000},
{"nom": "Ruchka", "turkum": "kantselyariya", "soni": 5, "narx": 4000},
{"nom": "Mehrobdan chayon", "turkum": "kitob", "soni": 20, "narx": 55000},
]
def tekshir(nom, matn):
"""Natijani saqlangan nusxa bilan solishtiradi."""
OLTIN.mkdir(exist_ok=True)
fayl = OLTIN / f"{nom}.txt"
if not fayl.exists():
fayl.write_text(matn, encoding="utf-8")
raise AssertionError(f"oltin nusxa yaratildi: {fayl.name} - uni ko'rib chiqing")
assert matn == fayl.read_text(encoding="utf-8")
def test_hisobot_oltin_nusxaga_mos():
sarlavha = lambda: "Hisobot sanasi: 2026-01-01"
tekshir("hisobot", hisobot(BUYURTMA, sarlavha_beruvchi=sarlavha))
Birinchi ishga tushirishda nusxa hali yo'q - test ataylab yiqiladi:
pytest -q -p no:randomly --tb=line test_oltin.py
F [100%]
=============================== FAILURES ===============================
E AssertionError: oltin nusxa yaratildi: hisobot.txt - uni ko'rib chiqing
/home/dasturchi/loyiha/test_oltin.py:21: AssertionError: oltin nusxa yaratildi: hisobot.txt - uni ko'rib chiqing
======================= short test summary info ========================
FAILED test_oltin.py::test_hisobot_oltin_nusxaga_mos - AssertionError...
1 failed in 0.01s
Yaratilgan faylni ko'rib chiqamiz:
cat oltin/hisobot.txt
Hisobot sanasi: 2026-01-01
Daftar: 91200
O'tkan kunlar: 175500
Ruchka: 20000
Mehrobdan chayon: 940500
CHEGIRMA: 50000
JAMI: 1177200
Natija to'g'ri ko'rinsa, fayl versiya nazoratiga qo'shiladi. Endi test o'tadi:
pytest -q -p no:randomly test_oltin.py
. [100%]
1 passed in 0.01s
Ikkita xavf bor va ikkalasi ham amalda tez-tez uchraydi:
Birinchi xavf - ko'rmasdan yangilash. Test yiqilganda "nusxani yangilash" tugmasi bosiladi va xato "to'g'ri javob" bo'lib qoladi. Yangilashdan oldin farqni albatta o'qing.
Ikkinchi xavf - haddan tashqari keng nusxa. Butun HTML sahifani saqlasangiz, har CSS sinfi o'zgarganda test yiqiladi. Nusxa faqat mazmunni qamrasin.
Qoida: oltin nusxa - refaktoring uchun vaqtinchalik tayanch. Kod kichik qismlarga ajratilgandan keyin uning o'rnini aniq testlar egallashi kerak.
Xavfsiz refaktoring #
Endi tayanch bor - refaktoring qilamiz. Katta funksiyani kichiklarga ajratamiz:
from datetime import date
KOP_SONI = 10
KOP_CHEGIRMA = 0.95
KITOB_CHEGIRMA = 0.9
KATTA_SUMMA = 1_000_000
KATTA_CHEGIRMA = 50_000
def sarlavha():
return f"Hisobot sanasi: {date.today().isoformat()}"
def qator_summasi(buyurtma):
summa = buyurtma["soni"] * buyurtma["narx"]
if buyurtma["soni"] > KOP_SONI:
summa *= KOP_CHEGIRMA
if buyurtma["turkum"] == "kitob":
summa *= KITOB_CHEGIRMA
return summa
def hisobot(buyurtmalar, sarlavha_beruvchi=sarlavha):
qatorlar = [sarlavha_beruvchi()]
jami = 0.0
for buyurtma in buyurtmalar:
summa = qator_summasi(buyurtma)
jami += summa
qatorlar.append(f"{buyurtma['nom']}: {int(summa)}")
if jami > KATTA_SUMMA:
qatorlar.append(f"CHEGIRMA: {KATTA_CHEGIRMA}")
jami -= KATTA_CHEGIRMA
qatorlar.append(f"JAMI: {int(jami)}")
return "\n".join(qatorlar)
pytest -q -p no:randomly test_tikish.py test_oltin.py
.. [100%]
2 passed in 0.01s
Oltin nusxa yashil - demak chiqish bir belgi ham o'zgarmadi. Endi
ajratilgan qator_summasi ga haqiqiy testlar yozish mumkin:
import pytest
from eski import qator_summasi
@pytest.mark.parametrize("soni, turkum, narx, kutilgan", [
(1, "texnika", 100_000, 100_000), # chegirmasiz
(10, "texnika", 10_000, 100_000), # chegara: hali ko'p emas
(11, "texnika", 10_000, 104_500), # 5% chegirma
(1, "kitob", 100_000, 90_000), # kitob chegirmasi
(11, "kitob", 10_000, 94_050), # ikkala chegirma birga
])
def test_qator_summasi(soni, turkum, narx, kutilgan):
buyurtma = {"nom": "X", "turkum": turkum, "soni": soni, "narx": narx}
assert qator_summasi(buyurtma) == pytest.approx(kutilgan)
pytest -q -p no:randomly test_qator.py
..... [100%]
5 passed in 0.01s
Mana endi bizda chegara qiymatlari sinalgan, o'qiladigan testlar bor. Oltin nusxa o'z vazifasini bajardi va uni olib tashlash mumkin.
Eski kod bilan ishlashda vasvasa doim bitta: "avval tozalab olay, keyin test yozaman". Bu teskari tartib va u eng ko'p nosozlik keltiradi.
To'g'ri ketma-ketlik:
- Xarakteristik test yoki oltin nusxa bilan hozirgi holatni muhrlang.
- Tikish joyi qo'yib, tashqi bog'liqlikni ajrating.
- Kichik qadamlar bilan refaktoring qiling, har qadamdan keyin testni ishga tushiring.
- Kod bo'lingandan keyin aniq testlar yozing.
- Oltin nusxani olib tashlang.
Har qadamda testlar yashil bo'lishi shart. Yashil bo'lmasa - oxirgi qadamni orqaga qaytaring, kattaroq qadam qo'ymang.
eski.pyni yozib, uning hozirgi chiqishini ko'ring.- Xarakteristik test yozib, natijani aynan ko'chiring.
- Kodda bitta sonni o'zgartiring - test yiqiladimi?
- Oltin nusxa testini yozing va birinchi ishga tushirishda nima bo'lishini kuzating.
- Saqlangan faylni o'qib chiqing - natija to'g'ri ko'rinadimi?
sarlavha_beruvchitikish joyini qo'ying va sanani testda belgilang.- Refaktoringni bajarib, oltin nusxa yashil qolganini tekshiring.
qator_summasiga chegara qiymatlari bilan testlar yozing.- 10 va 11 dona uchun natijalar nima uchun farq qilishini tushuntiring.
- O'z loyihangizdagi eng katta funksiyani toping va unga xarakteristik test yozing.
Xulosa #
- Eski kod - testi yo'q kod (Michael Feathers, 2004); yoshi ahamiyatsiz.
- Tuzoq: xavfsiz o'zgartirish uchun test kerak, test yozish uchun o'zgartirish kerak.
- Xarakteristik test kodning hozir nima qilayotganini yozib oladi, to'g'ri qilayotganini emas.
- U to'g'rilikni tasdiqlamaydi - u refaktoring paytida xatti-harakat o'zgarmaganini kafolatlaydi.
- Tasodifan buzish va ataylab o'zgartirish - ikki xil narsa; xarakteristik test shu farqni saqlaydi.
- Oltin nusxa usulida natija faylga saqlanadi va har ishga tushirishda solishtiriladi.
- Birinchi ishga tushirishda nusxa yaratiladi va test ataylab yiqiladi - siz uni ko'rib chiqishingiz uchun.
- Ko'rilmagan nusxani saqlash - xatoni "to'g'ri javob" sifatida muhrlab qo'yish demak.
- Nusxa faqat mazmunni qamrasin: butun HTML ni saqlash har CSS o'zgarishida testni yiqitadi.
- Tikish joyi - kodni o'zgartirmasdan xatti-harakatini almashtirish mumkin bo'lgan nuqta.
- Eng arzon tikish - sukut qiymatli yangi argument: mavjud chaqiruvlar o'zgarmaydi.
- Refaktoring tartibi: muhrlang, tikish qo'ying, kichik qadamlar, aniq testlar, nusxani olib tashlang.
- "Avval tozalab olay, keyin test yozaman" - eng ko'p nosozlik keltiradigan teskari tartib.
- Har qadamda testlar yashil bo'lsin; yashil bo'lmasa, oxirgi qadamni orqaga qaytaring.
Keyingi bo'limda testlarni kompyuterning o'ziga o'ylab toptiramiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.