Azimjon Abdurasulov

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 request fixture'i bilan brauzersiz API testlar yozasiz;
  • javoblarni Zod sxemalari bilan tekshirasiz va TypeScript turlarini sxemadan olasiz;
  • BookingClient API 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

  1. Nega API'ni avtomatlashtiramiz
  2. Playwright request: asoslar
  3. Loyiha tuzilmasi va config
  4. Zod bilan schema validatsiya
  5. BookingClient: API qatlami
  6. Fixture'lar: token, client va test data hayot sikli
  7. To'liq CRUD suite
  8. Negativ testlar va ma'lum bug'lar
  9. API + UI gibrid testlar
  10. Boshqa stack'lar: Newman, REST Assured, pytest
  11. Amaliy mashqlar
  12. Xulosa
  13. 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:

XususiyatUI (E2E) testAPI test
Brauzer kerakmihayo'q
Tezliksoniyalar (sahifa yuklanishi, render)odatda yuzlab millisekundlar
Barqarorlikanimatsiya, render, locator'lar ta'sir qiladifaqat tarmoq va backend
Nimani tekshiradifoydalanuvchi oqimi, UIbiznes qoidalari, kontrakt, ruxsatlar, xato holatlari
Xato joyini topishqiyinroq (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:

UsulNimaQachon
request fixturehar test uchun yangi kontekst, project'ning baseURL va extraHTTPHeadersini oladioddiy API testlar
playwright.request.newContext({...})qo'lda yaratiladigan kontekst, dispose() qilish kerakworker fixture'lar, beforeAll
page.requestsahifa bilan cookie'larni bo'lishadibrauzer 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
});
OpsiyaMa'nosi
databody: obyekt bo'lsa JSON qilib yuboriladi, string/Buffer — o'zicha
formapplication/x-www-form-urlencoded forma
multipartfayl yuklash (multipart/form-data)
paramsquery parametrlar
headersshu so'rov uchun header'lar
failOnStatusCodetrue 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'rniga await 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

MetodMos kelmasaQachon
Schema.parse(data)xato tashlaydi (test shu joyda yiqiladi)testlarda — eng ko'p ishlatiladi
Schema.safeParse(data){ success: false, error } qaytaradixatolarni 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 strictObject ishlatsangiz, 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"'
]
ZodJSON Schema + Ajv
Qayerdan keladiTypeScript kodda yoziladiOpenAPI/JSON fayldan
TypeScript turiz.infer bilan bepulalohida generatsiya kerak
Tillararofaqat JS/TSistalgan til (Java, Python, Postman ham)
QachonTS-first jamoa, tez boshlashspetsifikatsiya "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 APIResponse qaytaradi, assertion qilmaydi.** Negativ testlar 403 yoki 404 ni tekshirishi kerak — klient ichida toBeOK() bo'lsa, ularni yozib bo'lmasdi.
  • Token konstruktorda. Token'siz klient (new BookingClient(request)) — tayyor "anonim foydalanuvchi" negativ testlar uchun.
  • Turlar sxemadan. Booking turi z.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, /auth 4 marta chaqiriladi, 100 marta emas. Worker fixture requestni ishlata olmaydi (u test doirasida), shuning uchun playwright.request.newContext() bilan o'z kontekstini ochib, dispose() qiladi.
  • newBooking — teardown kafolati: test yiqilsa ham use()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'rovHaqiqiy javobREST bo'yicha kutilganIzoh
POST /booking200 + { bookingid, booking }201 Createdresurs yaratildi, lekin 200
DELETE /booking/:id201 Created200 yoki 204eng mashhur quirk
DELETE qayta (o'chirilganni)405 Method Not Allowed404
GET /ping201200
POST /auth noto'g'ri parol200 + {"reason":"Bad credentials"}401

Testda qaysi kodni yozish kerak? Qoida:

  1. 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.
  2. 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:

HolatNatija
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:

  • **addInitScript sahifadagi har qanday skriptdan oldin** ishlaydi. Agar avval goto, keyin localStorage.setItem qilsangiz, 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/orders orqali DELIVERED holatigacha 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.

StackTilKuchli tomoniQachon tanlanadi
Playwright requestTS/JSUI testlar bilan bitta framework, fixture'lar, gibrid testlarweb jamoa, E2E ham Playwright'da
Postman + NewmanJS (skriptlar)kod yozmasdan boshlash, kolleksiya = hujjatmanual QA'dan avtomatlashtirishga birinchi qadam
REST AssuredJava/KotlinJVM ekotizimi, Spring backend bilan bir repobackend Java/Kotlin
pytest + requestsPythonsodda sintaksis, kuchli fixture'larPython 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, header Content-Type: application/json
  • Body: {"lastname":"Karimov"} (firstname, totalprice va boshqalar yo'q)
  • Kutilgan natija: 400 Bad Request va 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.fail bilan 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?

  1. "Promo-kod qo'llanganda checkout'da chegirma ko'rinadi"
  2. "Yetkazilgan buyurtmaga sharh yozish mumkin"
  3. "Admin foydalanuvchini bloklaydi, u tizimga kira olmaydi"

<details> <summary>💡 Javobni ko'rish</summary>

  1. API: login (token), savatchaga mahsulotlarni POST /api/v1/cart/items bilan qo'shish. UI: checkout sahifasini ochib, promo-kodni UI orqali kiritish va chegirma qatorini tekshirish (aynan shu tekshirilyapti).
  2. API: buyurtma yaratish va uni DELIVERED holatiga o'tkazish (test muhitidagi admin/servis endpoint orqali). UI: "Buyurtmalarim" → "Sharh yozish" tugmasi ko'rinishi va sharh formasi.
  3. 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}

  • [ ] request fixture, newContext() va page.request farqini bilaman
  • [ ] Zod sxemasi yozib, parse/safeParse va z.infer ishlata 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 request fixture'i project'ning baseURL va extraHTTPHeadersini 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, APIResponse qaytaradi — 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}

  1. Playwright — API testing
  2. Playwright — APIRequestContext
  3. Playwright — APIResponse assertions (toBeOK)
  4. Playwright — Fixtures (worker scope)
  5. Zod — dokumentatsiya
  6. Ajv JSON schema validator
  7. Faker — dokumentatsiya
  8. Restful-booker API docs
  9. Practice Software Testing (Toolshop) — GitHub
  10. Newman — Postman CLI runner
  11. REST Assured — Kotlin extension module
  12. Requests — Python HTTP kutubxonasi

Markdown manba

iOS Lessons