Property-based testlash: Hypothesis va fast-check amalda
Chuqur3 daqInnovialabs tahririyati
Qisqacha
Bir nechta misol o‘rniga istalgan kirish uchun to‘g‘ri bo‘lgan xossani tasvirlaysiz, Hypothesis yoki fast-check esa yuzlab holatlar yaratib, topilgan xatoni minimal misolgacha qisqartiradi.
Misollar o‘rniga xossalar
Oddiy test misolni tekshiradi: «2 va 3 uchun yig‘indi 5 ga teng». Property-based test istalgan kirish ma’lumoti uchun bajarilishi kerak bo‘lgan tasdiqni tekshiradi: «saralash natijasi tartiblangan va o‘sha elementlarni o‘z ichiga oladi».
Kutubxona o‘zi ko‘plab kirishlar yaratadi — bo‘sh ro‘yxatlar, manfiy sonlar, Unicode, juda uzun satrlar — va xossa buziladigan holatni qidiradi. Bu oddiy testlarni yaxshi to‘ldiradi: misollar ma’lum xatti-harakatni qayd etadi, xossalar esa siz o‘ylamagan narsani qidiradi.
- Python — Hypothesis.
- JavaScript va TypeScript — fast-check.
Generatorlar va shrinking
Generator (Hypothesis’da — strategy, fast-check’da — arbitrary) qanday ma’lumot yaratishni tasvirlaydi: st.integers(), st.lists(st.text()), fc.array(fc.integer()), fc.record({...}). Generatorlar birlashtiriladi, shuning uchun buyurtma yoki foydalanuvchi obyektini yig‘ish mumkin.
Shrinking — asosiy qulaylik. Ulkan tasodifiy kirishda xato topilganda kutubxona uni qadamma-qadam soddalashtiradi, toki xato hamon takrorlanadigan minimal misolga yetguncha. 80 ta tasodifiy sonli ro‘yxat o‘rniga siz [0, -1] ni ko‘rasiz.
Qaysi xossalarni tekshirish kerak
| Xossa | Misol |
|---|---|
| Round-trip | decode(encode(x)) == x, parse(format(x)) == x |
| Invariant | saralashdan keyin uzunlik o‘zgarmaydi, elementlar tartiblangan |
| Idempotentlik | normalize(normalize(x)) == normalize(x) |
| Orakul | tez funksiya oddiy sekin versiya bilan mos keladi |
| Yiqilmaydi | funksiya har qanday to‘g‘ri kirishda istisno tashlamaydi |
Round-trip va «yiqilmaydi» xossalaridan boshlang — ularni ifodalash eng oson.
Hypothesis: teng bo‘linmaydigan pul
Funksiya tiyindagi summani n qismga bo‘ladi:
from hypothesis import given, strategies as st
def split_amount(total, n):
part = round(total / n)
return [part] * n
@given(st.integers(min_value=0, max_value=10**9),
st.integers(min_value=1, max_value=100))
def test_parts_sum_to_total(total, n):
assert sum(split_amount(total, n)) == total
(100, 4) kabi misollar o‘tadi, lekin Hypothesis tezda buzilishni topadi va uni total=1, n=2 ga o‘xshash holatgacha qisqartiradi: qismlar yig‘indisi dastlabki summaga teng emas. Tuzatish — qoldiqni taqsimlash:
def split_amount(total, n):
q, r = divmod(total, n)
return [q + 1] * r + [q] * (n - r)
fast-check: o‘zimiz yozgan serializatsiya
Oddiy funksiya obyektni key=value&... satriga aylantiradi:
import fc from "fast-check";
const stringify = (o: Record<string, string>) =>
Object.entries(o).map(([k, v]) => `${k}=${v}`).join("&");
fc.assert(
fc.property(fc.dictionary(fc.string(), fc.string()), (obj) => {
expect(parse(stringify(obj))).toEqual(obj);
})
);
fast-check & yoki = belgisi bor kalit yoki qiymatni topadi va round-trip buziladi. { name: "Ali" } bilan qo‘lda yozilgan test bu xatoni hech qachon ko‘rsatmasdi. Tuzatish — qiymatlarni ekranlash, masalan encodeURIComponent orqali.
Qanday og‘riqsiz joriy qilish mumkin
- Sof funksiyalarni tanlang: parserlar, formatterlar, hisob-kitoblar, ma’lumotlarni o‘zgartirish.
- Bitta xossani yozing va generatorni real domen bilan cheklang (diapazonlar, uzunliklar).
- Topilgan har bir kontrmisolni oddiy misol-test sifatida saqlang — bu regressiya testi.
- CI’da yiqilganda seed’ni qayd eting: ikkala kutubxona ham ishga tushirishni qanday takrorlashni aytadi.
- Xossa ichida implementatsiyani takrorlamang, aks holda test o‘sha xatoni qaytaradi.
Keng tarqalgan xatolar
- Juda keng generatorlar, natijada holatlarning ko‘pchiligi filtrlar tomonidan tashlab yuboriladi.
- Hech narsani tekshirmaydigan, oddiygina har doim to‘g‘ri xossa.
- Sekin testlar: har bir iteratsiya ichida ma’lumotlar bazasi bilan og‘ir ishlar.