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

Testcontainers: testlar uchun haqiqiy Postgres konteyneri Node'da

Qisqacha: Testcontainers — test boshida haqiqiy bazani Docker konteynerida o'zi ko'tarib, oxirida o'chiradigan kutubxona. Test uchun oldindan baza o'rnatish, port va parol sozlash kerak emas: new PostgreSqlContainer("postgres:18").start() — va getConnectionUri() tayyor. Kanonda: konteyner 5.0 s, prisma migrate deploy 4.1 s, snapshot() 0.8 s, har testdan oldin restoreSnapshot() ~0.4 s; 10/10 test o'tdi, qoldiq konteyner 0. Narxi — Docker kerak va har ishga tushirish ~10 s qo'shadi, shuning uchun kanonda bu testlar alohida (TESTCONTAINERS=1).

Bu darsda

  • Testcontainers nima muammoni hal qilishini tushuntirasiz.
  • @testcontainers/postgresql bilan konteyner ko'tarib, migratsiyalarni qo'llaysiz.
  • snapshot() / restoreSnapshot() bilan har testni toza bazada boshlaysiz.
  • Ryuk, tasodifiy port, sukut parol, global setup, konteynerni qayta ishlatish va CI haqida bilasiz.
  • vazifalar-api ga konteyner testlarini qo'shasiz.

Oldin bilishingiz kerak: Test DB strategiyalari, Image, container va registry, Prisma Migrate chuqur.

Oldingi darsda test bazasini tozalashning to'rt usulini ko'rdik. Hammasida bitta shart bor edi: test bazasi allaqachon mavjud va to'g'ri sxemada. Bugun bu shartni ham olib tashlaymiz.

1. Nega bu kerak?

Yangi dasturchi vazifalar-api ni klonladi va npm test berdi. Postgres testlari SKIP bo'ldi: kompyuterida 127.0.0.1:5499 da vazifalar_test bazasi yo'q. Uni yaratish uchun README'ni o'qib, Docker konteynerini ishga tushirib, sxemani qo'llash kerak. Boshqa dasturchida baza bor, lekin eski sxemada — testlar g'alati yiqiladi. CI serverida esa yana boshqa sozlama.

Testcontainers g'oyasi: test kerakli bazani o'zi ko'taradi. Har ishga tushirishda — yangi, toza, aniq versiyali Postgres. Test tugadi — konteyner o'chdi. Hamma kompyuterda va CI'da bir xil.

O'xshatish: har safar ijaraga olingan yangi, toza mashina. Kimdir uni avval qanday ishlatgani sizga ta'sir qilmaydi, ishingiz bitgach qaytarasiz.

sequenceDiagram
  participant T as Test
  participant D as Docker
  participant P as Postgres konteyner
  T->>D: start postgres:18
  D->>P: yaratadi, port ochadi
  T->>P: prisma migrate deploy
  T->>P: snapshot (template)
  loop har test
    T->>P: restoreSnapshot
    T->>P: test so'rovlari
  end
  T->>D: stop

Diagrammaga qarang: qimmat ishlar (konteyner, migratsiya) bir marta, har test oldidan esa faqat arzon tiklash (restore).

2. Testcontainers qanday ishlaydi

Testcontainers — Docker API'si ustidagi kutubxona (Java'da boshlangan, hozir Node, Go, Python, .NET uchun ham bor). Node versiyasi ikki paket:

  • testcontainers — umumiy: istalgan image'dan konteyner (GenericContainer).
  • @testcontainers/postgresql — Postgres uchun tayyor modul: sukut parol, baza nomi, "tayyor bo'lguncha kutish" va snapshot.
bash
npm i -D --save-exact testcontainers@12.2.1 \
  @testcontainers/postgresql@12.2.1

