QA 20: API avtomatlashtirish — Playwright request, Zod schema va API+UI gibrid testlar
🧪 QA Engineer — Zero to Hero
QA 20: API avtomatlashtirish — Playwright request, Zod schema va API+UI gibrid testlar
Holat: 2026-yil oktabr. Dars Playwright 1.64, Zod 4.6, @faker-js/faker 10 va Node.js 24 LTS asosida yozilgan. Barcha testlar restful-booker.herokuapp.com va practicesoftwaretesting.com (Toolshop) API'larida haqiqatan ishga tushirib tekshirilgan; javob kodlari va xato matnlari — real natijalar.
Bu darsdan keyin siz: API testlarini qo'lda Postman'da bosishdan avtomatik, CI'da ishlaydigan suite darajasiga olib chiqasiz.
- Playwright
requestfixture'i bilan brauzersiz API testlar yozasiz; - javoblarni Zod sxemalari bilan tekshirasiz va TypeScript turlarini sxemadan olasiz;
BookingClientAPI qatlami va fixture'lar bilan test ma'lumotining hayot siklini boshqarasiz;- ma'lum bug'larni
test.fail()bilan to'g'ri hujjatlashtirasiz; - API + UI gibrid testlar bilan UI testlarni tezlashtirasiz;
- Postman/Newman, REST Assured va pytest bilan solishtira olasiz.
Mundarija
- Nega API'ni avtomatlashtiramiz
- Playwright request: asoslar
- Loyiha tuzilmasi va config
- Zod bilan schema validatsiya
- BookingClient: API qatlami
- Fixture'lar: token, client va test data hayot sikli
- To'liq CRUD suite
- Negativ testlar va ma'lum bug'lar
- API + UI gibrid testlar
- Boshqa stack'lar: Newman, REST Assured, pytest
- Amaliy mashqlar
- Xulosa
- Manbalar
1. Nega API'ni avtomatlashtiramiz {#nega-api}
QA 10 va QA 11da API'ni qo'lda — curl va Postman bilan — testladik va test dizaynini o'rgandik. Endi o'sha bilimni kodga aylantiramiz.
Test piramidasida (QA 4) API testlari o'rta qavatda turadi va eng yaxshi "narx/foyda" nisbatini beradi:
| Xususiyat | UI (E2E) test | API test |
|---|---|---|
| Brauzer kerakmi | ha | yo'q |
| Tezlik | soniyalar (sahifa yuklanishi, render) | odatda yuzlab millisekundlar |
| Barqarorlik | animatsiya, render, locator'lar ta'sir qiladi | faqat tarmoq va backend |
| Nimani tekshiradi | foydalanuvchi oqimi, UI | biznes qoidalari, kontrakt, ruxsatlar, xato holatlari |
| Xato joyini topish | qiyinroq (UI yoki backend?) | aniq: qaysi endpoint, qaysi maydon |
Bizning ishga tushirishimizda bu darsdagi 10 ta API test restful-booker'ga qarshi 4 ta worker'da taxminan 6 soniyada tugadi — ularning har biri yangi bron yaratib, tekshirib, o'chirib chiqadi.
API avtomatlashtirish ushlaydigan tipik bug'lar:
- kontrakt buzilishi — backend maydon nomini o'zgartirdi (
firstname→firstName), mobil ilova esa yiqildi; - ruxsat xatolari — token'siz so'rov 403 o'rniga 200 qaytaradi (QA 24);
- validatsiya — noto'g'ri ma'lumotga 400 o'rniga 500 (server ichida ishlov berilmagan xato);
- biznes qoidalari — DemoBozor'da 300 000 so'mlik buyurtmaga yetkazib berish narxi qo'shilib ketishi.
2. Playwright request: asoslar {#request-asoslari}
Playwright faqat brauzer emas — unda to'liq HTTP klient bor: APIRequestContext. Uni uch yo'l bilan olasiz:
| Usul | Nima | Qachon |
|---|---|---|
request fixture | har test uchun yangi kontekst, project'ning baseURL va extraHTTPHeadersini oladi | oddiy API testlar |
playwright.request.newContext({...}) | qo'lda yaratiladigan kontekst, dispose() qilish kerak | worker fixture'lar, beforeAll |
page.request | sahifa bilan cookie'larni bo'lishadi | brauzer sessiyasi ichidan API chaqirish |
Eng oddiy test:
import { test, expect } from "@playwright/test";
test("ping 201", async ({ request }) => {
const response = await request.get("/ping");
expect(response.status()).toBe(201);
});
/ping — restful-booker'ning "server tirikmi?" endpoint'i. U 200 emas, 201 qaytaradi — bu servisning o'ziga xosligi, 7-bo'limda bunday "quirk"larni batafsil ko'ramiz.
2.1. So'rov opsiyalari
await request.post("/booking", {
data: { firstname: "Aziz" }, // obyekt -> JSON, Content-Type avtomatik
headers: { Cookie: "token=abc123" }, // qo'shimcha header'lar
params: { firstname: "Aziz" }, // ?firstname=Aziz
timeout: 10_000, // ms
});
| Opsiya | Ma'nosi |
|---|---|
data | body: obyekt bo'lsa JSON qilib yuboriladi, string/Buffer — o'zicha |
form | application/x-www-form-urlencoded forma |
multipart | fayl yuklash (multipart/form-data) |
params | query parametrlar |
headers | shu so'rov uchun header'lar |
failOnStatusCode | true bo'lsa, 2xx/3xx bo'lmagan javobda darhol xato tashlaydi |
2.2. Javob bilan ishlash
const response = await request.get("/booking/1");
response.status(); // 200
response.ok(); // true, agar status 200–299
response.headers(); // { 'content-type': 'application/json; charset=utf-8', ... }
await response.json(); // body -> obyekt
await response.text(); // body -> string
await expect(response).toBeOK(); // 2xx bo'lmasa, xato matnida javob body'si ham ko'rinadi
💡 Maslahat:
expect(response.ok()).toBeTruthy()o'rnigaawait expect(response).toBeOK()yozing: yiqilganda Playwright javob statusini va body'sini xato xabariga qo'shadi — bug report uchun tayyor dalil.
3. Loyiha tuzilmasi va config {#tuzilma}
QA 19dagi loyihaga API qatlamini qo'shamiz:
qa-demo-tests/
├── playwright.config.ts # 'api' project: baseURL + Accept header
├── .env.example # API_BASE_URL, BOOKER_USERNAME, BOOKER_PASSWORD
├── api/BookingClient.ts # restful-booker uchun API klient
├── schemas/booking.ts # Zod sxemalari
├── data/bookingFactory.ts # test ma'lumoti generatori (faker)
├── fixtures/api.ts # authToken (worker), bookingClient, newBooking
└── tests/api/booking.spec.ts
Config'dagi api project (to'liq config — QA 19, 4.2-bo'lim):
{
name: 'api',
testDir: './tests/api',
use: {
baseURL: process.env.API_BASE_URL ?? 'https://restful-booker.herokuapp.com',
extraHTTPHeaders: { Accept: 'application/json' },
},
},
extraHTTPHeaders shu project'dagi har bir so'rovga qo'shiladi. Bu project brauzer ishga tushirmaydi va setupga bog'liq emas.
Paketlar:
npm i -D @playwright/test zod @faker-js/faker dotenv
Faqat API testlarni ishga tushirish:
npx playwright test --project=api
4. Zod bilan schema validatsiya {#zod}
"Status 200" — bu faqat birinchi qadam. Backend totalpriceni raqam o'rniga string qilib qaytarsa yoki sanani buzsa, status baribir 200 bo'ladi, mobil ilova esa yiqiladi. Schema validatsiya javobning shaklini tekshiradi: qaysi maydonlar bor, turlari, formatlari.
QA 11da JSON Schema'ni ko'rdik. TypeScript loyihada eng qulay vosita — Zod: sxema bir vaqtda ham runtime tekshiruv, ham TypeScript turi.
// schemas/booking.ts
import { z } from "zod";
export const BookingSchema = z.object({
firstname: z.string().min(1),
lastname: z.string().min(1),
totalprice: z.number().int().nonnegative(),
depositpaid: z.boolean(),
bookingdates: z.object({
checkin: z.iso.date(), // "2026-11-01" formatidagi sana
checkout: z.iso.date(),
}),
additionalneeds: z.string().optional(),
});
export const CreatedBookingSchema = z.object({
bookingid: z.number().int().positive(),
booking: BookingSchema,
});
export const AuthTokenSchema = z.object({
token: z.string().min(10),
});
// Sxemadan TypeScript turini avtomatik olamiz — ikki marta yozmaymiz
export type Booking = z.infer<typeof BookingSchema>;
4.1. parse va safeParse
| Metod | Mos kelmasa | Qachon |
|---|---|---|
Schema.parse(data) | xato tashlaydi (test shu joyda yiqiladi) | testlarda — eng ko'p ishlatiladi |
Schema.safeParse(data) | { success: false, error } qaytaradi | xatolarni o'zingiz tahlil qilmoqchi bo'lsangiz |
Buzilgan javobni tekshirib ko'ramiz (totalprice string, sana mavjud emas — 13-oy):
import { BookingSchema } from "../schemas/booking";
const result = BookingSchema.safeParse({
firstname: "Aziz",
lastname: "Karimov",
totalprice: "250",
depositpaid: true,
bookingdates: { checkin: "2026-11-01", checkout: "2026-13-05" },
});
if (!result.success) {
console.log(
result.error.issues.map(
(issue) => `${issue.path.join(".")}: ${issue.message}`,
),
);
}
Haqiqiy natija (Zod 4.6):
[
'totalprice: Invalid input: expected number, received string',
'bookingdates.checkout: Invalid ISO date'
]
Bug report uchun ideal: qaysi maydon va nima kutilgani aniq yozilgan.
4.2. Ortiqcha maydonlarni ushlash: strictObject
z.object() noma'lum maydonlarni jimgina tashlab yuboradi. Kontrakt o'zgarishini (backend yangi maydon qo'shdi yoki maxfiy maydonni oshkor qildi) sezish uchun z.strictObject():
const Strict = z.strictObject({ token: z.string() });
const result = Strict.safeParse({ token: "abc", role: "admin" });
// result.success === false, xabar: 'Unrecognized key: "role"'
⚠️ Diqqat: Hamma joyda
strictObjectishlatsangiz, backend har yangi maydon qo'shganda testlar yiqiladi. Uni ataylab — masalan, javobda parol xeshi, ichki ID kabi chiqmasligi kerak bo'lgan maydonlarni ushlash uchun qo'llang.
4.3. Muqobil: JSON Schema + Ajv
Jamoada OpenAPI spetsifikatsiyasi bo'lsa, undagi JSON Schema'ni to'g'ridan-to'g'ri Ajv bilan ishlatish mumkin:
import Ajv from "ajv";
import addFormats from "ajv-formats";
const bookingJsonSchema = {
type: "object",
required: [
"firstname",
"lastname",
"totalprice",
"depositpaid",
"bookingdates",
],
properties: {
firstname: { type: "string", minLength: 1 },
lastname: { type: "string", minLength: 1 },
totalprice: { type: "integer", minimum: 0 },
depositpaid: { type: "boolean" },
bookingdates: {
type: "object",
required: ["checkin", "checkout"],
properties: {
checkin: { type: "string", format: "date" },
checkout: { type: "string", format: "date" },
},
},
additionalneeds: { type: "string" },
},
};
const ajv = new Ajv({ allErrors: true });
addFormats(ajv); // "date" kabi formatlar uchun
const validate = ajv.compile(bookingJsonSchema);
const sample = {
firstname: "Aziz",
lastname: "Karimov",
totalprice: "250",
depositpaid: true,
bookingdates: { checkin: "2026-11-01", checkout: "2026-13-05" },
};
if (!validate(sample)) {
console.log(validate.errors?.map((e) => `${e.instancePath} ${e.message}`));
}
Natija:
[
'/totalprice must be integer',
'/bookingdates/checkout must match format "date"'
]
| Zod | JSON Schema + Ajv | |
|---|---|---|
| Qayerdan keladi | TypeScript kodda yoziladi | OpenAPI/JSON fayldan |
| TypeScript turi | z.infer bilan bepul | alohida generatsiya kerak |
| Tillararo | faqat JS/TS | istalgan til (Java, Python, Postman ham) |
| Qachon | TS-first jamoa, tez boshlash | spetsifikatsiya "haqiqat manbai" bo'lgan jamoa |
5. BookingClient: API qatlami {#api-client}
Testlarda URL, header va token qatorlarini takrorlamaslik uchun yupqa API klient yozamiz. Bu — UI'dagi Page Object'ning API'dagi ekvivalenti:
// api/BookingClient.ts
import type { APIRequestContext, APIResponse } from "@playwright/test";
import type { Booking } from "../schemas/booking";
// restful-booker uchun yupqa API qatlam: testlar URL va header'larni bilmaydi
export class BookingClient {
private readonly request: APIRequestContext;
private readonly token?: string;
constructor(request: APIRequestContext, token?: string) {
this.request = request;
this.token = token;
}
private authHeaders(): Record<string, string> {
return this.token ? { Cookie: `token=${this.token}` } : {};
}
createBooking(booking: Booking): Promise<APIResponse> {
return this.request.post("/booking", { data: booking });
}
getBooking(id: number): Promise<APIResponse> {
return this.request.get(`/booking/${id}`);
}
findBookingIds(filter: Record<string, string>): Promise<APIResponse> {
return this.request.get("/booking", { params: filter });
}
updateBooking(id: number, booking: Booking): Promise<APIResponse> {
return this.request.put(`/booking/${id}`, {
data: booking,
headers: this.authHeaders(),
});
}
patchBooking(id: number, changes: Partial<Booking>): Promise<APIResponse> {
return this.request.patch(`/booking/${id}`, {
data: changes,
headers: this.authHeaders(),
});
}
deleteBooking(id: number): Promise<APIResponse> {
return this.request.delete(`/booking/${id}`, {
headers: this.authHeaders(),
});
}
}
Dizayn qarorlari:
- **Klient
APIResponseqaytaradi, assertion qilmaydi.** Negativ testlar 403 yoki 404 ni tekshirishi kerak — klient ichidatoBeOK()bo'lsa, ularni yozib bo'lmasdi. - Token konstruktorda. Token'siz klient (
new BookingClient(request)) — tayyor "anonim foydalanuvchi" negativ testlar uchun. - Turlar sxemadan.
Bookingturiz.inferdan keladi: sxema o'zgarsa, klient ham avtomatik yangilanadi.
restful-booker token'ni Cookie: token=... header'ida kutadi (muqobil — Authorization: Basic ...). Bu uning hujjatlarida yozilgan; sizning loyihangizda odatda Authorization: Bearer <token> bo'ladi.
6. Fixture'lar: token, client va test data hayot sikli {#fixtures}
6.1. Test ma'lumoti generatori
// data/bookingFactory.ts
import { faker } from "@faker-js/faker";
import type { Booking } from "../schemas/booking";
// Har chaqiruvda yangi, unikal ma'lumot — testlar bir-biriga xalaqit bermaydi
export function buildBooking(overrides: Partial<Booking> = {}): Booking {
return {
firstname: faker.person.firstName(),
lastname: `${faker.person.lastName()}-${Date.now()}`,
totalprice: faker.number.int({ min: 50, max: 1000 }),
depositpaid: faker.datatype.boolean(),
bookingdates: { checkin: "2026-11-01", checkout: "2026-11-05" },
additionalneeds: "Breakfast",
...overrides,
};
}
Bu Test Data Builder pattern'i: standart qiymatlar tayyor, test faqat o'ziga muhim maydonni beradi — buildBooking({ totalprice: 0 }). lastnamega Date.now() qo'shilgani — parallel testlar (va boshqa odamlarning public demo'dagi ma'lumotlari) bilan to'qnashmaslik uchun.
6.2. Fixture'lar
// fixtures/api.ts
import { test as base, expect } from "@playwright/test";
import { BookingClient } from "../api/BookingClient";
import {
AuthTokenSchema,
CreatedBookingSchema,
type Booking,
} from "../schemas/booking";
import { buildBooking } from "../data/bookingFactory";
type WorkerFixtures = {
authToken: string;
};
type TestFixtures = {
bookingClient: BookingClient;
newBooking: { id: number; data: Booking };
};
export const test = base.extend<TestFixtures, WorkerFixtures>({
// worker scope: token har worker uchun BIR marta olinadi
authToken: [
async ({ playwright }, use, workerInfo) => {
const api = await playwright.request.newContext({
baseURL: workerInfo.project.use.baseURL,
});
const response = await api.post("/auth", {
data: {
username: process.env.BOOKER_USERNAME ?? "admin",
password: process.env.BOOKER_PASSWORD ?? "password123",
},
});
const { token } = AuthTokenSchema.parse(await response.json());
await api.dispose();
await use(token);
},
{ scope: "worker" },
],
bookingClient: async ({ request, authToken }, use) => {
await use(new BookingClient(request, authToken));
},
// setup: bron yaratamiz -> testga beramiz -> teardown: o'chiramiz
newBooking: async ({ bookingClient }, use) => {
const data = buildBooking();
const response = await bookingClient.createBooking(data);
const { bookingid } = CreatedBookingSchema.parse(await response.json());
await use({ id: bookingid, data });
await bookingClient.deleteBooking(bookingid);
},
});
export { expect };
Uch qatlam bir-biriga bog'langan:
authToken (worker) ──► bookingClient (test) ──► newBooking (test)
POST /auth new BookingClient( POST /booking (setup)
bir marta / worker request, token) ... test ...
DELETE /booking (teardown)
authToken— worker doirasida: 4 ta worker bo'lsa,/auth4 marta chaqiriladi, 100 marta emas. Worker fixturerequestni ishlata olmaydi (u test doirasida), shuning uchunplaywright.request.newContext()bilan o'z kontekstini ochib,dispose()qiladi.newBooking— teardown kafolati: test yiqilsa hamuse()dan keyingi qism ishlaydi va bron o'chiriladi. Ma'lumotlar bazasi "axlat" bilan to'lib ketmaydi.
🧠 Esda tuting: Test ma'lumoti qoidasi: har test o'z ma'lumotini yaratadi va o'zi tozalaydi. "Bazada id=1 bo'lgan bron bor" degan taxmin bilan yozilgan test ertami-kechmi yiqiladi — restful-booker ham ma'lumotlarni vaqti-vaqti bilan qayta tiklaydi.
7. To'liq CRUD suite {#crud}
// tests/api/booking.spec.ts
import { test, expect } from "../../fixtures/api";
import { BookingClient } from "../../api/BookingClient";
import { BookingSchema, CreatedBookingSchema } from "../../schemas/booking";
import { buildBooking } from "../../data/bookingFactory";
test.describe("Booking API — CRUD", () => {
test(
"POST /booking yangi bron yaratadi",
{ tag: "@smoke" },
async ({ bookingClient }) => {
const data = buildBooking();
const response = await bookingClient.createBooking(data);
expect(response.status()).toBe(200); // restful-booker 201 emas, 200 qaytaradi
const body = CreatedBookingSchema.parse(await response.json());
expect(body.booking).toEqual(data);
await bookingClient.deleteBooking(body.bookingid); // tozalash
},
);
test("GET /booking/:id yaratilgan bronni qaytaradi", async ({
bookingClient,
newBooking,
}) => {
const response = await bookingClient.getBooking(newBooking.id);
await expect(response).toBeOK();
const booking = BookingSchema.parse(await response.json());
expect(booking).toEqual(newBooking.data);
});
test("PUT /booking/:id to'liq yangilaydi", async ({
bookingClient,
newBooking,
}) => {
const updated = buildBooking({ totalprice: 777, additionalneeds: "Lunch" });
const response = await bookingClient.updateBooking(newBooking.id, updated);
await expect(response).toBeOK();
expect(await response.json()).toEqual(updated);
});
test("PATCH /booking/:id faqat bitta maydonni o'zgartiradi", async ({
bookingClient,
newBooking,
}) => {
const response = await bookingClient.patchBooking(newBooking.id, {
totalprice: 999,
});
await expect(response).toBeOK();
const booking = BookingSchema.parse(await response.json());
expect(booking.totalprice).toBe(999);
expect(booking.firstname).toBe(newBooking.data.firstname); // qolgani o'zgarmagan
});
test("DELETE /booking/:id o'chiradi, keyin GET 404", async ({
bookingClient,
}) => {
const created = await bookingClient.createBooking(buildBooking());
const { bookingid } = CreatedBookingSchema.parse(await created.json());
const deleted = await bookingClient.deleteBooking(bookingid);
expect(deleted.status()).toBe(201); // ma'lum quirk: 200/204 o'rniga 201
const afterDelete = await bookingClient.getBooking(bookingid);
expect(afterDelete.status()).toBe(404);
});
test("GET /booking?firstname&lastname filtri yangi bronni topadi", async ({
bookingClient,
newBooking,
}) => {
const response = await bookingClient.findBookingIds({
firstname: newBooking.data.firstname,
lastname: newBooking.data.lastname,
});
await expect(response).toBeOK();
const ids: { bookingid: number }[] = await response.json();
expect(ids.map((item) => item.bookingid)).toContain(newBooking.id);
});
});
Ishga tushirish va haqiqiy natija:
npx playwright test --project=api
✓ [api] › tests/api/booking.spec.ts › Booking API — CRUD › POST /booking yangi bron yaratadi @smoke (1.3s)
✓ [api] › tests/api/booking.spec.ts › Booking API — CRUD › GET /booking/:id yaratilgan bronni qaytaradi (1.6s)
✓ [api] › tests/api/booking.spec.ts › Booking API — CRUD › PUT /booking/:id to'liq yangilaydi (1.6s)
...
10 passed (6.4s)
7.1. "Quirk" bilan nima qilamiz?
Restful-booker REST konvensiyalaridan bir necha joyda chetga chiqadi. Biz ularni tekshirdik:
| So'rov | Haqiqiy javob | REST bo'yicha kutilgan | Izoh |
|---|---|---|---|
POST /booking | 200 + { bookingid, booking } | 201 Created | resurs yaratildi, lekin 200 |
DELETE /booking/:id | 201 Created | 200 yoki 204 | eng mashhur quirk |
DELETE qayta (o'chirilganni) | 405 Method Not Allowed | 404 | |
GET /ping | 201 | 200 | |
POST /auth noto'g'ri parol | 200 + {"reason":"Bad credentials"} | 401 |
Testda qaysi kodni yozish kerak? Qoida:
- Hujjatlashtirilgan xulq bo'lsa (servis shunday ishlashi kelishilgan) — haqiqiy kodni tekshiring va izoh qoldiring (
// restful-booker 201 emas, 200 qaytaradi). Biz CRUD testlarida shunday qildik. - Kelishilmagan, xato xulq bo'lsa — bug report oching va testni to'g'ri kutilgan natija bilan yozib,
test.fail()bilan belgilang (8-bo'lim).
Muhimi — jim o'tkazib yubormaslik. Mobil ilova 201ni kutib yozilgan bo'lsa, 200 uni sindirishi mumkin.
8. Negativ testlar va ma'lum bug'lar {#negativ}
Pozitiv testlar "ishlaydimi?" deb so'raydi, negativ testlar — "noto'g'ri ishlatilganda to'g'ri yiqiladimi?".
test.describe("Booking API — negativ holatlar", () => {
test("token'siz PUT 403 qaytaradi", async ({ request, newBooking }) => {
const anonymous = new BookingClient(request); // token yo'q
const response = await anonymous.updateBooking(
newBooking.id,
buildBooking(),
);
expect(response.status()).toBe(403);
});
test("mavjud bo'lmagan bron 404", async ({ bookingClient }) => {
const response = await bookingClient.getBooking(999_999_999);
expect(response.status()).toBe(404);
});
test("noto'g'ri parol bilan /auth token bermaydi", async ({ request }) => {
const response = await request.post("/auth", {
data: { username: "admin", password: "notogri-parol" },
});
expect(response.status()).toBe(200); // quirk: 401 emas
expect(await response.json()).toEqual({ reason: "Bad credentials" });
});
test("majburiy maydonsiz POST 400 qaytarishi kerak", async ({ request }) => {
test.fail(true, "BUG: server 400 o'rniga 500 qaytaradi");
const response = await request.post("/booking", {
data: { lastname: "Karimov" },
});
expect(response.status()).toBe(400);
});
});
Oxirgi testga e'tibor bering. firstnamesiz bron yaratishga urinishda restful-booker 500 Internal Server Error qaytaradi. Bu — haqiqiy bug: mijoz xatosi (noto'g'ri so'rov) uchun 4xx kodi va tushunarli xabar bo'lishi kerak, 500 esa "server ichida ishlov berilmagan xato" degani.
test.fail() ning ishlash mantiqi:
| Holat | Natija |
|---|---|
| Test yiqildi (bug hali bor) | ✅ "kutilgan xato" — suite yashil, list reporter'da ✘ belgisi bilan ko'rinadi, lekin hisobda passed |
| Test o'tdi (bug tuzatildi!) | ❌ "kutilmagan o'tish" — Playwright xabar beradi: test.fail()ni olib tashlang |
Shu tariqa suite yashil qoladi, bug esa unutilmaydi: annotatsiyada bug raqami (BUG-123) yozilsa, hisobotdan to'g'ridan-to'g'ri ticket'ga o'tasiz.
🎯 Intervyuda: "API test faqat status kodni tekshirsa yetarlimi?" — Yo'q. Status kod + body qiymatlari + sxema (turlar, majburiy maydonlar) + muhim header'lar + side effect (resurs haqiqatan yaratildimi/o'chirildimi — masalan, DELETE'dan keyin GET 404) tekshiriladi. Negativ holatlarda xato xabarining formati ham kontraktning bir qismi.
Negativ testlar g'oyalarini qayerdan olasiz? QA 11dagi ro'yxat: majburiy maydon yo'q, noto'g'ri tur, chegaraviy qiymatlar, token yo'q/eskirgan/boshqa foydalanuvchiniki, mavjud bo'lmagan ID, takroriy so'rov (idempotency).
9. API + UI gibrid testlar {#gibrid}
Eng kuchli UI testlar tayyorgarlikni API orqali qiladi: ma'lumot yaratish, login, holatni sozlash. UI'da esa faqat tekshirilayotgan narsa qoladi. Natija — tezroq va barqarorroq testlar.
Gibrid test: token API orqali olinadi, hisob sahifasi UI'da tekshiriladi
Toolshop'da buni ko'rsatamiz. Uning SPA'si JWT token'ni localStoragedagi auth-token kalitida saqlaydi — buni brauzerda login qilib, localStorage tarkibini ko'rib aniqladik (DevTools → Application bo'limida ham ko'rinadi, QA 9). Demo foydalanuvchi customer@practicesoftwaretesting.com / welcome01 (Jane Doe) loyihaning ochiq README'sida berilgan.
9.1. API orqali login → UI hisob sahifasi
// tests/ui/toolshop-hybrid.spec.ts
import { test, expect } from "@playwright/test";
const SHOP = "https://practicesoftwaretesting.com";
const API = "https://api.practicesoftwaretesting.com";
test("API orqali login -> UI hisob sahifasi", async ({ page, request }) => {
// 1) API: token olamiz (UI formasisiz)
const loginResponse = await request.post(`${API}/users/login`, {
data: {
email:
process.env.TOOLSHOP_EMAIL ?? "customer@practicesoftwaretesting.com",
password: process.env.TOOLSHOP_PASSWORD ?? "welcome01",
},
});
expect(loginResponse.ok()).toBeTruthy();
const { access_token } = await loginResponse.json();
// 2) API: foydalanuvchi ma'lumoti — keyin UI bilan solishtiramiz
const meResponse = await request.get(`${API}/users/me`, {
headers: { Authorization: `Bearer ${access_token}` },
});
const me = await meResponse.json();
// 3) Token'ni sahifa yuklanishidan OLDIN localStorage'ga qo'yamiz
await page.addInitScript((token) => {
window.localStorage.setItem("auth-token", token);
}, access_token);
// 4) UI: login formasiz to'g'ridan-to'g'ri hisob sahifasi
await page.goto(`${SHOP}/account`);
await expect(page.getByTestId("page-title")).toHaveText("My account");
await expect(page.getByTestId("nav-menu")).toContainText(
`${me.first_name} ${me.last_name}`,
);
});
Nozik joylar:
- **
addInitScriptsahifadagi har qanday skriptdan oldin** ishlaydi. Agar avvalgoto, keyinlocalStorage.setItemqilsangiz, Angular ilovasi allaqachon "login qilinmagan" holatda yuklangan bo'ladi. - Toolshop token'i atigi 300 soniya amal qiladi (
expires_in: 300) — har test yangi token oladi, saqlab qo'ymaydi. - Bu test UI login formasini tekshirmaydi — u alohida, kichik UI test bilan qoplanadi. Gibrid yondashuv qolgan yuzlab testlarni login sahifasiga qaramlikdan qutqaradi.
9.2. API va UI bir xil ma'lumot ko'rsatadimi?
test("API va UI bir xil mahsulotni ko'rsatadi", async ({ page, request }) => {
const apiResponse = await request.get(`${API}/products?page=1`);
expect(apiResponse.ok()).toBeTruthy();
const { data } = await apiResponse.json();
await page.goto(SHOP);
const firstCard = page.getByTestId("product-name").first();
await expect(firstCard).toHaveText(data[0].name);
});
Bu "konsistentlik" testi: UI ma'lumotni noto'g'ri map qilsa (masalan, nomni boshqa maydondan olsa yoki keshdan eski ro'yxatni ko'rsatsa) — ushlanadi.
9.3. page.request — brauzer bilan umumiy cookie'lar
Agar ilovangiz sessiyani cookieda saqlasa (localStorage emas), page.request ishlating: u sahifa kontekstining cookie'larini ishlatadi va javobdagi Set-Cookielar brauzerga ham yoziladi. Biz buni sinab ko'rdik: page.request.get('https://httpbin.org/cookies/set/demo/123')dan keyin context.cookies() ichida demo=123 paydo bo'ldi. Ya'ni API orqali login qilib, o'sha sessiya bilan darhol UI'ni ochish mumkin.
🇺🇿 Mahalliy kontekst: DemoBozor'da gibrid testning klassik misoli: buyurtmani (xayoliy)
POST /api/v1/ordersorqaliDELIVEREDholatigacha tayyorlab, UI'da "Sharh yozish" tugmasi faqat yetkazilgan buyurtmada chiqishini tekshirish. UI orqali buyurtmani yetkazilgan holatga olib kelish deyarli imkonsiz — API orqali esa bir necha so'rov.
10. Boshqa stack'lar: Newman, REST Assured, pytest {#boshqa-stacklar}
Playwright yagona yo'l emas. Jamoangiz tiliga qarab boshqa vositalar uchraydi — g'oyalar bir xil: so'rov → status → body → sxema → tozalash.
10.1. Postman kolleksiyasi CI'da: Newman
QA 10'da yozgan Postman kolleksiyangizni kodga ko'chirmasdan CI'da ishga tushirish mumkin:
npx newman run postman/booker.postman_collection.json \
-e postman/staging.postman_environment.json \
-r cli,junit --reporter-junit-export results/newman.xml
Biz 3 so'rovli kolleksiya (auth → create → delete) bilan sinadik: barcha 3 assertion o'tdi va results/newman.xml JUnit hisobot yaratildi — uni CI test tab'iga ulash mumkin (QA 21).
10.2. REST Assured (Java/Kotlin)
Backend Java/Kotlin'da bo'lgan jamoalarda ko'p uchraydi. Kotlin extension modulidagi (io.rest-assured:kotlin-extensions) uslub:
import io.restassured.http.ContentType
import io.restassured.module.kotlin.extensions.Given
import io.restassured.module.kotlin.extensions.Then
import io.restassured.module.kotlin.extensions.When
import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.Test
class BookingApiTest {
@Test
fun `yangi bron yaratiladi`() {
Given {
baseUri("https://restful-booker.herokuapp.com")
contentType(ContentType.JSON)
accept(ContentType.JSON)
body("""{"firstname":"Aziz","lastname":"Karimov","totalprice":250,"depositpaid":true,
"bookingdates":{"checkin":"2026-11-01","checkout":"2026-11-05"}}""")
} When {
post("/booking")
} Then {
statusCode(200)
body("booking.firstname", equalTo("Aziz"))
}
}
}
10.3. pytest + requests (Python)
import requests
BASE_URL = "https://restful-booker.herokuapp.com"
def test_create_and_get_booking():
payload = {
"firstname": "Aziz",
"lastname": "Karimov",
"totalprice": 250,
"depositpaid": True,
"bookingdates": {"checkin": "2026-11-01", "checkout": "2026-11-05"},
"additionalneeds": "Breakfast",
}
created = requests.post(f"{BASE_URL}/booking", json=payload, timeout=10)
assert created.status_code == 200
booking_id = created.json()["bookingid"]
fetched = requests.get(f"{BASE_URL}/booking/{booking_id}", timeout=10)
assert fetched.status_code == 200
assert fetched.json() == payload
Bu test pytest bilan o'tdi (1 passed). Haqiqiy loyihada tozalash (DELETE) pytest fixture'ida, yielddan keyin yoziladi — Playwright'dagi use() bilan bir xil g'oya.
| Stack | Til | Kuchli tomoni | Qachon tanlanadi |
|---|---|---|---|
Playwright request | TS/JS | UI testlar bilan bitta framework, fixture'lar, gibrid testlar | web jamoa, E2E ham Playwright'da |
| Postman + Newman | JS (skriptlar) | kod yozmasdan boshlash, kolleksiya = hujjat | manual QA'dan avtomatlashtirishga birinchi qadam |
| REST Assured | Java/Kotlin | JVM ekotizimi, Spring backend bilan bir repo | backend Java/Kotlin |
| pytest + requests | Python | sodda sintaksis, kuchli fixture'lar | Python jamoa, data/ML loyihalar |
🎯 Intervyuda: "API testlarni qaysi vosita bilan yozasiz?" — Vosita ikkinchi darajali; muhimi — test dizayni (pozitiv/negativ, chegaralar, ruxsatlar), sxema validatsiya, mustaqil test ma'lumoti va CI'da ishlashi. Keyin jamoa tiliga mos vositani aytasiz va nega tanlaganingizni tushuntirasiz.
11. Amaliy mashqlar {#mashqlar}
Mashq 1: Ro'yxat sxemasi
GET /booking javobi [{ "bookingid": 1 }, ...] ko'rinishidagi massiv. Zod sxemasini yozing va ro'yxat bo'sh emasligini tekshiring.
<details> <summary>💡 Javobni ko'rish</summary>
import { z } from "zod";
import { test, expect } from "../../fixtures/api";
const BookingIdsSchema = z.array(
z.object({ bookingid: z.number().int().positive() }),
);
test("GET /booking — id ro'yxati sxemaga mos", async ({ request }) => {
const response = await request.get("/booking");
await expect(response).toBeOK();
const ids = BookingIdsSchema.parse(await response.json());
expect(ids.length).toBeGreaterThan(0);
});
</details>
Mashq 2: Token'siz PATCH
PUT uchun token'siz test bor. PATCH uchun ham xuddi shunday negativ test yozing.
<details> <summary>💡 Javobni ko'rish</summary>
test("token'siz PATCH 403", async ({ request, newBooking }) => {
const anonymous = new BookingClient(request);
const response = await anonymous.patchBooking(newBooking.id, {
totalprice: 1,
});
expect(response.status()).toBe(403);
});
Restful-booker bu holatda haqiqatan 403 qaytaradi. Yana bir g'oya: boshqa foydalanuvchining token'i bilan o'zgartirishga urinish (IDOR/BOLA) — real loyihalarda eng xavfli bug'lardan biri (QA 24).
</details>
Mashq 3: Toolshop mahsulot sxemasi
GET https://api.practicesoftwaretesting.com/products?page=1 javobida current_page, total va data massivi bor. Har bir mahsulotda: id (string), name, price (son), in_stock, is_rental (boolean), category {id, name, slug}, brand {id, name}. Sxema yozib, birinchi sahifani tekshiring.
<details> <summary>💡 Javobni ko'rish</summary>
import { z } from "zod";
import { test, expect } from "@playwright/test";
const ProductSchema = z.object({
id: z.string(),
name: z.string().min(1),
price: z.number().nonnegative(),
in_stock: z.boolean(),
is_rental: z.boolean(),
category: z.object({ id: z.string(), name: z.string(), slug: z.string() }),
brand: z.object({ id: z.string(), name: z.string() }),
});
const ProductPageSchema = z.object({
current_page: z.number().int(),
total: z.number().int(),
data: z.array(ProductSchema),
});
test("Toolshop: mahsulotlar sahifasi sxemaga mos", async ({ request }) => {
const response = await request.get(
"https://api.practicesoftwaretesting.com/products?page=1",
);
await expect(response).toBeOK();
const page = ProductPageSchema.parse(await response.json());
expect(page.data.length).toBeGreaterThan(0);
});
Toolshop'da id'lar raqam emas, ULID ko'rinishidagi string (01M4GP9P47G415PHF0W7Z35G46) — sxemada z.number() yozsangiz, test darhol ko'rsatadi.
</details>
Mashq 4: Qidiruv — API va UI
Toolshop'da GET /products/search?q=pliers API qidiruvi bor, UI'da esa search-query maydoni va search-submit tugmasi. UI natijalari API natijalari bilan bir xil ekanini tekshiradigan gibrid test yozing.
<details> <summary>💡 Javobni ko'rish</summary>
test("Toolshop: qidiruv — API va UI bir xil natija", async ({
page,
request,
}) => {
const apiResponse = await request.get(
"https://api.practicesoftwaretesting.com/products/search",
{
params: { q: "pliers" },
},
);
const { data } = await apiResponse.json();
const apiNames: string[] = data.map(
(product: { name: string }) => product.name,
);
await page.goto("https://practicesoftwaretesting.com");
await page.getByTestId("search-query").fill("pliers");
await page.getByTestId("search-submit").click();
await expect(page.getByTestId("product-name")).toHaveText(apiNames);
});
toHaveText(massiv) — elementlar soni va tartibi ham mos bo'lishini talab qiladi. Bizning tekshiruvimizda API 4 ta natija qaytardi va UI aynan shu 4 tasini ko'rsatdi.
</details>
Mashq 5: Bug report
8-bo'limdagi "majburiy maydonsiz POST → 500" holati uchun QA 8 formatida bug report yozing.
<details> <summary>💡 Javobni ko'rish</summary>
Sarlavha: POST /booking: firstname maydoni yuborilmasa 400 o'rniga 500 Internal Server Error qaytadi
- Muhit: restful-booker.herokuapp.com (public demo), 2026-10-09
- Qadamlar:
POST /booking, headerContent-Type: application/json- Body:
{"lastname":"Karimov"}(firstname, totalprice va boshqalar yo'q) - Kutilgan natija:
400 Bad Requestva qaysi maydon yetishmasligi haqida JSON xabar - Haqiqiy natija:
500 Internal Server Error, body:Internal Server Error - Severity: Major — mijoz xatosi server xatosi sifatida qaytadi; monitoring yolg'on alert beradi, mijoz nimani tuzatishni bilmaydi
- Priority: Medium
- Dalil: avtotest
tests/api/booking.spec.ts→ "majburiy maydonsiz POST 400 qaytarishi kerak" (test.failbilan belgilangan), curl buyrug'i ilova qilingan
</details>
Mashq 6: Gibrid testni qayerda qo'llaysiz?
DemoBozor'da quyidagi uch UI testni tezlashtirish uchun qaysi qadamlarni API'ga ko'chirasiz?
- "Promo-kod qo'llanganda checkout'da chegirma ko'rinadi"
- "Yetkazilgan buyurtmaga sharh yozish mumkin"
- "Admin foydalanuvchini bloklaydi, u tizimga kira olmaydi"
<details> <summary>💡 Javobni ko'rish</summary>
- API: login (token), savatchaga mahsulotlarni
POST /api/v1/cart/itemsbilan qo'shish. UI: checkout sahifasini ochib, promo-kodni UI orqali kiritish va chegirma qatorini tekshirish (aynan shu tekshirilyapti). - API: buyurtma yaratish va uni
DELIVEREDholatiga o'tkazish (test muhitidagi admin/servis endpoint orqali). UI: "Buyurtmalarim" → "Sharh yozish" tugmasi ko'rinishi va sharh formasi. - API: yangi foydalanuvchi yaratish va admin token bilan bloklash. UI: shu foydalanuvchi bilan login urinish → xato xabari. (Bloklashning admin UI'si alohida testda.)
Qoida: sinovdagi xatti-harakat UI'da qoladi, tayyorgarlik va tozalash API'ga o'tadi. Bu yerdagi endpoint'lar xayoliy DemoBozor API'siga tegishli.
</details>
O'zingizni tekshiring {#checklist}
- [ ]
requestfixture,newContext()vapage.requestfarqini bilaman - [ ] Zod sxemasi yozib,
parse/safeParsevaz.inferishlata olaman - [ ] API klient (
BookingClient) va worker-scoped token fixture yoza olaman - [ ] Test ma'lumotini fixture'da yaratib, teardown'da o'chira olaman
- [ ] Quirk va bug farqini bilaman,
test.fail()ni to'g'ri qo'llayman - [ ] Gibrid testda nimani API'ga, nimani UI'ga qoldirishni tanlay olaman
Xulosa {#xulosa}
- API testlar piramidaning o'rta qavati: brauzersiz, tez va barqaror; biznes qoidalari, kontrakt va ruxsatlarni tekshiradi.
- Playwright
requestfixture'i project'ningbaseURLvaextraHTTPHeadersini oladi;await expect(response).toBeOK()yiqilganda javob body'sini ham ko'rsatadi. - Zod sxemasi — runtime tekshiruv + TypeScript turi; xato xabarlari maydon nomi bilan keladi. OpenAPI bor jamoada JSON Schema + Ajv ham yaxshi.
- API klient UI'dagi Page Object'ning ekvivalenti: assertion qilmaydi,
APIResponseqaytaradi — negativ testlar ham yoziladi. - Fixture'lar: worker-scoped token (bir marta), test-scoped ma'lumot (yaratish →
use→ o'chirish). Har test o'z ma'lumotini o'zi yaratadi. - Quirk'larni hujjatlashtiring, haqiqiy bug'larni esa to'g'ri kutilgan natija bilan yozib
test.fail('BUG-...')bilan belgilang. - Gibrid testlar: tayyorgarlik API'da, tekshiruv UI'da — tez va barqaror UI suite'ning siri.
- Keyingi dars: QA 21: CI/CD'da testlar — GitHub Actions, hisobotlar, sharding, flaky testlar va quality gate
Manbalar {#manbalar}
- Playwright — API testing
- Playwright — APIRequestContext
- Playwright — APIResponse assertions (toBeOK)
- Playwright — Fixtures (worker scope)
- Zod — dokumentatsiya
- Ajv JSON schema validator
- Faker — dokumentatsiya
- Restful-booker API docs
- Practice Software Testing (Toolshop) — GitHub
- Newman — Postman CLI runner
- REST Assured — Kotlin extension module
- Requests — Python HTTP kutubxonasi