IlmHamroh
JavaScript Full-stack/27-qism. Ma'lumotlar bazasi chuqur: Postgres, ORM, Mongo, Redis65/65-dars19 daqiqa
Mundarija (25)

Repository va SQL'ni testlash: cheklovlar, trigger'lar va pg-mem chegarasi

Qisqacha: Ombor (repository) kodining eng muhim qismi — SQL va bazaning o'zi: cheklovlar, trigger'lar, RLS. Ularni faqat haqiqiy Postgres'da sinash mumkin. Kanon in-memory taqlid pg-mem ni sinadi: vazifalar migratsiyasining 15 faylidan birortasi ham o'tmadi (0/15). SQLite bilan almashtirish ham xavfli — boshqa SQL dialekti. Haqiqiy bazadagi testlar esa haqiqiy xatolarni topadi: Prisma contains % belgisini ekranlamasligi aynan shunday testda ochildi. Bu darsda holat 65 yakunlanadi: 112 test (111 o'tdi, 1 SKIP), Testcontainers 10/10, qamrov 98.82 %.

Bu darsda

  • Nega ombor testlari haqiqiy bazada bo'lishi kerakligini o'lchov bilan tushuntirasiz.
  • CHECK, EXCLUDE, trigger, tashqi kalit va RLS uchun test yozasiz.
  • Shartnoma testlari bitta interfeysning bir nechta amalga oshirilishini qanday tekshirishini bilasiz.
  • Migratsiyaning o'zini testlashni bilasiz.
  • vazifalar-api holat 65 ni yakunlab, commit qilasiz.

Oldin bilishingiz kerak: Factory va fixture'lar, Testcontainers, Prisma prodakshnda, Mock, stub, spy va fake.

Oldingi darsda fabrikalarni yozdik. Endi ular bilan bazaning eng "qimmat" xulqini — cheklov va trigger'larni — tekshiramiz. Bu 27-qismning oxirgi darsi.

1. Nega bu kerak?

vazifalar bazasida ko'p qoida koddan tashqarida, bazaning o'zida yashaydi:

  • vazifa matni 100 belgigacha va chetida bo'shliqsiz (CHECK);
  • bir egasining faol vazifalari orasida matn takrorlanmaydi, registrga befarq (EXCLUDE, 014);
  • tartib, versiya, teglar va izohlar_soni — trigger'lar hisoblaydi;
  • foydalanuvchi o'chsa — vazifalari ham (FK CASCADE);
  • ilova roli faqat o'z egasining qatorlarini ko'radi (RLS, 105).

API bu qoidalarga tayanadi: 409 takror-matn aynan EXCLUDE xatosidan chiqadi. Agar migratsiyada bitta qoida tushib qolsa yoki noto'g'ri yozilsa — API "ishlaydi", lekin noto'g'ri ma'lumotni qabul qiladi. Bunday xatoni faqat haqiqiy bazada ishlaydigan test ushlaydi.

O'xshatish: avtomobil tormozini sinash. Rasm yoki maketda emas — haqiqiy mashinada, haqiqiy yo'lda.

2. In-memory taqlidlar

2.1 pg-mem

pg-mem — Postgres'ni JavaScript'da taqlid qiladigan kutubxona: baza xotirada, Docker kerak emas, juda tez. Jozibali. Kanon uni vazifalar ning haqiqiy migratsiyasi bilan sinadi (pg-mem 3.0.14, 0_init, 15 fayl):

text
--- 5) pg-mem (in-memory Postgres taqlidi) kanon migratsiyasini ko'taradimi (0_init, 15 fayl)?
  001-jadvallar.sql: XATO — operator does not exist: text ~ text
  002-tashqi-kalitlar.sql: XATO — relation "vazifalar" does not exist
  004-muddat-ustuvorlik.sql: XATO — relation "vazifalar" does not exist
  101-sozlamalar-jsonb.sql: XATO — relation "foydalanuvchilar" does not exist
pg-mem: 0/15 fayl