Docker ishlab turishi shart (Docker Desktop yoki Linux'da Docker Engine — Konteyner nima va Docker'ni o'rnatish). Testcontainers Docker'ga o'zi ulanadi.

Eng kichik misol:

ts
import { PostgreSqlContainer } from "@testcontainers/postgresql";

const konteyner =
  await new PostgreSqlContainer("postgres:18").start();
const url = konteyner.getConnectionUri();
// url — postgres://<foydalanuvchi>:<parol>@<host>:<port>/<baza>
// … testlar …
await konteyner.stop();

start() image'ni (bo'lmasa) yuklaydi, konteynerni yaratadi va Postgres ulanishni qabul qilguncha kutadi. Shuning uchun keyingi qatorda baza tayyor. getConnectionUri() — tayyor ulanish satri. Port har safar tasodifiy. Foydalanuvchi, parol va baza nomi esa sukut bo'yicha test (12.2.1 paket kodida shunday; withUsername, withPassword bilan o'zgartiriladi).

2.1 Sukut xulq: Ryuk va port

Ikki narsani bilib qo'ying (hujjat bo'yicha):

  • Ryuk — Testcontainers yoniga ko'taradigan kichik "qorovul" konteyner. Test jarayoni kutilmaganda o'lsa (yiqilish, Ctrl+C), Ryuk uning konteynerlarini o'zi o'chiradi — kompyuterda "yetim" konteynerlar to'planmaydi.
  • Port — konteynerning 5432 porti hostdagi tasodifiy portga bog'lanadi va sukut bo'yicha barcha tarmoq interfeyslarida ochiladi. Parol esa tasodifiy emas — sukut test. Konteyner faqat test davomida yashaydi, lekin shu vaqtda tarmoqdagi boshqa kompyuter ham ulanishi mumkin. Umumiy tarmoqda (ofis, Wi-Fi) .withPassword(randomUUID()) bering.

Tasodifiy port muhim: bir vaqtda ikki loyihaning testi yoki ikki test jarayoni bir-biriga xalaqit bermaydi. Kodga hech qachon port yozmang — doim getConnectionUri() yoki getPort().

2.2 Kanondagi istisno

Kanonning o'z qoidasi bor edi: hamma konteyner kanon27-* nomli va faqat 127.0.0.1 da (egasining kompyuterida boshqa konteynerlar ham ishlaydi). Shuning uchun kanon:

  • konteynerga nom berdi — kanon27-tc-<pid>;
  • Ryuk'ni o'chirdi (TESTCONTAINERS_RYUK_DISABLED=true) — Ryuk konteynerining nomi qoidaga to'g'ri kelmaydi; o'rniga after() da konteynerni o'zi to'xtatadi;
  • port bo'yicha istisno qildi: barcha interfeyslarda, lekin faqat test davomida (parol — sukut test).

O'zingizning loyihangizda sukut xulqni qoldiring: Ryuk yoqiq — xavfsizroq. Kanon tekshiruvidan keyin qoldiq konteyner: 0.

3. Migratsiyalar va snapshot

3.1 before: bir marta tayyorlash

ts
before(async () => {
  let t = performance.now();
  const k = await new PostgreSqlContainer("postgres:18")
    .withName(`kanon27-tc-${String(process.pid)}`)
    // diskka yozmaydi — tezroq, to'xtaganda yo'qoladi
    .withTmpFs({ "/var/lib/postgresql": "rw" })
    .start();
  konteyner = k;
  vaqtlar["konteyner"] = performance.now() - t;
  t = performance.now();
  const r = spawnSync(process.execPath,
    [join("node_modules", "prisma", "build", "index.js"),
      "migrate", "deploy"], {
      env: { ...process.env, DATABASE_URL: k.getConnectionUri() },
      encoding: "utf8",
    });
  if (r.status !== 0) {
    throw new Error(`migrate deploy: ${r.stdout}${r.stderr}`);
  }
  vaqtlar["migratsiya"] = performance.now() - t;
  t = performance.now();
  await k.snapshot(); // migratsiyalangan bo'sh baza → template
  vaqtlar["snapshot"] = performance.now() - t;
});

Uch qadam:

  1. Konteyner. withTmpFs — Postgres ma'lumot papkasini xotiradagi diskka (tmpfs) qo'yadi: yozuvlar tez, konteyner o'chganda hech narsa qolmaydi. Test bazasi uchun ideal.
  2. Migratsiya. Xuddi production deploy'idagi buyruq (Migratsiyani deploy jarayonida qo'llash): prisma migrate deploy, faqat DATABASE_URL — konteynerniki. Shunday qilib testlar haqiqiy migratsiyalar sxemasida ishlaydi — qo'lda yozilgan test sxemasida emas. Migratsiya yiqilsa — test ham yiqiladi, xabari bilan.
  3. Snapshot. snapshot() — migratsiyalangan bo'sh bazani shablon sifatida saqlaydi. Ichida — oldingi darsdagi CREATE DATABASE … TEMPLATE (Test DB strategiyalari).

