Mundarija (25)
- Bu darsda
- 1. Nega bu kerak?
- 2. In-memory taqlidlar
- 2.1 pg-mem
- 2.2 SQLite bilan almashtirish
- 2.3 Mock qachon to'g'ri
- 3. Cheklovlar va trigger'larni testlash
- 3.1 SQL darajasida
- 3.2 RLS
- 3.3 Node testida: kod va cheklov nomi
- 4. Shartnoma testlari: bitta interfeys, ko'p amalga oshirish
- 4.1 Topilma: Prisma contains va %
- 5. Migratsiyani testlash
- 6. Loyiha qadami: holat 65 yakuni va commit
- 6.1 SQL testlari bloki
- 6.2 Yakuniy tekshiruv
- 6.3 Commit
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): kodni toping
- 2-mashq (o'rta): LIKE ekranlash
- 3-mashq (qiyin): kichik shartnoma
- 9. Real ishda
- Xulosa
- Manbalar
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:
vazifalarmigratsiyasining 15 faylidan birortasi ham o'tmadi (0/15). SQLite bilan almashtirish ham xavfli — boshqa SQL dialekti. Haqiqiy bazadagi testlar esa haqiqiy xatolarni topadi: Prismacontains%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-apiholat 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 vaizohlar_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):
--- 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:
INTEGERustunga matn yozilishi mumkin (STRICT jadvalsiz); JSONB, massivlar,EXCLUDE, RLS,ILIKE, PL/pgSQL trigger'lari,RETURNINGning 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:
\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_SQLSTATEERROR: new row for relation "vazifalar" violates check constraint "vazifalar_matn_check"
23514
ERROR: new row for relation "vazifalar" violates check constraint "vazifalar_matn_check"
23514Ikkala 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.
\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%';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:
tartib7 (egasining ro'yxati oxiriga),versiya1. - Oxirgi
DELETE— tajribani tozaladik (keyingi bloklar uchun).
3.2 RLS
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;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):
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.pgdrayveri Postgres xatosiningcodevaconstraintmaydonlarini 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,ROLLBACKbilan.SET LOCALfaqat shu tranzaksiyaga ta'sir qiladi (Test DB strategiyalari dagi hovuz tuzog'i). - Har
try—finally { c.release() }. Ulanish hovuzga qaytmasa, keyingi testdagitoza()ichidagipool.end()uni kutib qoladi va test osilib qoladi (pg-poolend()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):
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):
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):
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; 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:
// 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:
- Noldan qo'llanadimi — Testcontainers'da
prisma migrate deploytoza bazaga (Testcontainers); 106 dagiFOR ROLE postgresxatosi aynan shu yerda topilgan. - Natija kutilgan sxemami — konteyner testi:
0_initqo'llangan, 5 trigger, 1 materialized view, 5 RLS'li jadval. - Migratsiyalar va qo'lda yozilgan
sql/bir xil sxema beradimi — ikkala yo'l bilan qurilgan bazalarningpg_dump --schema-onlychiqishi solishtiriladi:
--- 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
npm run tip
npm run lint
npm test
TESTCONTAINERS=1 npm test
npm run test:qamrovKanon natijalari (kanon o'lchovi, IlmHamroh, 2026-10):
--- 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).
- 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):
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 —
LIKEnaqshida "istalgan ketma-ketlik" belgisi —- Bitta interfeysning hamma amalga oshirishi o'tadigan test to'plami — testlari
Yechim
- 23514 — check_violation.
- 23P01 — exclusion_violation;
23505esa 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
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:
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
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:
massiv: 3/3
map: 2/3Bir 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 deployva 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
codevaconstrainttekshiriladi; trigger — natija bo'yicha; RLS — bitta ulanishda, tranzaksiyada. - Shartnoma testlari beshta omborni bir xil tekshiradi; haqiqiy bazadagi test Prisma
containsxatosini 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
- PostgreSQL 18 — xato kodlari (23514, 23P01): https://www.postgresql.org/docs/18/errcodes-appendix.html
- PostgreSQL 18 — LIKE va ekran belgisi: https://www.postgresql.org/docs/18/functions-matching.html
- pg-mem: https://github.com/oguimbal/pg-mem
- Prisma — filtrlash (
contains,mode: insensitive): https://www.prisma.io/docs/orm/prisma-client/queries/filtering-and-sorting - SQLite — tip tizimi va farqlar: https://www.sqlite.org/datatype3.html
- O'lchovlar va kod — kanon o'lchovi (IlmHamroh, 2026-10): pg-mem 3.0.14, PostgreSQL 18.6; holat 65 —
test/konteyner.test.ts,test/prisma-ombor.test.ts,src/vazifalar/prisma-ombor.ts. psql misollari — biz ishga tushirdik (PostgreSQL 17.6, holat 65 sxemasi).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!