تست ادغام با Jest و PostgreSQL

یکی از وظایف حیاتی یک توسعهدهنده، نوشتن تستهای یکپارچهسازی برای پایگاه داده است تا اطمینان حاصل کند که پیادهسازی طبق انتظار انجام میشود. با این حال، اجرای تست های ادغام به طور مستقل و همزمان در برابر یک پایگاه داده مشترک می تواند منجر به تداخل تست ها با یکدیگر و شکست متناوب شود. همه نمونه های کد در TypeScript هستند، اما منطق مشابه برای هر زبان دیگری اعمال می شود.
مشکل زمانی به وجود می آید که مجموعه تست های یکپارچه سازی به طور همزمان در برابر یک نمونه پایگاه داده مشترک اجرا شوند. برای مثال، با توجه به پیادهسازی ساده یک مخزن SQL با استفاده از کتابخانه ts-postgres و مجموعههای تست Jest ارائه شده در بلوکهای کد، میتوانید jest را چند بار اجرا کنید و مشکل همزمانی را تجربه کنید. مشکل از خالی شدن پایگاه داده بین await repository.insert(id);
و expect(await repository.findAll()).toEqual([[id]]);
عبارات، با توجه به این واقعیت که تست Selects nothing تمام می کند و رکوردهای جدول را درست قبل از عبارت expect حذف می کند.
src/exampleRepository.ts
import { Client, Value } from "ts-postgres";
export class ExampleRepository {
private readonly _client: Client;
constructor(client: Client) {
this._client = client;
}
async findAll(): Promise<Array<Value>> {
return (await this._client.query("SELECT * FROM example")).rows;
}
async insert(id: string): Promise<void> {
await this._client.query("INSERT INTO example (id) VALUES ($1)", [id]);
}
}
src/تست/problematic/insert.test.ts
import { Client } from "ts-postgres";
import { ExampleRepository } from "../../exampleRepository";
import { config } from "dotenv";
import { v4 } from "uuid";
import { rootConnect } from "../postgresUtils";
describe("Insert Test Suite", () => {
let client: Client;
let repository: ExampleRepository;
beforeAll(async () => {
config();
client = await rootConnect();
});
afterAll(async () => {
await client.end();
});
beforeEach(() => {
repository = new ExampleRepository(client);
});
afterEach(async () => {
await client.query("DELETE FROM example");
});
it("inserts and select", async () => {
const id = v4();
await repository.insert(id);
expect(await repository.findAll()).toEqual([[id]]);
});
});
src/تست/problematic/select.test.ts
import { Client } from "ts-postgres";
import { ExampleRepository } from "../../exampleRepository";
import { config } from "dotenv";
import { rootConnect } from "../postgresUtils";
describe("Select Test Suite", () => {
let client: Client;
let repository: ExampleRepository;
beforeAll(async () => {
config();
client = await rootConnect();
});
afterAll(async () => {
await client.end();
});
beforeEach(() => {
repository = new ExampleRepository(client);
});
afterEach(async () => {
await client.query("DELETE FROM example");
});
it("selects nothing", async () => {
expect(await repository.findAll()).toEqual([]);
});
});
src/تست/postgreUtils.ts
export const rootConnect = async () => connectToDatabase(process.env.DB_NAME);
const connectToDatabase = async (database: string) => {
const client = new Client({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database,
ssl: SSLMode.Disable,
});
await client.connect();
return client;
};
در حالی که می توانستیم مشخص کنیم --maxWorkers=1
به شوخی، این مدت زمان تست های ما را به شدت افزایش می دهد. یک رویکرد بهتر، فعال کردن تست یکپارچه سازی قابل اعتماد و ایزوله است، با اطمینان از اینکه هر مجموعه آزمایشی در برابر یک نمونه پایگاه داده مجزا و ایزوله اجرا می شود. برای رسیدن به این هدف، میتوانیم یک پایگاه داده جدید در طول تابع BeforeAll ایجاد کنیم.
با فرض اینکه مجموعه آزمایشی شما در یک پوشه است src/__test__
و اسکریپت های SQL مورد نیاز برای تنظیم جداول پایگاه داده شما موجود است src/sql
، راه حل به آسانی استفاده از اتصال ریشه برای ایجاد یک پایگاه داده جدید با نام UUID و سپس استفاده از آن اتصال برای اجرای اسکریپت های مهاجرت SQL است. برای رسیدن به این هدف، میتوانیم یک کمککننده ساده ایجاد کنیم که به ما امکان میدهد تنها یک تابع را در مجموعههای آزمایشی خود فراخوانی کنیم. کد اصلاح شده در زیر نشان داده شده است.
src/تست/postgreUtils.ts
import { Client, SSLMode } from "ts-postgres";
import { v4 as uuid } from "uuid";
import { readdirSync, readFileSync } from "fs";
export const connectToTestDatabase = async () => {
let client = await rootConnect();
const database = uuid();
await client.query(`CREATE DATABASE "${database}"`);
await client.end();
client = await connectToDatabase(database);
await runMigrations(client);
return client;
};
export const rootConnect = async () => connectToDatabase(process.env.DB_NAME);
const connectToDatabase = async (database: string) => {
const client = new Client({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database,
ssl: SSLMode.Disable,
});
await client.connect();
return client;
};
const runMigrations = async (client: Client) => {
const migrationsPath = `${__dirname}/../../sql`;
for (const filePath of readdirSync(migrationsPath))
await runMigration(client, `${migrationsPath}/${filePath}`);
};
const runMigration = async (client: Client, filePath) => {
for (const query of queriesInMigrationFile(filePath))
await client.query(query);
};
/**
* :warning: - Fails if a query inserts data containing ";" character
* @param filePath
*/
const queriesInMigrationFile = (filePath: string) =>
readFileSync(filePath).toString().split(";");
با استفاده از توابع کمکی فوق، میتوانید اطمینان حاصل کنید که هر مجموعه آزمایشی بر اساس نمونه پایگاه داده خودش اجرا میشود، از مسائل همزمانی اجتناب میکند و امکان تست یکپارچهسازی قابل اعتماد و ایزوله را فراهم میکند.
به منظور بهبود عملکرد مهاجرتهای در حال اجرا، میتوانیم از یک سرویس گیرنده PostgreSQL استفاده کنیم. در حالی که راه حل قبلی شامل تجزیه سفارشی فایل های SQL ممکن است برای شما خوب کار کند، کلاینت مانند PostgreSQL می تواند کارایی بهتری ارائه دهد و از مهاجرت های شامل TRIGGERS برای مثال پشتیبانی می کند.
برای شروع، به سادگی با پیروی از دستورالعمل های زیر بسته به سیستم عامل خود، یک سرویس گیرنده PostgreSQL را نصب کنید:
- برای OS X، اجرا کنید
brew install libpq && brew link --force libpq
- برای اوبونتو/دبیان اجرا کنید
sudo apt install postgresql-client
نصب را با اجرا تایید کنید psql --version
در مرحله بعد، ما میتوانیم عملکرد کمکی runMigrations خود را برای اجرای هر فایل مهاجرت با استفاده از پیکربندی مشتری PostgreSQL تغییر دهیم. قطعه کد این مورد در زیر نشان داده شده است:
export const runMigrations: async (client: Client) => {
for (const filePath of readdirSync(migrationsPath))
child_process.execSync(
`PGPASSWORD="${client.config.password}" psql -h ${client.config.host} -p ${client.config.port} -U ${client.config.user} -d ${client.config.database} -a -f ${migrationsPath}/${filePath}`
);
};
برای پایان دادن به این مقاله، میخواهم دو نمونه از کارهای CI با استفاده از سرویس پایگاه داده PostgreSQL را به شما ارائه دهم، بنابراین تستهای شما را بر روی خط لوله CI/CD خودکار میکند.
با gitlab-ci (.gitlab-ci.yml)
stages:
- test
integration-tests:
stage: test
image: node:18.14.0
services:
- name: postgres:15.2
alias: postgres
variables:
POSTGRES_DB: jest-psql-example
POSTGRES_USER: postgres
POSTGRES_PASSWORD: example
POSTGRES_HOST_AUTH_METHOD: trust
DB_HOST: "postgres"
DB_PORT: "5432"
DB_USER: "postgres"
DB_PASSWORD: "example"
DB_NAME: "my-database"
before_script:
- apt-get update && apt-get install -y postgresql-client
- yarn
- for f in ./sql/*.sql; do psql -h ${DB_HOST} -U ${DB_USER} -d ${DB_NAME} -a -f $f > /dev/null; done
script:
- jest
با گردش کار github (.github/worflows/test.yaml)
name: test
on: [ push ]
jobs:
integration-tests:
runs-on: ubuntu-latest
container: node:18.14
services:
postgres:
image: postgres:15.2
env:
POSTGRES_DB: jest-psql-example
POSTGRES_USER: postgres
POSTGRES_PASSWORD: example
POSTGRES_HOST_AUTH_METHOD: trust
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install -g yarn
- run: yarn
- name: Run migrations
env:
DB_HOST: "postgres"
DB_USER: "postgres"
PGPASSWORD: "example"
DB_NAME: "my-database"
run: |
apt-get update
apt-get install --yes postgresql-client
for f in ./sql/*.sql; do psql -h ${DB_HOST} -U ${DB_USER} -d ${DB_NAME} -a -f $f > /dev/null; done
- name: Run integration tests
env:
DB_HOST: "postgres"
DB_PORT: "5432"
DB_USER: "postgres"
DB_PASSWORD: "example"
DB_NAME: "jest-psql-example"
run: jest
تمام نمونه کدها در مخزن Jest-PostgresSQL-Integration-Testing موجود هستند