3.2 Har testdan oldin: restore

ts
/** Har test toza bazada: restore (template'dan qayta yaratish) —
 *  pool'ni oldin yopish kerak (restore ochiq ulanishlarni uzadi) */
async function toza(): Promise<Pool> {
  if (konteyner === undefined) {
    throw new Error("konteyner ishga tushmagan");
  }
  await pool?.end();
  const t = performance.now();
  await konteyner.restoreSnapshot();
  vaqtlar["restore (oxirgi)"] = performance.now() - t;
  const yangi = pgPool(konteyner.getConnectionUri(), { max: 5 });
  pool = yangi;
  return yangi;
}

restoreSnapshot() joriy bazani o'chirib, shablondan qayta yaratadi. 12.2.1 paket kodida bu ikki buyruq: DROP DATABASE "test" WITH (FORCE) va CREATE DATABASE "test" WITH TEMPLATE …. WITH (FORCE) — bazaga ulangan hamma sessiyani majburan uzadi, shuning uchun oldingi darsdagi 55006 xatosi bu yerda chiqmaydi.

Lekin eski hovuzning ulanishlari ham uziladi. Hovuz ularni "tirik" deb o'ylaydi va keyingi so'rov xato bilan yiqiladi. Shuning uchun avval hovuzni yopamiz (pool.end()), restore'dan keyin esa yangi hovuz ochamiz. Har testda — yangi hovuz.

3.3 Kanon vaqtlari

Yakuniy tekshiruvdan (TESTCONTAINERS=1):

text
--- Testcontainers (TESTCONTAINERS=1):   [Testcontainers vaqtlari] konteyner 5001 ms, migratsiya 4063 ms, snapshot 827 ms, restore (oxirgi) 401 ms ℹ tests 10 ℹ pass 10 ℹ fail 0 ℹ skipped 0 ; kanon27-tc qoldiqlari: 0
Testcontainers: bosqichlar vaqti
  • Konteyner start5 001 ms
  • prisma migrate deploy4 063 ms
  • snapshot827 ms
  • restore (har test)401 ms

Manba: IlmHamroh kanon o'lchovi, 2026-10-25: Testcontainers 12.2.1, postgres:18 (tmpfs), Docker Desktop (WSL2), i5-12500H

Bir martalik narx 10 soniya (kanon boshqa ishga tushirishlarda konteyner 5.0–11.2 s, migratsiya 4.1–5.7 s o'lchagan — mashina yuklamasiga bog'liq). Har test oldidan — 0.4 s: TRUNCATE (0.1 s) dan sekin, lekin to'liq toza baza, trigger'lar va sequence'lar ham boshlang'ich holatda.

Tekshirib ko'ring: Nega konteyner har test uchun emas, har fayl uchun bir marta ko'tariladi?

Javob

Konteyner va migratsiya ~9 s. 10 ta test uchun alohida konteyner — 90 s. Bir marta ko'tarib, testlar orasida faqat restoreSnapshot() (0.4 s) qilish — 10 + 4 s atrofida. Izolyatsiya bir xil: har test baribir toza bazada boshlanadi.

4. Qachon ishlatish va qachon emas

4.1 Opt-in

Kanon Testcontainers testlarini ixtiyoriy qildi:

ts
const YOQIQ = process.env["TESTCONTAINERS"] === "1";

if (!YOQIQ) {
  test("Testcontainers (BT-11)", {
    skip: "TESTCONTAINERS=1 bilan ishga tushiriladi (Docker kerak)",
  }, () => {});
} else {
  process.env["TESTCONTAINERS_RYUK_DISABLED"] = "true";
  const { PostgreSqlContainer } =
    await import("@testcontainers/postgresql");
  // … before, after, toza, testlar …
}

