# 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](#nega-api)
2. [Playwright request: asoslar](#request-asoslari)
3. [Loyiha tuzilmasi va config](#tuzilma)
4. [Zod bilan schema validatsiya](#zod)
5. [BookingClient: API qatlami](#api-client)
6. [Fixture'lar: token, client va test data hayot sikli](#fixtures)
7. [To'liq CRUD suite](#crud)
8. [Negativ testlar va ma'lum bug'lar](#negativ)
9. [API + UI gibrid testlar](#gibrid)
10. [Boshqa stack'lar: Newman, REST Assured, pytest](#boshqa-stacklar)
11. [Amaliy mashqlar](#mashqlar)
12. [Xulosa](#xulosa)
13. [Manbalar](#manbalar)

---

## 1. Nega API'ni avtomatlashtiramiz {#nega-api}

[QA 10](/ios-lessons/qa-api-postman) va [QA 11](/ios-lessons/qa-api-advanced)da API'ni qo'lda — curl va Postman bilan — testladik va test dizaynini o'rgandik. Endi o'sha bilimni kodga aylantiramiz.

Test piramidasida ([QA 4](/ios-lessons/qa-levels-types)) 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](/ios-lessons/qa-security));
- **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 `extraHTTPHeaders`ini 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:

```typescript
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

```typescript
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

```typescript
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 19](/ios-lessons/qa-playwright-advanced)dagi loyihaga API qatlamini qo'shamiz:

```text
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):

```typescript
{
  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 `setup`ga bog'liq emas.

Paketlar:

```bash
npm i -D @playwright/test zod @faker-js/faker dotenv
```

Faqat API testlarni ishga tushirish:

```bash
npx playwright test --project=api
```

---

## 4. Zod bilan schema validatsiya {#zod}

"Status 200" — bu faqat birinchi qadam. Backend `totalprice`ni 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 11](/ios-lessons/qa-api-advanced)da JSON Schema'ni ko'rdik. TypeScript loyihada eng qulay vosita — **Zod**: sxema bir vaqtda ham runtime tekshiruv, ham TypeScript turi.

```typescript
// 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):

```typescript
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):

```text
[
  '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()`:

```typescript
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:

```typescript
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:

```text
[
  '/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:

```typescript
// 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.infer`dan 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

```typescript
// 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 })`. `lastname`ga `Date.now()` qo'shilgani — parallel testlar (va boshqa odamlarning public demo'dagi ma'lumotlari) bilan to'qnashmaslik uchun.

### 6.2. Fixture'lar

```typescript
// 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:

```text
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 `request`ni 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}

```typescript
// 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:

```bash
npx playwright test --project=api
```

```text
  ✓  [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:

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 `201`ni 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?".

```typescript
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. `firstname`siz 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 11](/ios-lessons/qa-api-advanced)dagi 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](/lessons/images/qa/qa_20_api_ui_hybrid.svg)

Toolshop'da buni ko'rsatamiz. Uning SPA'si JWT token'ni `localStorage`dagi `auth-token` kalitida saqlaydi — buni brauzerda login qilib, `localStorage` tarkibini ko'rib aniqladik (DevTools → Application bo'limida ham ko'rinadi, [QA 9](/ios-lessons/qa-web-http-devtools)). Demo foydalanuvchi `customer@practicesoftwaretesting.com` / `welcome01` (Jane Doe) loyihaning ochiq README'sida berilgan.

### 9.1. API orqali login → UI hisob sahifasi

```typescript
// 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?

```typescript
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 **cookie**da saqlasa (localStorage emas), `page.request` ishlating: u sahifa kontekstining cookie'larini ishlatadi va javobdagi `Set-Cookie`lar 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:

```bash
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](/ios-lessons/qa-ci-cd)).

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

```kotlin
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)

```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, `yield`dan 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>

```typescript
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>

```typescript
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](/ios-lessons/qa-security)).

</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>

```typescript
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>

```typescript
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](/ios-lessons/qa-bug-reports) 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:**
  1. `POST /booking`, header `Content-Type: application/json`
  2. 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 `extraHTTPHeaders`ini 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](/ios-lessons/qa-ci-cd)

## Manbalar {#manbalar}

1. [Playwright — API testing](https://playwright.dev/docs/api-testing)
2. [Playwright — APIRequestContext](https://playwright.dev/docs/api/class-apirequestcontext)
3. [Playwright — APIResponse assertions (toBeOK)](https://playwright.dev/docs/api/class-apiresponseassertions)
4. [Playwright — Fixtures (worker scope)](https://playwright.dev/docs/test-fixtures)
5. [Zod — dokumentatsiya](https://zod.dev/)
6. [Ajv JSON schema validator](https://ajv.js.org/)
7. [Faker — dokumentatsiya](https://fakerjs.dev/)
8. [Restful-booker API docs](https://restful-booker.herokuapp.com/apidoc/index.html)
9. [Practice Software Testing (Toolshop) — GitHub](https://github.com/testsmith-io/practice-software-testing)
10. [Newman — Postman CLI runner](https://github.com/postmanlabs/newman)
11. [REST Assured — Kotlin extension module](https://github.com/rest-assured/rest-assured/wiki/Kotlin)
12. [Requests — Python HTTP kutubxonasi](https://requests.readthedocs.io/)