(Chiqishdan 4 qator ko'rsatildi.) Birinchi fayl yiqildi: email CHECK'idagi ~ (regex) operatori pg-mem'da yo'q. Keyin zanjir: jadval yaratilmadi — keyingi fayllar "jadval yo'q" deydi. Trigger, funksiya, EXCLUDE, RLS fayllari (006, 009, 010, 013, 014, 104, 105, 106) esa "so'rovni tahlil qilib bo'lmadi" (query failed to parse) xatosini berdi — pg-mem bu sintaksisni umuman tushunmaydi.

Natija: 0/15. Bu sxema uchun pg-mem yaroqsiz. Muammo pg-mem sifatida emas — u Postgres'ning kichik qismini taqlid qiladi. Oddiy CRUD'li sxemada ishlaydi; lekin ombor testining eng qimmatli qismi aynan "oddiy bo'lmagan" xulq.

2.2 SQLite bilan almashtirish

Ikkinchi mashhur yo'l — testda Postgres o'rniga SQLite (fayl yoki xotirada). ORM ikkalasini ham "qo'llaydi". Lekin bu boshqa baza (kanonda sinalmagan, hujjatlar bo'yicha farqlar):

  • tiplar erkin: INTEGER ustunga matn yozilishi mumkin (STRICT jadvalsiz);
  • JSONB, massivlar, EXCLUDE, RLS, ILIKE, PL/pgSQL trigger'lari, RETURNING ning ba'zi shakllari — yo'q yoki boshqacha;
  • tranzaksiya va qulf modeli butunlay boshqa (bitta yozuvchi);
  • collation va satr solishtirish farqi — saralash testlari boshqa natija beradi.

Natija: testlar SQLite'da o'tadi, production Postgres'da xato. Bunday test xavfsizlik hissini beradi, lekin himoya qilmaydi.

2.3 Mock qachon to'g'ri

Mock, stub, spy va fake darsidagi qoida o'zgarmaydi: xizmat qatlamini testlashda ombor mock yoki xotira ombori bilan almashtiriladi — u yerda bazaning xulqi muhim emas. vazifalar-api da XotiraOmbori aynan shunday ishlatiladi. Lekin omborning o'zini testlashda — haqiqiy baza.

Nimani testlaysiz Nima bilan
Xizmat mantiqi, HTTP marshrutlar xotira ombori / mock
Ombor, SQL, ORM so'rovlari haqiqiy Postgres
Cheklov, trigger, RLS, migratsiya haqiqiy Postgres (aynan production versiyasi)

3. Cheklovlar va trigger'larni testlash

3.1 SQL darajasida

Avval xulqni psql'da ko'ramiz. Biz ishga tushirdik (PostgreSQL 17.6, holat 65 sxemasi va namuna ma'lumoti). \set VERBOSITY terse — xato xabari bitta qatorda (DETAIL'da vaqt kabi o'zgaruvchan qiymatlar bor), LAST_ERROR_SQLSTATE — oxirgi xato kodi:

sql
\set VERBOSITY terse
INSERT INTO vazifalar (egasi_id, matn) VALUES (1, repeat('x', 101));
\echo :LAST_ERROR_SQLSTATE
INSERT INTO vazifalar (egasi_id, matn) VALUES (1, ' a ');
\echo :LAST_ERROR_SQLSTATE
text
ERROR:  new row for relation "vazifalar" violates check constraint "vazifalar_matn_check"
23514
ERROR:  new row for relation "vazifalar" violates check constraint "vazifalar_matn_check"
23514

Ikkala holat ham — 23514 (check_violation), cheklov nomi vazifalar_matn_check. Testda aynan kod va cheklov nomi tekshiriladi, xabar matni emas: matn Postgres versiyasi va tiliga qarab o'zgarishi mumkin.

sql
\set VERBOSITY terse
INSERT INTO vazifalar (egasi_id, matn)
  VALUES (1, 'JAVASCRIPT KURSINING 26-QISMINI BOSHLASH #O''QISH');
\echo :LAST_ERROR_SQLSTATE
INSERT INTO vazifalar (egasi_id, matn)
  VALUES (2, 'JavaScript kursining 26-qismini boshlash #o''qish')
  RETURNING egasi_id, tartib, versiya;
DELETE FROM vazifalar
  WHERE egasi_id = 2 AND matn LIKE 'JavaScript kursining%';
text
ERROR:  conflicting key value violates exclusion constraint "vazifalar_egasi_id_matn_excl"
23P01
 egasi_id | tartib | versiya 
----------+--------+---------
        2 |      7 |       1
(1 row)

INSERT 0 1
DELETE 1
  • 1-egada shu matnli faol vazifa bor — katta harf bilan yozilgan nusxa ham rad etildi: 23P01 (exclusion_violation). Registrga befarqlik ishlayapti.
  • 2-egada xuddi shu matn — ruxsat. Trigger'lar ishladi: tartib 7 (egasining ro'yxati oxiriga), versiya 1.
  • Oxirgi DELETE — tajribani tozaladik (keyingi bloklar uchun).

3.2 RLS

sql
BEGIN;
SET LOCAL ROLE vazifalar_ilova;
SELECT set_config('ilova.foydalanuvchi_id', '1', true);
SELECT count(*) AS korinadi FROM vazifalar;
ROLLBACK;
SELECT count(*) AS hammasi FROM vazifalar;
text
BEGIN
SET
 set_config 
------------
 1
(1 row)

 korinadi 
----------
       13
(1 row)

ROLLBACK
 hammasi 
---------
      48
(1 row)

Ilova roli bilan 1-foydalanuvchi faqat o'zining 13 ta vazifasini ko'rdi; superuser — hammasini (48). Row Level Security darsidagi siyosat ishlayapti. set_config(…, true) — SET LOCAL ning funksiya shakli: qiymatni parametr sifatida berish mumkin. Kanon testida bu yer `SET LOCAL ilova.foydalanuvchi_id = '${String(a.id)}'` — satr ichiga son qo'yish. Son bo'lgani uchun xavfsiz, lekin qoida sifatida set_config va $1 parametri afzal (Parametrli so'rovlar va SQL injection).

3.3 Node testida: kod va cheklov nomi

Konteyner testlaridagi BT-13 bloki — fabrika bilan (kanon kodi, qatorlar bo'lingan):

ts
describe("BT-13: SQL va cheklovlar "
  + "(in-memory taqlid bera olmaydigan xulq)", () => {
  beforeEach(async () => { db = await toza(); fabrikaniBoshla(); });

  test("CHECK: 101 belgili matn → 23514; "
    + "chetida bo'shliq → 23514",
    async () => {
      const f = await foydalanuvchi(db);
      await assert.rejects(
        vazifa(db, { egasi_id: f.id, matn: "x".repeat(101) }),
        { code: "23514", constraint: "vazifalar_matn_check" });
      await assert.rejects(
        vazifa(db, { egasi_id: f.id, matn: " a " }),
        { code: "23514" });
    });

  test("014 EXCLUDE: faol vazifalar orasida registrga befarq takror"
    + " → 23P01; o'chirilgandan keyin — mumkin", async () => {
    const f = await foydalanuvchi(db);
    const v = await vazifa(db, { egasi_id: f.id, matn: "Non olish" });
    await assert.rejects(
      vazifa(db, { egasi_id: f.id, matn: "NON OLISH" }),
      { code: "23P01", constraint: "vazifalar_egasi_id_matn_excl" });
    await db.query(
      "UPDATE vazifalar SET ochirilgan_vaqt = now() WHERE id = $1",
      [v.id]);
    assert.equal(
      (await vazifa(db, { egasi_id: f.id, matn: "NON OLISH" })).matn,
      "NON OLISH");
    // boshqa foydalanuvchida — mumkin
    assert.ok(await vazifa(db, { matn: "Non olish" }));
  });

  test("trigger'lar: tartib oxiriga, versiya +1, teglar matndan, "
    + "izohlar soni", async () => {
    const f = await foydalanuvchi(db);
    const a = await vazifa(db, { egasi_id: f.id, matn: "Bir #ish" });
    const b = await vazifa(db,
      { egasi_id: f.id, matn: "Ikki #uy #Ish" });
    assert.deepEqual([a.tartib, b.tartib], [1, 2]);
    const { rows: [u] } = await db.query<{ versiya: number }>(
      "UPDATE vazifalar SET bajarildi = true, "
      + "bajarilgan_vaqt = now() WHERE id = $1 RETURNING versiya",
      [a.id]);
    assert.equal(u?.versiya, 2);
    const { rows: teglar } =
      await db.query<{ nom: string; n: number }>(
      "SELECT t.nom, count(*)::int AS n FROM teglar t "
      + "JOIN vazifa_teglari vt ON vt.teg_id = t.id "
      + "GROUP BY 1 ORDER BY 1");
    assert.deepEqual(teglar,
      [{ nom: "#ish", n: 2 }, { nom: "#uy", n: 1 }]);
    await db.query("INSERT INTO izohlar "
      + "(vazifa_id, muallif_id, matn) "
      + "VALUES ($1, $2, 'a'), ($1, $2, 'b')", [a.id, f.id]);
    const { rows: [s] } = await db.query<{ izohlar_soni: number }>(
      "SELECT izohlar_soni FROM vazifalar WHERE id = $1", [a.id]);
    assert.equal(s?.izohlar_soni, 2);
  });

  test("FK CASCADE: foydalanuvchi o'chsa vazifalari ham; RLS: ilova "
    + "roli boshqa odamning vazifasini ko'rmaydi", async () => {
    const a = await foydalanuvchi(db);
    const b = await foydalanuvchi(db);
    await vazifa(db, { egasi_id: a.id });
    await vazifa(db, { egasi_id: b.id });
    const c = await db.connect();
    try {
      await c.query("BEGIN");
      await c.query("SET LOCAL ROLE vazifalar_ilova");
      await c.query(
        `SET LOCAL ilova.foydalanuvchi_id = '${String(a.id)}'`);
      const { rows: [n] } = await c.query<{ n: number }>(
        "SELECT count(*)::int AS n FROM vazifalar");
      assert.equal(n?.n, 1);
      await c.query("ROLLBACK");
    } finally { c.release(); }
    await db.query("DELETE FROM foydalanuvchilar WHERE id = $1",
      [a.id]);
    const { rows: [q] } = await db.query<{ n: number }>(
      "SELECT count(*)::int AS n FROM vazifalar");
    assert.equal(q?.n, 1);
  });
});

Har testda nima muhim:

  • assert.rejects(promise, { code, constraint }) — xato obyektining maydonlari tekshiriladi. pg drayveri Postgres xatosining code va constraint maydonlarini beradi (node-postgres (pg) asoslari).
  • Fabrika faqat muhim maydonni oldi (matn), egasini o'zi yaratdi. "Boshqa foydalanuvchida — mumkin" testi — vazifa(db, { matn }): yangi ega avtomatik.
  • Trigger testi natijani tekshiradi (tartib, versiya, teglar soni), trigger qanday yozilganini emas. Ertaga trigger qayta yozilsa ham test o'zgarmaydi.
  • RLS testi — bitta ulanishda (db.connect()), tranzaksiya ichida, ROLLBACK bilan. SET LOCAL faqat shu tranzaksiyaga ta'sir qiladi (Test DB strategiyalari dagi hovuz tuzog'i).
  • Har try — finally { c.release() }. Ulanish hovuzga qaytmasa, keyingi testdagi toza() ichidagi pool.end() uni kutib qoladi va test osilib qoladi (pg-pool end() hamma ulanish qaytishini kutadi).

Tekshirib ko'ring: Nega testda xato matni emas, code: "23514" va cheklov nomi tekshiriladi?

Javob

Matn Postgres versiyasi, lc_messages tili va tarjimasiga bog'liq — boshqa serverda "yangi qator … cheklovni buzadi" chiqishi mumkin va test sababsiz yiqiladi. SQLSTATE kodi (23514) standart va o'zgarmaydi. Cheklov nomi esa qaysi qoida ishlaganini aniq aytadi: matnli CHECK'mi yoki boshqasimi. API ham xuddi shu ikki maydonga qarab 409 yoki 400 tanlaydi.

4. Shartnoma testlari: bitta interfeys, ko'p amalga oshirish

VazifaOmbori interfeysining beshta amalga oshirishi bor: xotira, fayl, PgOmbori, PrismaOmbori va KeshliOmbor. Ularning hammasi bitta test to'plamidan o'tadi — omborShartnomasi(nom, yarat) (Backend test strategiyasi darsida yozganmiz):

ts
omborShartnomasi("postgres", async () => {
  await tozala(pool);
  return new PgOmbori(pool, 1);
});
omborShartnomasi("xotira + Redis kesh", () => Promise.resolve(
  new KeshliOmbor(new XotiraOmbori(), redis, ++egasi, { ttlS: 60 })));
omborShartnomasi("postgres (Testcontainers)", async () => {
  const p = await toza();
  await p.query("INSERT INTO foydalanuvchilar (email, toliq_ism) "
    + "VALUES ('test@example.com', 'Test')");
  return new PgOmbori(p, 1);
});

Har amalga oshirish "bo'sh ombor"ni o'zicha tayyorlaydi (TRUNCATE, yangi kesh egasi, snapshot), testlar esa bir xil. Shartnoma xotira omborida o'tib, Postgres'da yiqilsa — farq topildi: yoki Postgres ombori xato, yoki shartnoma noaniq.

Shartnomadan tashqari har ombor o'ziga xos testlarga ham ega ("Postgres'ga xos": yumshoq o'chirish, trigger'dan kelgan teglar, matn bo'yicha saralashda COLLATE "C").

4.1 Topilma: Prisma contains va %

PrismaOmbori ning o'ziga xos testi "qidiruv — oddiy matn, maxsus belgi emas"ni tekshiradi (kanon kodi):

ts
await a.yarat({ matn: "Azizniki 100% #ish" });
await a.yarat({ matn: "Boshqasi" });
const r = (qidiruv: string) => a.royxat({
  filtr: { qidiruv }, saralash: [], limit: 10,
  sahifa: { tur: "kursor" },
});
assert.deepEqual((await r("%")).vazifalar.map((v) => v.id), [1]);
assert.deepEqual((await r("_")).vazifalar.map((v) => v.id), []);
assert.deepEqual((await r("' OR 1=1 --")).vazifalar.map((v) => v.id),
  []);

Holat 20 da bu test yiqildi: "%" qidiruvi ikkala vazifani qaytardi. Sabab: Prisma 7.10 contains + mode: "insensitive" ni ILIKE '%…%' ga aylantiradi va qiymatdagi % va _ ni ekranlamaydi. Foydalanuvchi % yozsa — "hamma narsa". SQL injection emas (qiymat parametr), lekin qidiruv noto'g'ri. PgOmbori esa strpos ishlatadi — unda muammo yo'q edi.

Xulqni psql'da ko'ramiz. Biz ishga tushirdik (PostgreSQL 17.6, holat 65 namunasi; qidiruv qiymati % parametr sifatida kelgandek):

sql
SELECT count(*) AS ilike_ekransiz
  FROM vazifalar WHERE matn ILIKE '%' || '%' || '%';
SELECT count(*) AS ilike_ekranli
  FROM vazifalar WHERE matn ILIKE '%' || '\%' || '%';
SELECT count(*) AS strpos_bilan
  FROM vazifalar WHERE strpos(lower(matn), lower('%')) > 0;
text
 ilike_ekransiz 
----------------
             48
(1 row)

 ilike_ekranli 
---------------
             0
(1 row)

 strpos_bilan 
--------------
            0
(1 row)

Ekransiz — 48 ta, hamma vazifa. Ekranlangan \% (Postgres'da LIKE uchun sukut ekran belgisi \) — 0, chunki namunada % belgili vazifa yo'q. strpos — LIKE maxsus belgilarini umuman bilmaydi. Kanon PrismaOmbori ga ekranlashni qo'shdi:

ts
// contains + insensitive → ILIKE '%…%'. Prisma 7 qiymatdagi % va _ ni
// EKRANLAMAYDI — LIKE maxsus belgilarini o'zimiz ekranlaymiz
if (filtr.qidiruv !== undefined) s.push({ matn: {
  contains: filtr.qidiruv.replace(/[\\%_]/g, "\\$&"),
  mode: "insensitive",
} });

Bu topilma darsning asosiy g'oyasini ko'rsatadi: ORM — abstraksiya, va u bazaga qanday SQL yuborishini faqat haqiqiy bazadagi test ko'rsatadi. Mock'langan Prisma bilan bu test o'tib ketardi.

5. Migratsiyani testlash

Migratsiyaning o'zi ham kod — u ham sinaladi. Kanon yakuniy tekshiruvi uch xil tekshiradi:

  1. Noldan qo'llanadimi — Testcontainers'da prisma migrate deploy toza bazaga (Testcontainers); 106 dagi FOR ROLE postgres xatosi aynan shu yerda topilgan.
  2. Natija kutilgan sxemami — konteyner testi: 0_init qo'llangan, 5 trigger, 1 materialized view, 5 RLS'li jadval.
  3. Migratsiyalar va qo'lda yozilgan sql/ bir xil sxema beradimi — ikkala yo'l bilan qurilgan bazalarning pg_dump --schema-only chiqishi solishtiriladi:
text
--- migratsiyalar va sql/ sxemasi farqi (pg_dump, _prisma_migrations'siz): 9 qator (9 — _prisma_migrations ustunlari)

Farq faqat Prisma'ning o'z xizmat jadvali — boshqa hech narsa. Shuningdek prisma migrate status — "Database schema is up to date!" (Prisma Migrate chuqur). Murakkab migratsiyalarda (expand-contract, Zero-downtime: expand-contract) ma'lumotli test ham yoziladi: eski sxemaga namuna ma'lumot → migratsiya → ma'lumot to'g'ri ko'chganini tekshirish.

6. Loyiha qadami: holat 65 yakuni va commit

6.1 SQL testlari bloki

test/konteyner.test.ts ga «Node testida: kod va cheklov nomi» bo'limidagi BT-13 blokini qo'shing — BT-11 blokidan keyin, shartnoma testlaridan oldin. Endi fayl to'liq: migratsiya testi, 4 ta SQL testi va 5 ta shartnoma testi.

6.2 Yakuniy tekshiruv

bash
npm run tip
npm run lint
npm test
TESTCONTAINERS=1 npm test
npm run test:qamrov

Kanon natijalari (kanon o'lchovi, IlmHamroh, 2026-10):

text
--- npm run tip
exit 0 (19906 ms)
--- npm run lint
exit 0 (24657 ms)
--- npm run test
ℹ tests 112
ℹ pass 111
ℹ fail 0
ℹ skipped 1
exit 0 (14021 ms)
--- faqat Postgres testlari: ℹ tests 18 ℹ pass 18 ℹ fail 0 ℹ skipped 0 
--- faqat Prisma testlari: ℹ tests 11 ℹ pass 11 ℹ fail 0 ℹ skipped 0 

(Chiqishdan tanlangan qatorlar.) 112 testdan 1 tasi SKIP — konteyner testining "o'chiq" o'rinbosari (TESTCONTAINERS=1 siz). Testcontainers rejimida — 10/10, qoldiq konteyner 0. Qamrov: 98.82 % satr, 87.10 % shox (branch), 93.58 % funksiya — test:qamrov chegaralari (95/85/90) o'tdi (Coverage va test sifati).

vazifalar-api testlari: 25-qism oxiri va holat 65
  • Holat 51 (25-qism)67 ta
  • Holat 65 (27-qism)+ Testcontainers 10 alohida112 ta

Manba: IlmHamroh kanon tekshiruvlari, 2026-10: holat 51 va holat 65 yakuniy tekshiruvi, node:test, Node 24.21.0

Prod bog'liqliklar — 10 ta: @prisma/adapter-pg, @prisma/client, cors, express, helmet, pg, pino, pino-http, redis, zod. Dev'ga 27-qismda 4 ta qo'shildi: prisma, @faker-js/faker, testcontainers, @testcontainers/postgresql.

6.3 Commit

Hamma tekshiruv o'tgach — holat 65 commit'i (telefon uchun ikki qismda):

bash
git add -A
M='feat: Redis kesh (KeshliOmbor, avlod kaliti) va '
M+='Testcontainers testlari'
git commit -m "$M"

Kanon xabarida 106 tuzatishi (fix(db)) ham bor — sizda kerak emas: Rollar va huquqlar darsidagi 106-rollar.sql sizga allaqachon FOR ROLE CURRENT_USER bilan kelgan. Commit xabari faqat haqiqatan o'zgargan narsani aytadi.

Bu commit — 27-qismdagi vazifalar-api ning yakuni: Prisma 7 ombori, migratsiyalar va baseline, expand-contract, seed, deploy tekshiruvi, Redis kesh, Testcontainers va fabrikalar. Keyingi qismda (28) API'ga autentifikatsiya qo'shiladi — EGASI_ID o'rniga haqiqiy foydalanuvchi, RLS esa vazifalar_ilova roli bilan ishlay boshlaydi.

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Testlar o'tadi, production'da cheklov xatosi test SQLite yoki pg-mem'da haqiqiy Postgres, o'sha versiya
Test boshqa serverda yiqiladi xato matni tekshirilgan code va constraint
Qidiruv % hammasini qaytaradi ORM LIKE belgilarini ekranlamaydi \, %, _ ni ekranlang yoki strpos
RLS testi hamma qatorni ko'radi superuser yoki boshqa ulanish SET LOCAL ROLE bitta ulanishda, tranzaksiyada
Keyingi test osilib qoladi (pool.end() tugamaydi) ulanish release qilinmagan finally { c.release() }
Migratsiya faqat sizning bazangizda ishlaydi rol/foydalanuvchi nomiga bog'langan toza konteynerda migrate deploy testi
Trigger testi har refaktorda buziladi ichki tuzilma tekshirilgan natijani (qiymatlarni) tekshiring

8. Mashqlar

Mashqlar kurs/mashqlar/27/65-repository/ da.

1-mashq (oson): kodni toping

  • CHECK cheklovi buzilganda SQLSTATE —
  • EXCLUDE cheklovi buzilganda —
  • Kanon migratsiyasining pg-mem'da o'tgan fayllari soni —
  • LIKE naqshida "istalgan ketma-ketlik" belgisi —
  • Bitta interfeysning hamma amalga oshirishi o'tadigan test to'plami — testlari
Yechim
  • 23514 — check_violation.
  • 23P01 — exclusion_violation; 23505 esa unique_violation.
  • 0 — 15 dan 0; birinchisi ~ operatorida yiqildi.
  • % — _ esa bitta istalgan belgi; ikkalasini ham ekranlash kerak.
  • shartnoma — omborShartnomasi.

2-mashq (o'rta): LIKE ekranlash

likeEkranla(matn) funksiyasini yozing (kanondagi bitta qatorli replace kabi): \, % va _ oldiga \ qo'ysin. Keyin likeMosmi(matn, naqsh) — Postgres ILIKE ni soddalashtirib takrorlang: naqshni RegExp'ga o'giring (% → .*, _ → ., \x → x aynan), registrga befarq. Sinang: % qidiruvi ekransiz va ekranli holda "Azizniki 100% #ish" va "Boshqasi" ga mosmi.

Yechim
js
function likeEkranla(matn) {
  return matn.replace(/[\\%_]/g, "\\$&");
}

function likeMosmi(matn, naqsh) {
  let re = "";
  for (let i = 0; i < naqsh.length; i++) {
    const b = naqsh[i];
    if (b === "\\") re += RegExp.escape(naqsh[++i] ?? "");
    else if (b === "%") re += ".*";
    else if (b === "_") re += ".";
    else re += RegExp.escape(b);
  }
  return new RegExp(`^${re}$`, "is").test(matn);
}

const vazifalar = ["Azizniki 100% #ish", "Boshqasi"];
const qidiruv = "%";
for (const [nom, q] of [
  ["ekransiz", qidiruv],
  ["ekranli", likeEkranla(qidiruv)],
]) {
  const naqsh = `%${q}%`;
  const mos = vazifalar.filter((v) => likeMosmi(v, naqsh));
  console.log(`${nom}: ${naqsh} → ${JSON.stringify(mos)}`);
}
console.log(likeEkranla("50%_chegirma\\"));

Konsolda:

text
ekransiz: %%% → ["Azizniki 100% #ish","Boshqasi"]
ekranli: %\%% → ["Azizniki 100% #ish"]
50\%\_chegirma\\

Ekransiz %%% — "istalgan narsa", hamma vazifa mos: kanondagi Prisma xatosi. Ekranli %\%% — faqat ichida % bor matn mos. Teskari chiziq (\) ham ekranlanadi. Aks holda foydalanuvchi yozgan teskari chiziq keyingi belgini "yutib" yuboradi. ES2025 dagi yangi RegExp.escape metodi (Node 24 da bor) RegExp maxsus belgilarini ekranlaydi (Almashtirish chuqur va xavfsiz dinamik regex). s bayrog'i — . yangi qatorga ham mos.

3-mashq (qiyin): kichik shartnoma

Ikki ombor yozing: MassivOmbor (massiv) va MapOmbor (Map). Ikkalasida uchta metod bo'lsin. yarat(matn) id'ni 1 dan beradi. Registrga befarq takror matnda u kod: "23P01" li Error tashlaydi. Qolgan ikkitasi — ol(id) va ochir(id): o'chirilgandan keyin shu matn yana mumkin. shartnoma(nom, yasa) funksiyasi 3 ta tekshiruvni har omborda bajarib, nom: n/3 chiqarsin. MapOmbor ga ataylab xato kiriting: takror tekshiruvi registrga sezgir bo'lsin. Shartnoma buni ushlashini ko'rsating.

Yechim
js
function takrorXatosi() {
  return Object.assign(new Error("takror matn"), { kod: "23P01" });
}

class MassivOmbor {
  #q = [];
  yarat(matn) {
    const kichik = matn.toLowerCase();
    if (this.#q.some((v) => !v.ochirilgan
      && v.matn.toLowerCase() === kichik)) throw takrorXatosi();
    const v = { id: this.#q.length + 1, matn, ochirilgan: false };
    this.#q.push(v);
    return v;
  }
  ol(id) {
    return this.#q.find((v) => v.id === id && !v.ochirilgan);
  }
  ochir(id) {
    const v = this.ol(id);
    if (v) v.ochirilgan = true;
  }
}

class MapOmbor {
  #m = new Map();
  #id = 0;
  yarat(matn) {
    // ATAYLAB XATO: registrga sezgir
    if ([...this.#m.values()].some((v) => v.matn === matn)) {
      throw takrorXatosi();
    }
    const v = { id: ++this.#id, matn };
    this.#m.set(v.id, v);
    return v;
  }
  ol(id) {
    return this.#m.get(id);
  }
  ochir(id) {
    this.#m.delete(id);
  }
}

function xatoKodi(fn) {
  try {
    fn();
    return null;
  } catch (e) {
    return e.kod;
  }
}

function shartnoma(nom, yasa) {
  const tekshiruvlar = [
    () => yasa().yarat("Non").id === 1,
    () => {
      const o = yasa();
      o.yarat("Non olish");
      return xatoKodi(() => o.yarat("NON OLISH")) === "23P01";
    },
    () => {
      const o = yasa();
      const v = o.yarat("Sut");
      o.ochir(v.id);
      return xatoKodi(() => o.yarat("sut")) === null
        && o.ol(v.id) === undefined;
    },
  ];
  const otdi = tekshiruvlar.filter((t) => t()).length;
  console.log(`${nom}: ${otdi}/${tekshiruvlar.length}`);
}

shartnoma("massiv", () => new MassivOmbor());
shartnoma("map", () => new MapOmbor());

Konsolda:

text
massiv: 3/3
map: 2/3

Bir xil uchta tekshiruv ikkala omborda. MapOmbor "takror" testida yiqildi — "NON OLISH" ni qabul qildi. Shartnoma testlari aynan shu uchun: yangi amalga oshirish (PrismaOmbori, KeshliOmbor) eski kelishuvni buzsa — darhol ko'rinadi. Haqiqiy loyihada ochir dan keyingi xulq kabi nozik joylar — eng ko'p farq chiqadigan joy.

9. Real ishda

  • Backend jamoalari — ombor/SQL testlari Testcontainers yoki CI'dagi haqiqiy Postgres bilan; mock — faqat xizmat qatlamida.
  • ORM yangilanishi — Prisma yoki TypeORM major versiyasini yangilashda ombor testlari birinchi himoya: yuborilgan SQL o'zgarganini ular ushlaydi.
  • Migratsiya CI'da — har PR'da toza bazaga migrate deploy va sxema farqi tekshiruvi.
  • Intervyuda: "Repository'ni qanday testlaysiz?", "Nega in-memory baza bilan test yetarli emas?", "Contract test nima?", "DB cheklovlarini test qilasizmi?"

Xulosa

  • Ombor testining qiymati — bazaning o'z xulqida (cheklov, trigger, RLS); uni faqat haqiqiy Postgres ko'rsatadi.
  • pg-mem kanon migratsiyasini ko'tarolmadi (0/15); SQLite — boshqa dialekt, xavfsizlik illyuziyasi.
  • Testda code va constraint tekshiriladi; trigger — natija bo'yicha; RLS — bitta ulanishda, tranzaksiyada.
  • Shartnoma testlari beshta omborni bir xil tekshiradi; haqiqiy bazadagi test Prisma contains xatosini topdi.
  • Holat 65: 112 test (111 o'tdi), Testcontainers 10/10, qamrov 98.82 % — commit bilan 27-qism yakuni.

Keyingi dars: Xavfsizlik nima: CIA, tahdid va hujumchi fikrlashi — 28-qism boshlanadi: vazifalar-api ga autentifikatsiya qo'shishdan oldin, avval hujumchi qanday o'ylashini o'rganamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Repository va SQL'ni testlash: cheklovlar, trigger'lar va pg-mem chegarasi — IlmHamroh