Sabab: kundalik npm test tez bo'lishi kerak (kanonda 112 test ~14 s, lokal baza bilan). Docker'ni har saqlashda ishga tushirish og'ir. TESTCONTAINERS=1 npm test — commit yoki push oldidan, CI'da esa doim. await import(…) — paket faqat kerak bo'lganda yuklanadi (Dinamik import() va top-level await).

Boshqa yo'l: lokal baza testlarini butunlay Testcontainers'ga o'tkazish. Kichik jamoada bitta yondashuv sodda; katta loyihada ikkalasi.

4.2 Qayta ishlatish va CI

  • Qayta ishlatish (.withReuse(); 12.2.1 paket kodida TESTCONTAINERS_REUSE_ENABLE=false uni o'chiradi): konteyner test tugagach o'chmaydi, keyingi ishga tushirishda shu konteyner olinadi — 5 s tejaladi. Lokal ishlab chiqish uchun qulay, CI uchun emas. Kanonda sinalmagan.
  • CI — GitHub Actions'ning Linux runner'larida Docker bor, Testcontainers qo'shimcha sozlamasiz ishlaydi (hujjat; kanonda CI sinalmagan — kurs CI'si 33–34-qismlarda).
  • Image versiyasi — postgres:18 emas, aniq teg (postgres:18.6) yoki digest: aks holda bir kuni yangi minor versiya bilan testlar boshqacha ishlashi mumkin. Production versiyasi bilan bir xil bo'lsin.
Lokal test bazasi Testcontainers
Tayyorlash qo'lda (README) avtomatik
Sxema qo'lda qo'llanadi migratsiyalardan, har safar
Bir martalik narx 0 ~10 s
Docker shart emas shart
Versiya dasturchida har xil kodda aniq

4.3 Global setup: bitta konteyner hamma fayllar uchun

