Mundarija (28)
- Bu darsda
- 1. Nega bu kerak?
- 2. Buzuvchi o'zgarish nima
- 3. Uch yondashuv
- 4. OpenAPI surati
- 4.1 Swagger hujjatidan ixcham shaklga
- 4.2 Solishtirish qoidalari
- 4.3 Tajriba: buzuvchi o'zgarish testda
- 4.4 Ataylab o'zgartirish
- 5. Surat ko'rmagan narsa: javob
- 5.1 Topilma
- 5.2 Uni qanday yopish mumkin
- 6. Iste'molchi shartnomasi
- 6.1 Frontend nimaga tayanadi
- 6.2 Nega looseObject
- 6.3 Problem javoblari va Content-Type
- 7. Pact va Schemathesis
- 7.1 Pact
- 7.2 Schemathesis
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): buzadimi?
- 2-mashq (o'rta): parametrlarni solishtirish
- 3-mashq (qiyin): iste'molchi shartnomasi paketsiz
- 10. Loyiha qadami: test/shartnoma.test.ts
- 11. Real ishda
- Xulosa
- Manbalar
Contract va API sxema testlari: OpenAPI surati, iste'molchi shartnomasi, Pact va Schemathesis
Qisqacha: Shartnoma testi (contract test) — API va uni ishlatadigan dastur orasidagi kelishuv buzilmaganini avtomatik tekshirish. Buzuvchi o'zgarish (breaking change) — eski mijozni ishlamay qoldiradigan o'zgarish: majburiy maydon qo'shish, maydonni olib tashlash, turini o'zgartirish. Kanon ikki qatlam qo'ydi: OpenAPI surati (API sxemasi faylga yoziladi, har test uni joriy sxema bilan solishtiradi) va iste'molchi shartnomasi (frontend tayanadigan javob maydonlari Zod bilan). Tajribada surat so'rov tomonidagi buzilishni ushladi, lekin javob turidagi o'zgarishni ko'rmadi — Swagger hujjatimizda javob sxemalari yo'q. Uni faqat iste'molchi shartnomasi ushladi.
Bu darsda
- Buzuvchi va buzmaydigan o'zgarishlarni farqlaysiz.
- OpenAPI hujjatini ixcham suratga aylantirib, ikki versiyani solishtira olasiz.
- Iste'molchi shartnomasini Zod
looseObjectbilan yozasiz va negastrictObjectemasligini tushuntirasiz. - Pact va Schemathesis nima qilishini va qachon kerakligini bilasiz.
vazifalar-apigatest/shartnoma.test.tsvadocs/openapi-snapshot.jsonni qo'shasiz.
Oldin bilishingiz kerak: Backend test strategiyasi va contract testlar, API versiyalash va deprecation, Swagger: @nestjs/swagger, Asinxron komponentlarni testlash.
1. Nega bu kerak?
Backend test strategiyasi darsida vazifalar-api tarixidagi bir voqeani ko'rgansiz. Xato javobi { "xato": "…" } dan Problem Details'ga o'tdi. API testlari yashil edi — ular yangi formatni kutardi. Frontend esa eski xato maydonini o'qirdi va buni faqat brauzerdagi qo'lda sinov ko'rsatdi.
Muammo testlarning ko'pligida emas. Har ikki tomon o'z testlariga ega: backend "men to'g'ri javob beryapman" deydi, frontend "men soxta javob bilan to'g'ri ishlayapman" deydi (MSW darsidagi soxta API). Ikkalasi ham haq. Lekin ular bir xil javob haqida gapirayaptimi — buni hech kim tekshirmaydi.
O'xshatish: elektr vilkasi va rozetka. Zavod vilkani o'z standartiga ko'ra sinaydi, quruvchi rozetkani o'z standartiga ko'ra o'rnatadi. Ikkala sinov o'tadi — lekin vilka rozetkaga sig'maydi. Shartnoma testi — ikkalasini bitta o'lchov shabloni bilan tekshirish.
2. Buzuvchi o'zgarish nima
API versiyalash va deprecation darsida buzuvchi o'zgarishni (breaking change) versiya bilan boshqarishni o'rgandik. Endi uni avtomatik aniqlash kerak. Avval qaysi o'zgarish buzadi, qaysi biri yo'q — jadvalda:
| O'zgarish | So'rovda | Javobda |
|---|---|---|
| Ixtiyoriy maydon qo'shish | buzmaydi | buzmaydi (mijoz bilmagan maydonni e'tiborsiz qoldirsa) |
| Majburiy maydon qo'shish | buzadi — eski mijoz yubormaydi | buzmaydi |
| Maydonni olib tashlash | buzadi (sxema qat'iy bo'lsa — 422) | buzadi — mijoz o'qiydi |
| Maydon turini o'zgartirish | buzadi | buzadi |
| Yangi endpoint | buzmaydi | — |
| Endpoint yoki parametrni olib tashlash | buzadi | — |
Jadvalda bir nosimmetriya bor. So'rovga maydon qo'shish xavfli (agar majburiy bo'lsa), javobga qo'shish — xavfsiz. Javobdan olib tashlash xavfli, so'rovdan olib tashlash — ham xavfli, chunki bizning sxemalarimiz qat'iy: Zod 4 serverda darsidagi z.strictObject noma'lum maydonni 422 bilan rad etadi. Eski frontend olib tashlangan maydonni hamon yuborsa — xato oladi.
Javob tomonidagi "ixtiyoriy maydon qo'shish buzmaydi" faqat bir shartda to'g'ri: mijoz bag'rikeng o'quvchi (tolerant reader) bo'lsa — o'ziga keraklisini o'qib, qolganini e'tiborsiz qoldirsa. Bu shart iste'molchi shartnomasining markaziy g'oyasi, unga qaytamiz.
Tekshirib ko'ring:
GET /v1/vazifalarjavobidagibajarildimaydonibooleandan"ha" | "yo'q"satriga o'zgardi. Frontendif (v.bajarildi)deb yozilgan. Nima bo'ladi?
Javob
Hamma vazifa "bajarilgan" bo'lib ko'rinadi: "yo'q" — bo'sh bo'lmagan satr, JavaScript'da u rost (truthy). Xato ham, ogohlantirish ham chiqmaydi — eng yomon turdagi buzilish. Tur o'zgarishi shuning uchun buzuvchi: u har doim ham yiqilish bilan ko'rinmaydi.
3. Uch yondashuv
Shartnomani tekshirishning uchta mashhur yo'li bor:
| Yondashuv | Kim yozadi | Vositalar | Kanonda |
|---|---|---|---|
| Sxema surati (snapshot) | provayder | o'z buzilishlar(), oasdiff |
bor |
| Iste'molchi boshqaradigan (consumer-driven) | iste'molchi | Pact | g'oyasi — Zod bilan |
| Xususiyatga asoslangan (property-based) | hech kim — sxemadan generatsiya | Schemathesis | sinalmagan |
Sxema surati provayder tomonida: API sxemasini faylga yozib qo'yamiz va har o'zgarishda yangi sxema bilan solishtiramiz. U "nima o'zgardi" ni ko'radi, lekin "kimga zarar" ni bilmaydi.
Iste'molchi shartnomasi teskari tomondan: iste'molchi "men aynan shu maydonlarga tayanaman" deb yozadi. Provayder istagan narsani o'zgartirishi mumkin — faqat shu ro'yxat buzilmasin.
Xususiyatga asoslangan test sxemadan minglab so'rov yasaydi (chegaraviy qiymatlar, bo'sh satr, juda katta son) va javob sxemaga mosligini va server 500 qaytarmasligini tekshiradi. U shartnomadan ko'ra ko'proq "sxema rost gapiryaptimi" ni tekshiradi.
Kanon birinchi ikkitasini kutubxonasiz qurdi. Keling, ularni ko'ramiz.
4. OpenAPI surati
4.1 Swagger hujjatidan ixcham shaklga
Swagger darsida @nestjs/swagger /docs-json manzilida OpenAPI hujjatini berishini ko'rgansiz. Unda har operatsiya uchun parametrlar, so'rov tanasi sxemasi va javoblar bor. POST /v1/vazifalar uchun bo'lagi (biz ishga tushirdik, /docs-json dan):
{
"operationId": "yarat",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"matn": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"bajarildi": {
"type": "boolean"
}
},
"required": ["matn"],
"additionalProperties": false
}
}
}
},
"responses": {
"201": { "description": "" }
},
"tags": ["Vazifalar"]
}Ikki narsaga e'tibor bering. Birinchisi: additionalProperties: false — Zod'dagi strictObject ning OpenAPI'dagi ifodasi. Ikkinchisi: "201": { "description": "" } — javob haqida hech narsa yo'q, sxemasi ham. Bunga qaytamiz.
Butun hujjatni solishtirish noqulay: unda tavsiflar, teglar, misollar bor — ular o'zgarsa, mijoz buzilmaydi. Kanon hujjatni ixcham shaklga keltiradi: har operatsiya uchun faqat shartnomaga tegishli narsa. docs/openapi-snapshot.json dan ikki yozuv:
{
"GET /v1/vazifalar/{id}": {
"parametrlar": ["path:id!"],
"tana": null,
"javoblar": ["200"]
},
"POST /v1/vazifalar": {
"parametrlar": [],
"tana": {
"matn": { "tur": "\"string\"", "majburiy": true },
"bajarildi": { "tur": "\"boolean\"", "majburiy": false }
},
"javoblar": ["201"]
}
}path:id! — undov belgisi "majburiy" degani. Butun suratda 13 ta operatsiya.
4.2 Solishtirish qoidalari
Solishtirgich buzilishlar(eski, yangi) jadvaldagi qoidalarni kodga aylantiradi. Uning soddalashtirilgan nusxasi brauzerda ishlaydi — faqat tana maydonlari va operatsiyalar bilan:
function buzilishlar(eski, yangi) {
const b = [];
for (const [op, e] of Object.entries(eski)) {
const y = yangi[op];
if (!y) { b.push(`${op}: olib tashlandi`); continue; }
for (const [k, t] of Object.entries(y.tana ?? {})) {
const et = e.tana?.[k];
if (t.majburiy && !et?.majburiy) {
b.push(`${op}: tana.${k} endi majburiy`);
}
if (et && et.tur !== t.tur) {
b.push(`${op}: tana.${k} turi ${et.tur} → ${t.tur}`);
}
}
for (const k of Object.keys(e.tana ?? {})) {
if (!y.tana?.[k]) b.push(`${op}: tana.${k} olib tashlandi`);
}
}
return b;
}
const m = (tur, majburiy) => ({ tur, majburiy });
const eski = {
"POST /v1/vazifalar": {
tana: { matn: m("string", true), bajarildi: m("boolean", false) },
},
"DELETE /v1/vazifalar/{id}": { tana: null },
};
const v1 = structuredClone(eski);
v1["POST /v1/vazifalar"].tana.muddat = m("string", false);
v1["GET /v1/statistika"] = { tana: null };
console.log("ixtiyoriy maydon + yangi endpoint:",
buzilishlar(eski, v1));
const v2 = {
"POST /v1/vazifalar": {
tana: { matn: m("string", true), muddat: m("string", true) },
},
};
console.log(buzilishlar(eski, v2));Konsolda:
ixtiyoriy maydon + yangi endpoint: []
[
'POST /v1/vazifalar: tana.muddat endi majburiy',
'POST /v1/vazifalar: tana.bajarildi olib tashlandi',
'DELETE /v1/vazifalar/{id}: olib tashlandi'
]Birinchi o'zgarishda ikki narsa qo'shildi — ikkalasi ham buzmaydi, ro'yxat bo'sh. Ikkinchisida uchta buzilish: yangi majburiy maydon, olib tashlangan maydon va o'chirilgan endpoint. Solishtirish eskidan yangiga yo'nalgan: eski mijozning har kutgan narsasi yangisida bormi?
Kanon nusxasi bulardan tashqari parametrlarni (olib tashlangan, yangi majburiy) va javob kodlarini (olib tashlangan) ham tekshiradi. To'liq kod «Loyiha qadami» bo'limida.
4.3 Tajriba: buzuvchi o'zgarish testda
Endi haqiqiy loyihada sinaymiz. Tasavvur qiling: Sardor vazifa yaratishga muddat qo'shmoqchi. U src/vazifalar/sxema.ts dagi sxemani o'zgartirdi — muddat ni majburiy qildi va "endi kerak emas" deb bajarildi ni olib tashladi:
export const YangiVazifaSxemasi = z.strictObject({
- matn: Matn, bajarildi: Bajarildi.optional(),
+ matn: Matn, muddat: z.iso.datetime(),
});(Faylda bu bitta qator — telefon uchun bo'lib ko'rsatdik.)
Chiqishda × belgili testlar sonini va har FAIL ostidagi birinchi qatorni o'qing — u sababni aytadi. Biz ishga tushirdik (loyiha nusxasida, keyin o'zgarishni qaytardik; chiqish qisqartirilgan):
× OpenAPI snapshot: breaking o'zgarish yo'q (qo'shimchalar — ruxsat) 87ms
✓ solishtirgich o'zi: parametr olib tashlash, majburiy maydon, tur o'zgarishi, operatsiya o'chirish — ushlanadi 1ms
× iste'molchi shartnomasi: ro'yxat, yaratish, olish, PATCH, 404 va 422 javoblari frontend kutgan shaklda (+ Content-Type) 75ms
FAIL |asosiy| test/shartnoma.test.ts > OpenAPI snapshot: breaking o'zgarish yo'q (qo'shimchalar — ruxsat)
AssertionError: Expected values to be strictly deep-equal:
+ actual - expected
+ [
+ 'POST /v1/vazifalar: tana.muddat endi majburiy',
+ 'POST /v1/vazifalar: tana.bajarildi olib tashlandi (strict sxemada mijoz yuborsa — 422)'
+ ]
- []
FAIL |asosiy| test/shartnoma.test.ts > iste'molchi shartnomasi: ro'yxat, yaratish, olish, PATCH, 404 va 422 javoblari frontend kutgan shaklda (+ Content-Type)
Error: expected 201 "Created", got 422 "Unprocessable Entity"Ikkala qatlam ham yiqildi. Surat aniq sababni aytdi: ikki buzilish, nomi bilan. Iste'molchi shartnomasi oqibatni ko'rsatdi: frontend odatdagidek { matn } yuborganda 201 o'rniga 422 keladi.
Sardorga to'g'ri yo'l — muddat ni ixtiyoriy qilish va bajarildi ni qoldirish. Shunda ikkala test yashil bo'ladi. Agar o'zgarish haqiqatan kerak bo'lsa — yangi versiya (API versiyalash) yoki muvofiqlashtirilgan reliz.
4.4 Ataylab o'zgartirish
Yangi ixtiyoriy maydon qo'shsangiz, test o'tadi, lekin surat eskiradi. Shuning uchun suratni yangilash usuli bor:
SHARTNOMA_YANGILA=1 npx vitest run test/shartnoma.test.ts
git diff docs/openapi-snapshot.jsonIkkinchi buyruq muhimroq. Surat fayli repoda — har o'zgarish PR'da ko'rinadi. Ko'rib chiquvchi docs/openapi-snapshot.json dagi farqqa qaraydi va so'raydi: "bu maydon nega majburiy bo'ldi?". Bu — Vitest II darsidagi snapshot test g'oyasi, faqat UI emas, API uchun.
Diqqat:
SHARTNOMA_YANGILA=1— buzilishni "yashirish" tugmasi ham bo'lishi mumkin. Yiqilgan testni o'qimasdan suratni yangilash — testni o'chirish bilan teng. Qoida: surat faqat ataylab qilingan, kelishilgan o'zgarishda yangilanadi.
5. Surat ko'rmagan narsa: javob
5.1 Topilma
«Swagger hujjatidan ixcham shaklga» bo'limidagi "201": { "description": "" } ni eslang. 30-qismda Swagger'ni ulaganimizda so'rov sxemalari Zod'dan avtomatik chiqdi, javob sxemalari esa yozilmadi. Natijada suratdagi javoblar — faqat status kodlari ro'yxati: ["200"]. Javob tanasining shakli umuman yo'q.
Bu nimani anglatadi? Ikkinchi tajriba. Endi xotira omborida yangi vazifaning tartib maydonini son o'rniga satr qilib qaytaramiz — kimdir bexosdan String(...) bilan o'radi:
- tartib: Math.max(0, ...tartiblar) + 1,
+ tartib: String(Math.max(0, ...tartiblar) + 1),(Qisqartirilgan: tartiblar o'rnida faylda mavjud vazifalarning tartib qiymatlarini yig'adigan ifoda turadi.)
Bu safar qaysi test yashil qolganiga e'tibor bering. Biz ishga tushirdik (chiqish qisqartirilgan):
✓ OpenAPI snapshot: breaking o'zgarish yo'q (qo'shimchalar — ruxsat) 115ms
✓ solishtirgich o'zi: parametr olib tashlash, majburiy maydon, tur o'zgarishi, operatsiya o'chirish — ushlanadi 2ms
× iste'molchi shartnomasi: ro'yxat, yaratish, olish, PATCH, 404 va 422 javoblari frontend kutgan shaklda (+ Content-Type) 87ms
FAIL |asosiy| test/shartnoma.test.ts > iste'molchi shartnomasi: ro'yxat, yaratish, olish, PATCH, 404 va 422 javoblari frontend kutgan shaklda (+ Content-Type)
ZodError: [
{
"expected": "number",
"code": "invalid_type",
"path": [
"tartib"
],
"message": "Noto‘g‘ri kirish: kutilgan raqam, qabul qilingan string"
}
]Surat yashil — u javob turini bilmaydi. Iste'molchi shartnomasi tutdi: tartib — son kutilgan, satr keldi. Xabar o'zbekcha, chunki loyiha Zod'ga o'zbek lokalini ulagan (Zod 4 serverda).
flowchart LR
O["O'zgarish"] --> S{"Qayerda?"}
S -->|"so'rov sxemasi"| A["Surat ushlaydi"]
S -->|"javob shakli"| B["Surat ko'rmaydi"]
B --> C["Iste'molchi shartnomasi ushlaydi"]
A --> CDiagramma kanondagi ikki qatlamning vazifasini ko'rsatadi: so'rov tomonidagi buzilishni ikkalasi ham ushlaydi (surat aniqroq), javob tomonidagisini — faqat iste'molchi shartnomasi. Ikkinchi qatlam aynan shuning uchun yozilgan.
5.2 Uni qanday yopish mumkin
Ikki yo'l bor. Birinchisi — Swagger'ga javob sxemalarini qo'shish (@ApiResponse va Zod'dan toJSONSchema). Shunda surat javob turini ham solishtirar edi. Kanonda bu qilinmagan, keyingi ish ro'yxatida turibdi. Ikkinchisi — iste'molchi shartnomasi, kanon tanlagan yo'l. Ular bir-birini almashtirmaydi: javob sxemasi provayderning va'dasi, iste'molchi shartnomasi — iste'molchining talabi.
6. Iste'molchi shartnomasi
6.1 Frontend nimaga tayanadi
22-qismdagi vazifalar frontendi har vazifadan sakkiz maydon o'qiydi: id, matn, bajarildi, teglar, yaratildi, yangilandi, tartib, versiya. Ro'yxatdan — vazifalar va sahifa.limit. Xatodan — type, title, status, ixtiyoriy detail. Iste'molchi shartnomasi aynan shu ro'yxat, boshqa hech narsa:
const Vazifa = z.looseObject({
id: z.int().positive(),
matn: z.string(),
bajarildi: z.boolean(),
teglar: z.array(z.string()),
yaratildi: z.iso.datetime(),
yangilandi: z.iso.datetime(),
tartib: z.int(),
versiya: z.int().positive(),
});API javobida bulardan ko'proq maydon bor (masalan muddat, ustuvorlik). Shartnomada ular yo'q — frontend ularga tayanmaydi, demak ular o'zgarsa ham hech narsa buzilmaydi.
6.2 Nega looseObject
Zod'da obyektning uch xil sxemasi bor. Farqni haqiqiy Zod bilan ko'ramiz:
import { z } from "zod";
const shakl = { id: z.int(), matn: z.string() };
const javob = { id: 7, matn: "Non olish", muddat: null };
const qattiq = z.strictObject(shakl).safeParse(javob);
console.log("strictObject:", qattiq.success,
qattiq.error?.issues[0]?.code);
console.log("object:", z.object(shakl).parse(javob));
console.log("looseObject:", z.looseObject(shakl).parse(javob));Konsolda:
strictObject: false unrecognized_keys
object: { id: 7, matn: 'Non olish' }
looseObject: { id: 7, matn: 'Non olish', muddat: null }strictObject bilan yozilgan iste'molchi shartnomasi provayder yangi maydon qo'shgan zahoti yiqiladi — garchi bu buzmaydigan o'zgarish bo'lsa ham. Shartnoma o'zi "yolg'on signal" beradi va jamoa unga ishonmay qo'yadi. object ortiqcha maydonni jim olib tashlaydi, looseObject qoldiradi. Tekshiruv uchun ikkalasi ham to'g'ri. Kanon looseObject ni tanladi: shartnomaning ma'nosi ochiq ko'rinsin — "boshqa maydonlar bo'lishi mumkin".
Qoida qisqa: provayder qabul qilishda qat'iy, iste'molchi o'qishda bag'rikeng. Server noma'lum maydonli so'rovni 422 bilan rad etadi (strictObject). Mijoz esa javobdagi noma'lum maydonni e'tiborsiz qoldiradi (looseObject).
Intervyuda bu mavzu Postel qonuni (robustness principle) nomi bilan so'raladi: "yuborishda qat'iy, qabul qilishda bag'rikeng bo'l". Bir qarashda bizning server bu qonunga zid: u so'rovni qat'iy qabul qiladi. Ziddiyat yo'q — ikki xil narsa qabul qilinadi. Server buyruq qabul qiladi: noma'lum maydonni jim tashlab yuborsa, mijoz xatosi (masalan, muddat o'rniga mudat) sezilmay qoladi. Mijoz esa ma'lumot o'qiydi: ortiqcha maydon unga zarar bermaydi. Shuning uchun bag'rikenglik o'quvchi tomonda, qat'iylik buyruq qabul qiluvchi tomonda.
Tekshirib ko'ring: 31-qismda xato javoblaridagi
titleinglizchadan o'zbekchaga o'zgardi:"Not Found"o'rniga"Topilmadi". 25-qismdagi uchta test kutuvi yangilandi. Iste'molchi shartnomasi uchun bu buzuvchi o'zgarishmi?
Javob
Yo'q. Shartnomada title: z.string() — tur va borligi talab qilinadi, qiymat emas. Frontend title ni foydalanuvchiga ko'rsatadi, uning matniga qarab qaror qilmaydi — qaror uchun type va status bor (Problem Details). Shuning uchun 25-qism darslarida ko'rgan inglizcha title lar endi o'zbekcha chiqadi va bu kutilgan. Agar frontend if (x.title === "Not Found") deb yozilgan bo'lsa edi — o'zgarish uni buzardi. Shartnoma faqat tekshirgan narsasini himoya qiladi.
6.3 Problem javoblari va Content-Type
Shartnoma maydonlardan tashqari sarlavhani ham tekshiradi: 404 va 422 javoblari application/problem+json turida kelishi kerak. Frontend xatoni shu tur bo'yicha taniydi. 422 uchun qo'shimcha talab — xatolar massivi (joy, xabar): forma qaysi maydon yonida xato ko'rsatishini shundan biladi.
7. Pact va Schemathesis
7.1 Pact
Kanondagi iste'molchi shartnomasi bitta kamchilikka ega: uni backend repo'sida backend dasturchisi yozgan. Haqiqiy iste'molchi boshqaradigan shartnomada uni frontend yozadi. Pact shu ish oqimini quradi. Rasmiy hujjat bo'yicha (sinalmagan, 2026-11):
sequenceDiagram
participant F as Frontend testi
participant P as Pact fayli
participant B as Broker
participant A as API (provayder)
F->>P: kutilgan so'rov va javob
P->>B: nashr
A->>B: shartnomalarni olish
A->>A: so'rovlarni o'ziga yuborish
A->>B: natijaDiagrammani yuqoridan o'qing. Frontend testida Pact soxta server ko'taradi: "shu so'rovga shunday javob kutaman". Test o'tsa, kutilganlar JSON faylga (pact) yoziladi va broker ga — umumiy omborga — yuboriladi. API'ning CI'si brokerdan hamma iste'molchilar shartnomalarini oladi va ularni haqiqiy serverga qarshi "o'ynaydi". Biror shartnoma buzilsa, API deploy qilinmaydi. Brokerdagi "can I deploy?" tekshiruvi aynan shu savolga javob beradi.
Pact qachon kerak: iste'molchilar ko'p (veb, mobil, boshqa jamoalarning xizmatlari) va ular alohida jamoalar bo'lsa. vazifalar da bitta frontend va bitta jamoa — shuning uchun kanon g'oyani kutubxonasiz, bitta test faylida qo'lladi.
7.2 Schemathesis
Schemathesis — OpenAPI hujjatini o'qib, o'zi test so'rovlari yasaydigan vosita (Python'da yozilgan, CLI sifatida ishlaydi). U xususiyatga asoslangan test (property-based testing, Chegaraviy holatlar va algoritmni testlash) yondashuvidan foydalanadi: "har qanday to'g'ri so'rovga server 500 qaytarmasin va javob hujjatdagi sxemaga mos bo'lsin". Sxemada maxLength: 100 bo'lsa, u 100 va 101 belgili satrlarni ham, bo'sh satrni ham, Unicode belgilarni ham yuboradi. Kanon uni sinamagan (hujjat bo'yicha, 2026-11).
Bizning loyihada bitta jiddiy to'siq bor: javob sxemalari yo'q. Schemathesis javobni tekshira olmaydi — faqat "500 yo'q" va so'rov sxemasini sinay oladi. Avval «Uni qanday yopish mumkin» bo'limidagi birinchi yo'l kerak.
| Vosita | Nima beradi | Qachon |
|---|---|---|
| Sxema surati | aniq "nima o'zgardi" ro'yxati | har doim, arzon |
| Iste'molchi shartnomasi (Zod) | "frontend buziladimi" | bitta iste'molchi |
| Pact | ko'p iste'molchi, alohida jamoalar | mikroservislar |
| Schemathesis | sxema va kod mosligi, chegaraviy kirishlar | javob sxemalari bor bo'lsa |
8. Ko'p uchraydigan xatolar
| Xato | Oqibati | To'g'risi |
|---|---|---|
Iste'molchi shartnomasini strictObject bilan |
har yangi maydonda yolg'on yiqilish | looseObject yoki object |
| Yiqilgan suratni o'qimay yangilash | buzilish prodakshnga ketadi | farqni PR'da ko'rib chiqish |
| Butun OpenAPI hujjatini solishtirish | tavsif o'zgarsa ham yiqiladi | faqat shartnomaga tegishli qismni |
| "Swagger bor — shartnoma tekshirilgan" | javob sxemasi bo'lmasa, javob tekshirilmaydi | javob sxemasi yoki iste'molchi shartnomasi |
| Shartnomada hamma maydon | provayder hech narsani o'zgartira olmaydi | faqat iste'molchi ishlatadigan maydonlar |
Qiymatni shartnomaga kiritish (title: "Not Found") |
matn o'zgarsa, test buziladi | tur va borligi; qaror — type/status bo'yicha |
9. Mashqlar
Mashqlar kurs/mashqlar/31/32-shartnoma/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).
1-mashq (oson): buzadimi?
- Javobga yangi
muddatmaydoni qo'shildi — buzuvchi o'zgarish: - So'rovda
tegquery parametri olib tashlandi — buzuvchi: - Iste'molchi shartnomasi uchun Zod'dagi bag'rikeng obyekt —
z. - Kanondagi suratni ataylab yangilash o'zgaruvchisi —
SHARTNOMA_=1 - Iste'molchilar shartnomalarini saqlaydigan umumiy ombor Pact'da —
Yechim
- yo'q — bag'rikeng mijoz bilmagan maydonni e'tiborsiz qoldiradi.
- ha — uni yuborgan eski mijoz 422 oladi (qat'iy sxema) yoki filtr ishlamay qoladi.
- looseObject — noma'lum maydonlar qoladi va xato bermaydi.
- YANGILA —
SHARTNOMA_YANGILA=1 npx vitest run test/shartnoma.test.ts. - broker — API CI'si shartnomalarni shundan oladi.
2-mashq (o'rta): parametrlarni solishtirish
Kanon suratida parametrlar "query:teg", "path:id!" ko'rinishida saqlanadi (! — majburiy). parametrBuzilishlari(eski, yangi) funksiyasini yozing: olib tashlangan parametr (majburiy belgisi olib tashlangan bo'lsa ham — bu buzilish emas) va yangi majburiy parametr. Ishora: p.replace(/!$/, ""), endsWith("!"), includes (Matn ichida qidirish).
Yechim
function parametrBuzilishlari(eski, yangi) {
const b = [];
for (const p of eski) {
const qoldi = yangi.includes(p) ||
yangi.includes(p.replace(/!$/, ""));
if (!qoldi) b.push(`parametr ${p} olib tashlandi`);
}
for (const p of yangi) {
if (p.endsWith("!") && !eski.includes(p)) {
b.push(`yangi MAJBURIY parametr ${p}`);
}
}
return b;
}
const eski = ["query:teg", "query:limit", "query:q!"];
console.log(parametrBuzilishlari(eski,
["query:teg", "query:limit", "query:q", "query:sort"]));
console.log(parametrBuzilishlari(eski,
["query:limit", "query:q!", "query:sahifa!"]));Konsolda:
[]
[
'parametr query:teg olib tashlandi',
'yangi MAJBURIY parametr query:sahifa!'
]Birinchi holatda q majburiydan ixtiyoriyga o'tdi va ixtiyoriy sort qo'shildi — ikkalasi ham eski mijozni buzmaydi. Kanondagi buzilishlar() aynan shu qoidalarni ishlatadi.
3-mashq (qiyin): iste'molchi shartnomasi paketsiz
Zod'siz kichik tekshirgich yozing: shartnoma(sxema, javob) — sxema { maydon: "string" | "number" | "boolean" | "array" } ko'rinishida; har talab qilingan maydon bor va turi mos bo'lsin, ortiqcha maydonlar — ruxsat. Natija — xatolar ro'yxati ("tartib: number kutilgan, string keldi"). Ikkita javobni tekshiring: to'g'ri va tartib satr bo'lgan. Ishora: typeof, Array.isArray.
Yechim
function tur(q) {
if (Array.isArray(q)) return "array";
return q === null ? "null" : typeof q;
}
function shartnoma(sxema, javob) {
const xatolar = [];
for (const [k, kutilgan] of Object.entries(sxema)) {
if (!(k in javob)) { xatolar.push(`${k}: yo'q`); continue; }
const keldi = tur(javob[k]);
if (keldi !== kutilgan) {
xatolar.push(`${k}: ${kutilgan} kutilgan, ${keldi} keldi`);
}
}
return xatolar;
}
const Vazifa = {
id: "number", matn: "string", bajarildi: "boolean",
teglar: "array", tartib: "number",
};
const togri = {
id: 7, matn: "Non olish", bajarildi: false,
teglar: ["uy"], tartib: 3, muddat: null,
};
console.log(shartnoma(Vazifa, togri));
console.log(shartnoma(Vazifa, { ...togri, tartib: "3" }));
console.log(shartnoma(Vazifa, { id: 7, matn: "x" }));Konsolda:
[]
[ 'tartib: number kutilgan, string keldi' ]
[ "bajarildi: yo'q", "teglar: yo'q", "tartib: yo'q" ]Ortiqcha muddat: null xato bermadi — bu bag'rikeng o'quvchi. typeof [] "object" qaytaradi, shuning uchun massiv alohida tekshirildi. Uchinchi qatordagi xabarlar qo'shtirnoqda chiqdi — ichida apostrof bor.
10. Loyiha qadami: test/shartnoma.test.ts
Loyihaning yakuniy bosqichida ikkinchi fayl. U ikki narsa qo'shadi:
test/shartnoma.test.ts— uchta test: OpenAPI surati, solishtirgichning o'z testi va iste'molchi shartnomasi.docs/openapi-snapshot.json— surat. Uni qo'lda yozmaysiz: test birinchi ishga tushganda fayl yo'q bo'lsa, o'zi yaratadi.
Test ilovani xotira ombori bilan ko'taradi (XotiraOmbori) — baza kerak emas, Supertest so'rovlari jarayon ichida ketadi.
test/shartnoma.test.ts (kanon fayli; qatorlar telefon uchun bo'lingan, ba'zi uzun ifodalar oraliq o'zgaruvchiga ajratilgan — mantiq aynan; holat nusxasida tsc, ESLint va Vitest bilan tekshirildi):
// shartnoma.test.ts — shartnoma (contract) testlari.
// (1) OpenAPI SURATI (/docs-json) — docs/openapi-snapshot.json bilan
// solishtiriladi: olib tashlangan operatsiya/parametr, yangi
// MAJBURIY maydon, tur o'zgarishi — BREAKING (test yiqiladi);
// qo'shimchalar — ruxsat. Ataylab o'zgartirilganda:
// SHARTNOMA_YANGILA=1 npx vitest run test/shartnoma.test.ts
// (2) ISTE'MOLCHI shartnomasi (Pact g'oyasi, kutubxonasiz): frontend
// (22-qism) TAYANADIGAN javob shakllari — Zod bilan.
// Swagger hujjatida javob sxemalari YO'Q — shuning uchun (2).
import { test } from "vitest";
import assert from "node:assert/strict";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import request from "supertest";
import { z } from "zod";
import { expressIlova } from "./yordamchi.ts";
import { XotiraOmbori } from "../src/vazifalar/xotira-ombor.ts";
type Param = { name: string; in: string; required?: boolean };
type Hujjat = {
paths: Record<string, Record<string, {
parameters?: Param[];
requestBody?: { content: Record<string, { schema: unknown }> };
responses: Record<string, unknown>;
}>>;
};
type Maydon = { tur: string; majburiy: boolean };
type Operatsiya = {
parametrlar: string[];
tana: Record<string, Maydon> | null;
javoblar: string[];
};
type Sxema = {
properties?: Record<string, { type?: unknown }>;
required?: string[];
};
const SNAPSHOT =
new URL("../docs/openapi-snapshot.json", import.meta.url);
/** OpenAPI → ixcham shakl: "METOD /yo'l" → parametrlar,
* tana maydonlari (tur, majburiy), javob kodlari */
export function normallashtir(h: Hujjat): Record<string, Operatsiya> {
const n: Record<string, Operatsiya> = {};
for (const [yol, ops] of Object.entries(h.paths)) {
for (const [metod, o] of Object.entries(ops)) {
const kontent = Object.values(o.requestBody?.content ?? {});
const s = kontent[0]?.schema as Sxema | undefined;
const majburiy = s?.required ?? [];
n[`${metod.toUpperCase()} ${yol}`] = {
parametrlar: (o.parameters ?? [])
.map((p) =>
`${p.in}:${p.name}${p.required === true ? "!" : ""}`)
.sort(),
tana: s?.properties === undefined
? null
: Object.fromEntries(
Object.entries(s.properties).map(([k, v]) => [k, {
tur: JSON.stringify(v.type ?? "?"),
majburiy: majburiy.includes(k),
}]),
),
javoblar: Object.keys(o.responses).sort(),
};
}
}
return n;
}
/** eski → yangi: breaking o'zgarishlar ro'yxati (bo'sh — mos) */
export function buzilishlar(
eski: Record<string, Operatsiya>,
yangi: Record<string, Operatsiya>,
): string[] {
const b: string[] = [];
for (const [op, e] of Object.entries(eski)) {
const y = yangi[op];
if (y === undefined) {
b.push(`${op}: olib tashlandi`);
continue;
}
for (const p of e.parametrlar) {
const qoldi = y.parametrlar.includes(p) ||
y.parametrlar.includes(p.replace(/!$/, ""));
if (!qoldi) b.push(`${op}: parametr ${p} olib tashlandi`);
}
for (const p of y.parametrlar) {
if (p.endsWith("!") && !e.parametrlar.includes(p)) {
b.push(`${op}: yangi MAJBURIY parametr ${p}`);
}
}
for (const [k, t] of Object.entries(y.tana ?? {})) {
const et = e.tana?.[k];
if (t.majburiy && et?.majburiy !== true) {
b.push(`${op}: tana.${k} endi majburiy`);
}
if (et !== undefined && et.tur !== t.tur) {
b.push(`${op}: tana.${k} turi ${et.tur} → ${t.tur}`);
}
}
for (const k of Object.keys(e.tana ?? {})) {
if (y.tana?.[k] === undefined) {
b.push(`${op}: tana.${k} olib tashlandi ` +
"(strict sxemada mijoz yuborsa — 422)");
}
}
for (const j of e.javoblar) {
if (!y.javoblar.includes(j)) {
b.push(`${op}: javob ${j} olib tashlandi`);
}
}
}
return b;
}
const app = await expressIlova({ ombor: new XotiraOmbori() });
test("OpenAPI snapshot: breaking o'zgarish yo'q " +
"(qo'shimchalar — ruxsat)", async () => {
const javob = await request(app).get("/docs-json").expect(200);
const joriy = normallashtir(javob.body as Hujjat);
const yangila = process.env["SHARTNOMA_YANGILA"] === "1";
if (yangila || !existsSync(SNAPSHOT)) {
writeFileSync(SNAPSHOT, `${JSON.stringify(joriy, null, 2)}\n`);
}
const eski = JSON.parse(readFileSync(SNAPSHOT, "utf8")) as
Record<string, Operatsiya>;
assert.deepEqual(buzilishlar(eski, joriy), []);
const yangi = Object.keys(joriy).filter((k) => !(k in eski));
assert.ok(Array.isArray(yangi)); // yangi operatsiya — breaking emas
});
test("solishtirgich o'zi: parametr olib tashlash, majburiy maydon, " +
"tur o'zgarishi, operatsiya o'chirish — ushlanadi", () => {
const str = (majburiy: boolean) =>
({ tur: "\"string\"", majburiy });
const eski: Record<string, Operatsiya> = {
"GET /v1/vazifalar": {
parametrlar: ["query:teg", "query:limit"],
tana: null, javoblar: ["200"],
},
"POST /v1/vazifalar": {
parametrlar: [],
tana: { matn: str(true), muddat: str(false) },
javoblar: ["201"],
},
"DELETE /v1/vazifalar/{id}": {
parametrlar: ["path:id!"], tana: null, javoblar: ["204"],
},
};
const yangi: Record<string, Operatsiya> = {
"GET /v1/vazifalar": {
parametrlar: ["query:limit", "query:sahifa!"],
tana: null, javoblar: ["200"],
},
"POST /v1/vazifalar": {
parametrlar: [],
tana: {
matn: str(true),
muddat: { tur: "\"integer\"", majburiy: true },
},
javoblar: ["201"],
},
};
assert.deepEqual(buzilishlar(eski, yangi).sort(), [
"DELETE /v1/vazifalar/{id}: olib tashlandi",
"GET /v1/vazifalar: parametr query:teg olib tashlandi",
"GET /v1/vazifalar: yangi MAJBURIY parametr query:sahifa!",
"POST /v1/vazifalar: tana.muddat endi majburiy",
"POST /v1/vazifalar: tana.muddat turi " +
"\"string\" → \"integer\"",
].sort());
});
// Frontend (22-qism) tayanadigan maydonlar — ortiqcha maydon ruxsat
// (looseObject), lekin bular SHART va turi aynan
const Vazifa = z.looseObject({
id: z.int().positive(),
matn: z.string(),
bajarildi: z.boolean(),
teglar: z.array(z.string()),
yaratildi: z.iso.datetime(),
yangilandi: z.iso.datetime(),
tartib: z.int(),
versiya: z.int().positive(),
});
const Royxat = z.looseObject({
versiya: z.number(),
vazifalar: z.array(Vazifa),
sahifa: z.looseObject({ limit: z.int() }),
});
const Problem = z.looseObject({
type: z.string(),
title: z.string(),
status: z.int(),
detail: z.string().optional(),
});
const ProblemXatolar = Problem.extend({
xatolar: z.array(z.looseObject({
joy: z.string(), xabar: z.string(),
})),
});
const PROBLEM = /application\/problem\+json/;
test("iste'molchi shartnomasi: ro'yxat, yaratish, olish, PATCH, " +
"404 va 422 javoblari frontend kutgan shaklda (+ Content-Type)",
async () => {
const yarat = await request(app).post("/v1/vazifalar")
.send({ matn: "Shartnoma #test" }).expect(201);
const v = Vazifa.parse(yarat.body);
const yol = `/v1/vazifalar/${String(v.id)}`;
Vazifa.parse((await request(app).get(yol).expect(200)).body);
Royxat.parse((await request(app)
.get("/v1/vazifalar?limit=5").expect(200)).body);
Vazifa.parse((await request(app).patch(yol)
.set("Content-Type", "application/merge-patch+json")
.send({ bajarildi: true }).expect(200)).body);
const yoq = await request(app).get("/v1/vazifalar/99999")
.expect(404).expect("Content-Type", PROBLEM);
Problem.parse(yoq.body);
const notogri = await request(app).post("/v1/vazifalar")
.send({ matn: "" }).expect(422).expect("Content-Type", PROBLEM);
assert.ok(ProblemXatolar.safeParse(notogri.body).success);
});Fayl tuzilishi bo'yicha qisqa yo'riqnoma:
normallashtir— har operatsiya uchun parametrlar (saralangan — tartib o'zgarsa yolg'on farq chiqmasin), tana maydonlari va javob kodlari. TurJSON.stringifybilan satrga aylanadi:"string"va["string","null"]farqlanadi.buzilishlar— eskidan yangiga: eski mijoz kutgan hamma narsa joyidami.- Ikkinchi test solishtirgichning o'zini tekshiradi. Bu muhim:
buzilishlar()da xato bo'lsa, birinchi test har doim yashil bo'lardi — "hech narsa buzilmadi" va "solishtirgich ishlamayapti" bir xil ko'rinadi. Testni tekshiruvchi test. - Uchinchi test — iste'molchi shartnomasi. U oddiy javoblarni ham, xato javoblarini ham tekshiradi.
Ikki funksiya export qilingan: keyinchalik ularni CI skriptida ham ishlatish mumkin (masalan, ikki git versiyasi orasidagi suratlarni solishtirish).
Ishga tushiring:
npx vitest run test/shartnoma.test.ts --reporter=verboseBiz ishga tushirdik (bazasiz, qatorlar qisqartirilgan):
✓ |asosiy| test/shartnoma.test.ts > OpenAPI snapshot: breaking o'zgarish yo'q (qo'shimchalar — ruxsat) 74ms
✓ |asosiy| test/shartnoma.test.ts > solishtirgich o'zi: parametr olib tashlash, majburiy maydon, tur o'zgarishi, operatsiya o'chirish — ushlanadi 1ms
✓ |asosiy| test/shartnoma.test.ts > iste'molchi shartnomasi: ro'yxat, yaratish, olish, PATCH, 404 va 422 javoblari frontend kutgan shaklda (+ Content-Type) 148ms
Test Files 1 passed (1)
Tests 3 passed (3)Birinchi ishga tushishda docs/openapi-snapshot.json paydo bo'ladi — uni ham repoga qo'shasiz. Ichini oching: 13 ta operatsiya, har birida parametrlar, tana va javoblar.
TESTLAR.md ga bir qator qo'shing: test/shartnoma.test.ts — OpenAPI surati va iste'molchi shartnomasi; ataylab o'zgartirganda SHARTNOMA_YANGILA=1.
Hali commit qilmang — keyingi darsda loyihaning yuklama ostidagi xulqini o'lchaymiz, commit esa Test arxitekturasi va CI darsida.
11. Real ishda
- Qayerda uchraydi: mobil ilovasi bor har jamoada — eski versiya foydalanuvchi telefonida oylab yashaydi va API'ning har buzuvchi o'zgarishi uni sindiradi. Mikroservislarda — har xizmat boshqasining iste'molchisi.
- Vositalar: Pact va Pact Broker (PactFlow), oasdiff va openapi-diff (OpenAPI versiyalarini solishtirish), Schemathesis, Dredd; GraphQL'da sxema farqi — GraphQL Inspector. Ularning hech biri kanonda sinalmagan.
- Intervyu savollari: "Buzuvchi o'zgarishni qanday aniqlaysiz?", "Consumer-driven contract nima va qachon kerak?", "API'ga maydon qo'shish xavfsizmi?", "Postel qonuni (tolerant reader) nima?"
- Jamoada: shartnoma testi yiqilganda birinchi savol "testni qanday o'chiramiz?" emas, "qaysi iste'molchi buziladi?". Javob aniq bo'lsa — yo o'zgarish ixtiyoriy shaklga keltiriladi, yo yangi versiya chiqariladi va eski versiyaning tugash sanasi e'lon qilinadi.
Xulosa
- Shartnoma testi ikki tomon bir xil javob haqida gapirayotganini tekshiradi; o'z testlari buni ko'rmaydi.
- So'rovga majburiy maydon qo'shish, maydon yoki parametrni olib tashlash, turni o'zgartirish — buzadi; ixtiyoriy maydon va yangi endpoint — buzmaydi.
- OpenAPI surati "nima o'zgardi" ni aniq aytadi; kanonda u javob turini ko'rmaydi — Swagger'da javob sxemalari yo'q.
- Iste'molchi shartnomasi — faqat frontend ishlatadigan maydonlar,
looseObjectbilan: provayder qabulda qat'iy, iste'molchi o'qishda bag'rikeng. - Pact — ko'p iste'molchi va alohida jamoalar uchun; Schemathesis — sxemadan avtomatik so'rovlar (javob sxemalari kerak).
Keyingi dars: Yuklama va stress testlari — vazifalar-api qancha yukni ko'taradi, to'yinish nuqtasi qayerda va 31-qism qo'shgan qatlamlar har so'rovga qancha narx qo'shdi.
Manbalar
- OpenAPI 3.0.3 spetsifikatsiyasi — Responses Object: https://spec.openapis.org/oas/v3.0.3#responses-object (sinalmagan URL, 2026-11)
- Pact hujjati — Introduction: https://docs.pact.io/ (sinalmagan, 2026-11)
- Pact Broker — Can I Deploy: https://docs.pact.io/pact_broker/can_i_deploy (sinalmagan, 2026-11)
- Schemathesis hujjati: https://schemathesis.readthedocs.io/en/stable/ (sinalmagan, 2026-11)
- Martin Fowler — Tolerant Reader: https://martinfowler.com/bliki/TolerantReader.html (sinalmagan URL, 2026-11)
- Martin Fowler — Consumer-Driven Contracts: https://martinfowler.com/articles/consumerDrivenContracts.html (sinalmagan URL, 2026-11)
- Zod 4 hujjati — Objects (
z.object,z.strictObject,z.looseObject): https://zod.dev/api#objects (sinalmagan URL, 2026-11) - Kanon (IlmHamroh, 2026-11-03):
test/shartnoma.test.ts,docs/openapi-snapshot.json. Swagger'da javob sxemalari yo'qligi — kanon topilmasi. - Biz ishga tushirdik (2026-11-03, Vitest 5.0.5, Node 24.21.0):
/docs-jsonbo'lagi, ikki buzilish tajribasi,shartnoma.test.tsyugurishi. Zod bloki — zod 4.6.5. Brauzer modellari va mashqlar — Node 24.21.0.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!