Kanonda konteyner bitta test faylining before ida ko'tariladi. Testcontainers'li fayllar ko'paysa, har fayl o'z konteynerini ko'taradi — har biriga ~10 s. Node 24 test runner'ida buning yechimi bor: global setup — barcha test fayllaridan oldin bir marta ishlaydigan fayl (--test-global-setup bayrog'i, node:test va node:assert).

Kanonda sinalmagan. Biz konteynersiz kichik misolda sinadik (Node 24.21.0). setup.mjs:

js
export async function globalSetup() {
  process.env.TEST_DB_URL = "postgres://test@127.0.0.1:55000/test";
  console.log("globalSetup: konteyner ko'tarildi");
}
export async function globalTeardown() {
  console.log("globalTeardown: konteyner to'xtadi");
}

Ikkita test fayli (a.test.mjs, b.test.mjs) faqat process.env.TEST_DB_URL ni tekshiradi. Ishga tushirish (chiqishdan tanlangan qatorlar):

bash
node --test --test-global-setup=setup.mjs
text
globalSetup: konteyner ko'tarildi
✔ a fayli URL ni ko'radi (1.077ms)
✔ b fayli URL ni ko'radi (0.7554ms)
ℹ tests 2
ℹ pass 2
ℹ fail 0
globalTeardown: konteyner to'xtadi

Nima bo'ldi? globalSetup bitta marta, eng boshida ishladi. U qo'ygan muhit o'zgaruvchisini ikkala fayl ham ko'rdi — fayllar alohida jarayonlarda bo'lsa ham. globalTeardown eng oxirida ishladi. Haqiqiy loyihada globalSetup ichida new PostgreSqlContainer(…).start() va migratsiya bo'ladi. Ehtiyot bo'ling: hamma fayl bitta bazaga tushadi va parallel ishlaydi — har faylga alohida baza kerak (Test DB strategiyalari dagi parallel testlar muammosi).

4.4 Boshqa konteynerlar

Xuddi shunday Redis, Mongo, Kafka — @testcontainers/redis va boshqa modullar yoki GenericContainer("redis:8"). vazifalar-api keshi hozircha lokal Redis bilan sinaladi (TEST_REDIS_URL) — konteynerga o'tkazish keyingi qadam bo'lishi mumkin.

5. Loyiha qadami: vazifalar-api konteyner testlari (holat 65, 3-qism)

Ikki dev bog'liqlik:

bash
npm i -D --save-exact testcontainers@12.2.1 \
  @testcontainers/postgresql@12.2.1

Yangi fayl test/konteyner.test.ts. Bu darsda — uning karkasi, migratsiya testi va shartnoma testlari. Cheklovlar, trigger'lar va RLS testlari (BT-13 bloki) Repository va SQL'ni testlash darsida qo'shiladi; ular ishlatadigan fabrika — Factory va fixture'lar da. Kanon kodi (qatorlar telefon uchun bo'lingan, izohlar qisqartirilgan):

ts
// konteyner.test.ts — Testcontainers bilan HAQIQIY Postgres 18:
// test o'zi konteyner ko'taradi, migratsiyalarni
// (prisma migrate deploy) qo'llaydi, snapshot oladi,
// har testdan oldin restore.
// Opt-in: TESTCONTAINERS=1 (Docker kerak, ~10 s);
// oddiy npm test — SKIP.
import { after, before, beforeEach, describe, test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { join } from "node:path";
import type { Pool } from "pg";
import { pgPool, PgOmbori } from "../src/vazifalar/pg-ombor.ts";
import { omborShartnomasi } from "./ombor-shartnoma.ts";

const YOQIQ = process.env["TESTCONTAINERS"] === "1";

if (!YOQIQ) {
  test("Testcontainers (BT-11)", {
    skip: "TESTCONTAINERS=1 bilan ishga tushiriladi (Docker kerak)",
  }, () => {});
} else {
  process.env["TESTCONTAINERS_RYUK_DISABLED"] = "true";
  const { PostgreSqlContainer } =
    await import("@testcontainers/postgresql");
  type Ishlayotgan = Awaited<ReturnType<
    InstanceType<typeof PostgreSqlContainer>["start"]>>;
  let konteyner: Ishlayotgan | undefined;
  let pool: Pool | undefined;
  const vaqtlar: Record<string, number> = {};
  let db: Pool; // joriy test bazasi (beforeEach — toza())

  before(async () => {
    // … «before: bir marta tayyorlash» bo'limidagi kod …
  });

  after(async () => {
    await pool?.end();
    await konteyner?.stop();
    console.log(`  [Testcontainers vaqtlari] ${Object.entries(vaqtlar)
      .map(([k, v]) => `${k} ${v.toFixed(0)} ms`).join(", ")}`);
  });

  async function toza(): Promise<Pool> {
    // … «Har testdan oldin: restore» bo'limidagi kod …
  }

  describe("BT-11: migratsiyalar konteynerda", () => {
    beforeEach(async () => { db = await toza(); });

    test("hamma migratsiya qo'llangan, sxema obyektlari joyida",
      async () => {
        const { rows } = await db.query<{ m: string }>(
          "SELECT migration_name AS m FROM _prisma_migrations "
          + "WHERE finished_at IS NOT NULL ORDER BY 1");
        assert.equal(rows[0]?.m, "0_init");
        const { rows: [s] } = await db.query<
          { trg: number; mv: number; rls: number }>(`SELECT
          (SELECT count(*)::int FROM pg_trigger
            WHERE NOT tgisinternal) AS trg,
          (SELECT count(*)::int FROM pg_matviews) AS mv,
          (SELECT count(*)::int FROM pg_class
            WHERE relrowsecurity) AS rls`);
        assert.deepEqual(s, { trg: 5, mv: 1, rls: 5 });
      });
  });

  // BT-10: shartnoma testlari ham konteynerda — har test
  // template'dan toza baza (TRUNCATE kerak emas)
  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);
  });
}

before va toza ichi yuqoridagi bo'limlarda to'liq — faylga o'sha kodni qo'ying (… qatorlari o'rniga). Ikki muhim test:

  • Migratsiya testi — 0_init qo'llangan, 5 ta trigger, 1 materialized view, 5 ta RLS'li jadval bor. Bu "migratsiyalar haqiqatan sxemani to'liq quradimi" savoliga javob. ORM sxemasidan yaratilgan baza bilan bu test yiqilardi: trigger va RLS Prisma sxemasida yo'q (Prisma Migrate chuqur).
  • Shartnoma testlari — PgOmbori xotira va fayl omborlari o'tgan o'sha testlardan konteynerda ham o'tadi.

Bu yerda kanon muhim xatoni topdi. Testcontainers Postgres modulining sukut superuser'i — postgres emas, test. Birinchi ishga tushirishda migrate deploy yiqildi: role "postgres" does not exist. Sabab — 106-rollar.sql dagi ALTER DEFAULT PRIVILEGES FOR ROLE postgres (Rollar va huquqlar): migratsiya ma'lum foydalanuvchi nomiga bog'lanib qolgan edi. Managed bazalarda (boshqa admin nomi) ham xuddi shunday yiqilardi. Kanon FOR ROLE CURRENT_USER ga tuzatdi.

Odatda qo'llangan migratsiya fayli o'zgartirilmaydi — yangi migratsiya yoziladi (Migratsiya tushunchasi). Bu yerda istisno qilindi: kurs hali nashr qilinmagan va yangi bazada eski 106 baribir yiqiladi — tuzatuvchi 109-migratsiya yordam bermasdi. Haqiqiy loyihada bunday holatda production'da allaqachon qo'llangan faylning checksum'i o'zgaradi va Prisma ogohlantiradi — jamoa bilan kelishib hal qilinadi. Bu topilmaning o'zi Testcontainers foydasini ko'rsatadi: "faqat mening kompyuterimda ishlaydi" turidagi xato birinchi toza bazada ochildi.

Ishga tushirish (Docker yoqiq):

bash
TESTCONTAINERS=1 npm test

Natija tests 10, pass 10 (shartnoma + migratsiya + keyingi darslardagi testlar bilan birga), konteyner oxirida o'chadi. Hali commit qilmang.

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Testcontainers Docker'ni topa olmadi degan xato Docker ishlamayapti Docker Desktop/Engine'ni yoqing
role "postgres" does not exist migratsiya admin nomiga bog'langan CURRENT_USER, nomga bog'lanmang
restore'dan keyin birinchi so'rov yiqiladi eski hovuz ulanishlari uzilgan avval pool.end(), keyin yangi hovuz
Testlar 10 s sekinlashdi har testda yangi konteyner before da bir marta
Kompyuterda eski konteynerlar to'planadi Ryuk o'chiq va stop() chaqirilmagan Ryuk yoqiq qoldiring yoki after da stop()
CI'da testlar boshqacha postgres:latest yoki suzuvchi teg aniq versiya teg
Test kodida port 5432 konteyner porti tasodifiy getConnectionUri()

7. Mashqlar

Mashqlar kurs/mashqlar/27/63-testcontainers/ da.

1-mashq (oson): to'ldiring

  • Konteyner ulanish satrini beradigan metod —
  • Jarayon yiqilsa konteynerlarni o'chiradigan "qorovul" —
  • Migratsiyalangan bazani shablon qilish —
  • Har testdan oldin shablondan tiklash —
  • Ma'lumot papkasini xotiraga qo'yadigan sozlama —
Yechim
  • getConnectionUri — port har safar boshqa; kodga yozmang.
  • Ryuk — sukut yoqiq; kanon nomlash qoidasi tufayli o'chirgan.
  • snapshot — ichida CREATE DATABASE … TEMPLATE.
  • restoreSnapshot — kanonda ~0.4 s; oldin hovuzni yoping.
  • withTmpFs — tez, konteyner bilan yo'qoladi.

2-mashq (o'rta): parolni log'da yashirish

Kanon testi xato bo'lganda migrate deploy chiqishini log'ga yozadi — unda ulanish satri (parol bilan) bo'lishi mumkin. parolniYashir(matn) funksiyasini yozing: matndagi har postgres://foydalanuvchi:parol@ (yoki postgresql://) bo'lagida parolni *** ga almashtirsin. Sinang: postgres://test:Xk9q2Lm@localhost:55123/test, parolsiz URL va ikki URL bor matn.

Yechim
js
function parolniYashir(matn) {
  return matn.replace(
    /(postgres(?:ql)?:\/\/[^:/@\s]+:)[^@\s]+@/g,
    "$1***@",
  );
}

console.log(parolniYashir(
  "postgres://test:Xk9q2Lm@localhost:55123/test"));
console.log(parolniYashir(
  "postgres://postgres@127.0.0.1:5499/vazifalar"));
console.log(parolniYashir(
  "A=postgresql://a:1@h/x B=postgres://b:2@h/y",
));

Konsolda:

text
postgres://test:***@localhost:55123/test
postgres://postgres@127.0.0.1:5499/vazifalar
A=postgresql://a:***@h/x B=postgres://b:***@h/y

Birinchi guruh — sxema://foydalanuvchi: gacha, keyin parol (@ gacha) almashadi. Parolsiz URL'da : dan keyin @ kelmaydi — o'zgarmadi. g bayrog'i — matndagi hamma URL (Almashtirish chuqur va xavfsiz dinamik regex). Test paroli tasodifiy va vaqtinchalik bo'lsa ham, log'larni tozalash odati — productionda haqiqiy parollarni qutqaradi (Sirlarni boshqarish).

3-mashq (qiyin): qaysi strategiya arzon?

Kanon raqamlarini oling:

  • Testcontainers — bir martalik 5 001 + 4 063 + 827 ms, har test 401 ms;
  • lokal baza + TRUNCATE — bir martalik 0, har test 106 ms;
  • har testga yangi konteyner — har test 5 001 + 4 063 ms.

vaqt(strategiya, n) funksiyasini yozing va n = 10, 100, 500 testlar uchun soniyalarda (1 xona) jadval chiqaring.

Yechim
js
const STRATEGIYALAR = {
  "TRUNCATE (lokal)": { bir: 0, har: 106 },
  "Testcontainers + restore": { bir: 5001 + 4063 + 827, har: 401 },
  "har testga konteyner": { bir: 0, har: 5001 + 4063 },
};

function vaqt(s, n) {
  return (s.bir + s.har * n) / 1000;
}

for (const [nom, s] of Object.entries(STRATEGIYALAR)) {
  const qator = [10, 100, 500].map((n) => vaqt(s, n).toFixed(1));
  console.log(`${nom}: ${qator.join(" | ")} s`);
}

Konsolda:

text
TRUNCATE (lokal): 1.1 | 10.6 | 53.0 s
Testcontainers + restore: 13.9 | 50.0 | 210.4 s
har testga konteyner: 90.6 | 906.4 | 4532.0 s

Lokal baza + TRUNCATE hamma joyda tez — lekin tayyor baza talab qiladi. Testcontainers bir martalik ~10 s va har testga 4 baravar qimmatroq tiklash bilan izolyatsiya va avtomatik sozlash beradi. Har testga yangi konteyner — 500 testda 75 daqiqa: hech qachon. Amalda aralash: tez testlar lokal, to'liq tekshiruv Testcontainers bilan (kanondagi opt-in). Restore'ni har test emas, har describe bloki uchun qilish ham narxni kamaytiradi.

8. Real ishda

  • Java/Spring dunyosida Testcontainers — standart; Node'da ham tez tarqalmoqda (NestJS e2e testlari — 30-qismda).
  • CI — GitHub Actions/GitLab'da Docker bilan; ko'p jamoalar PR'da faqat Testcontainers testlarini ishlatadi.
  • Mikroxizmatlar — bitta testda Postgres + Redis + Kafka konteynerlari.
  • Intervyuda: "Integration testlarda bazani qanday tayyorlaysiz?", "Testcontainers nima va uning narxi?", "Testlar orasida bazani qanday tozalaysiz?"

Xulosa

  • Testcontainers — test o'zi haqiqiy bazani konteynerda ko'taradi: hamma kompyuterda va CI'da bir xil.
  • new PostgreSqlContainer("postgres:18").start() → getConnectionUri(); port tasodifiy, parol sukut test.
  • before: konteyner → prisma migrate deploy → snapshot(); har test: restoreSnapshot() (avval hovuzni yoping).
  • Kanon: ~10 s bir martalik, 0.4 s har test; 10/10, qoldiq 0; Ryuk sukut yoqiq (kanon nom qoidasi uchun o'chirgan).
  • Toza bazadagi birinchi ishga tushirish yashirin xatoni ochdi: migratsiya postgres roliga bog'langan edi.

Keyingi dars: Factory va fixture'lar — test ma'lumotini qo'lda emas, fabrika bilan: faker uz_UZ_latin, seed, minimal ma'lumot va bog'lanishli fabrikalar.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Testcontainers: testlar uchun haqiqiy Postgres konteyneri Node'da — IlmHamroh