Vitest کے ساتھ NestJS کی جانچ کرنا: یونٹ اور انٹیگریشن پیٹرنز
بغیر ٹیسٹ کے NestJS درخواست ایک ذمہ داری ہے۔ آپ اعتماد کے ساتھ ریفیکٹر نہیں کر سکتے، آپ موجودہ رویے کو توڑے بغیر فیچرز شامل نہیں کر سکتے، اور آپ ریگریشنز کو پروڈکشن تک پہنچنے سے پہلے پکڑ نہیں سکتے۔ لیکن خراب ٹیسٹ لکھنا بغیر ٹیسٹوں سے تقریباً بدتر ہے — سست ٹیسٹ جو کوئی نہیں چلاتا، عمل درآمد کی تفصیلات کے ساتھ مل کر ٹوٹنے والے ٹیسٹ، اور ایسے مضحکہ خیز ٹیسٹ جو حقیقی رویے کی عکاسی نہیں کرتے ہیں، سبھی ڈویلپر کے اعتماد کو ختم کرتے ہیں۔
یہ گائیڈ اس بات کا احاطہ کرتا ہے کہ NestJS کے لیے Vitest کا استعمال کرتے ہوئے تیز، قابل بھروسہ ٹیسٹ کیسے لکھیں — جدید Jest متبادل جو 1,300+ ٹیسٹوں کے لیے 30 سیکنڈ سے کم میں چلتا ہے۔ ہم مکمل اسپیکٹرم کا احاطہ کرتے ہیں: مضحکہ خیز انحصار کے ساتھ خالص یونٹ ٹیسٹ، حقیقی ڈیٹا بیس ٹرانزیکشن کے ساتھ انضمام کے ٹیسٹ، سیکیورٹی ٹیسٹ، اور CI کنفیگریشن جو یہ سب ہر کمٹ پر چلتا ہے۔
اہم ٹیک ویز
- Vitest Jest سے مطابقت رکھتا ہے لیکن 2-5x تیز — وہی API، کوئی منتقلی رگڑ نہیں
vi.mock()کے ساتھ عارضی ڈیڈ زون کے مسائل سے بچنے کے لیے فرضی فیکٹریوں کے لیےvi.hoisted()استعمال کریں- ٹیسٹ رویے، عمل درآمد نہیں - آؤٹ پٹ اور ضمنی اثرات پر زور دیں، اندرونی طریقہ کالوں پر نہیں۔
- بوندا باندی SELECT فرضی زنجیریں
.limit()کے ساتھ ختم ہونی چاہئیں، INSERT/UPDATE کے ساتھ.returning()- انٹیگریشن ٹیسٹوں میں حقیقی ڈیٹا بیس ٹرانزیکشنز کا استعمال کرنا چاہیے جو ہر ٹیسٹ کے بعد واپس لوٹ جاتے ہیں۔
- مناسب DI ٹیسٹنگ کے لیے NestJS سے
createTestingModuleاستعمال کریں، نہ کہ براہ راست کلاس انسٹی ٹیشن- سیکیورٹی ٹیسٹ (انجیکشن، تصدیق بائی پاس، شرح کی حد) ایک علیحدہ
__security__ڈائریکٹری میں ہیں- سروس لیئر کے لیے 80%+ لائن کوریج کا ہدف؛ تصنیف اور بلنگ ماڈیولز کے لیے 100%
Vitest کنفیگریشن
// apps/api/vitest.config.ts
import { defineConfig } from 'vitest/config';
import swc from 'unplugin-swc';
export default defineConfig({
test: {
globals: true,
root: './',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: [
'node_modules/**',
'dist/**',
'**/*.spec.ts',
'**/*.dto.ts', // DTOs are just type definitions
'**/index.ts', // Barrel exports
],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
},
},
reporters: ['verbose'],
maxConcurrency: 10,
pool: 'forks', // Better isolation for database tests
poolOptions: {
forks: { singleFork: false },
},
},
plugins: [
swc.vite({
module: { type: 'es6' },
}),
],
});
# Install
pnpm add -D vitest @vitest/coverage-v8 unplugin-swc @swc/core
# Run
cd apps/api && npx vitest run # All tests once
cd apps/api && npx vitest # Watch mode
cd apps/api && npx vitest run --coverage # With coverage report
یونٹ ٹیسٹ پیٹرن
سروس انڈر ٹیسٹ
// src/modules/contacts/contacts.service.ts
@Injectable()
export class ContactsService {
constructor(
@InjectDatabase() private db: Database,
private readonly cacheService: CacheService,
) {}
async findAll(organizationId: string, page = 1, limit = 20) {
const cached = await this.cacheService.get(`contacts:${organizationId}:${page}`);
if (cached) return cached;
const result = await this.db.query.contacts.findMany({
where: eq(contacts.organizationId, organizationId),
limit,
offset: (page - 1) * limit,
orderBy: desc(contacts.createdAt),
});
await this.cacheService.set(`contacts:${organizationId}:${page}`, result, 300);
return result;
}
async create(organizationId: string, dto: CreateContactDto) {
const [contact] = await this.db
.insert(contacts)
.values({ ...dto, organizationId })
.returning();
return contact;
}
}
یونٹ ٹیسٹ
// src/modules/contacts/contacts.service.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Test } from '@nestjs/testing';
import { ContactsService } from './contacts.service';
import { CacheService } from '../cache/cache.service';
// vi.hoisted() ensures mock factories run before vi.mock() calls
const mockDb = vi.hoisted(() => ({
query: {
contacts: {
findMany: vi.fn(),
},
},
insert: vi.fn(),
}));
const mockCache = vi.hoisted(() => ({
get: vi.fn(),
set: vi.fn(),
}));
vi.mock('@ecosire/db', () => ({ DATABASE_TOKEN: 'DATABASE_TOKEN' }));
describe('ContactsService', () => {
let service: ContactsService;
beforeEach(async () => {
vi.clearAllMocks();
const module = await Test.createTestingModule({
providers: [
ContactsService,
{
provide: 'DATABASE_TOKEN',
useValue: mockDb,
},
{
provide: CacheService,
useValue: mockCache,
},
],
}).compile();
service = module.get<ContactsService>(ContactsService);
});
describe('findAll', () => {
it('returns cached result when cache hit', async () => {
const cached = [{ id: '1', name: 'Alice' }];
mockCache.get.mockResolvedValue(cached);
const result = await service.findAll('org_1');
expect(result).toEqual(cached);
expect(mockDb.query.contacts.findMany).not.toHaveBeenCalled();
});
it('queries database on cache miss and caches result', async () => {
const dbResult = [{ id: '1', name: 'Bob', organizationId: 'org_1' }];
mockCache.get.mockResolvedValue(null);
// Drizzle SELECT chains end with .limit()
mockDb.query.contacts.findMany.mockResolvedValue(dbResult);
const result = await service.findAll('org_1');
expect(result).toEqual(dbResult);
expect(mockCache.set).toHaveBeenCalledWith(
'contacts:org_1:1',
dbResult,
300
);
});
});
describe('create', () => {
it('inserts contact and returns created record', async () => {
const newContact = { id: '2', name: 'Carol', organizationId: 'org_1' };
// Drizzle INSERT chains end with .returning()
const returning = vi.fn().mockResolvedValue([newContact]);
const values = vi.fn().mockReturnValue({ returning });
mockDb.insert.mockReturnValue({ values });
const result = await service.create('org_1', { name: 'Carol' });
expect(result).toEqual(newContact);
expect(mockDb.insert).toHaveBeenCalled();
});
});
});
بوندا باندی چین کے موکس کے لیے پھر قابل پراکسی
بوندا باندی کے سوالات روانی کی زنجیریں ہیں۔ انضمام کے ٹیسٹ کے منظرناموں کے لیے جہاں آپ کو مکمل چین کا مذاق اڑانے کی ضرورت ہے:
// test/helpers/drizzle-mock.ts
export function createDrizzleChainMock(resolveValue: unknown) {
const handler: ProxyHandler<object> = {
get(target, prop) {
if (prop === 'then') {
// Make the chain thenable so await works
return (resolve: (v: unknown) => void) => resolve(resolveValue);
}
// Return a proxy that returns itself for any method call
return (..._args: unknown[]) => new Proxy({}, handler);
},
};
return new Proxy({}, handler);
}
// Usage in tests
mockDb.select.mockReturnValue(createDrizzleChainMock([{ id: '1' }]));
اصلی ڈیٹا بیس کے ساتھ انٹیگریشن ٹیسٹ
// src/modules/__integration__/contacts.integration.spec.ts
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { Test } from '@nestjs/testing';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import * as schema from '@ecosire/db/schema';
import { ContactsModule } from '../contacts/contacts.module';
import { ContactsService } from '../contacts/contacts.service';
let sql: ReturnType<typeof postgres>;
let db: ReturnType<typeof drizzle>;
let service: ContactsService;
beforeAll(async () => {
// Use a separate test database
const testDbUrl = process.env.TEST_DATABASE_URL ||
'postgres://postgres:postgres@localhost:5433/ecosire_test';
sql = postgres(testDbUrl, { max: 1 });
db = drizzle(sql, { schema });
// Run migrations on test DB
await migrate(db, { migrationsFolder: './drizzle' });
const module = await Test.createTestingModule({
imports: [ContactsModule],
})
.overrideProvider('DATABASE_TOKEN')
.useValue(db)
.compile();
service = module.get<ContactsService>(ContactsService);
});
afterAll(async () => {
await sql.end();
});
beforeEach(async () => {
// Clean up test data between tests
await db.delete(schema.contacts);
});
describe('ContactsService (integration)', () => {
it('creates and retrieves a contact from the real database', async () => {
const created = await service.create('org_test_1', {
name: 'Integration Test User',
email: '[email protected]',
});
expect(created.id).toBeDefined();
expect(created.name).toBe('Integration Test User');
const all = await service.findAll('org_test_1');
expect(all).toHaveLength(1);
expect(all[0].id).toBe(created.id);
});
it('scopes results by organizationId (multi-tenancy)', async () => {
await service.create('org_1', { name: 'Org 1 Contact' });
await service.create('org_2', { name: 'Org 2 Contact' });
const org1Contacts = await service.findAll('org_1');
const org2Contacts = await service.findAll('org_2');
expect(org1Contacts).toHaveLength(1);
expect(org2Contacts).toHaveLength(1);
expect(org1Contacts[0].name).toBe('Org 1 Contact');
});
});
کنٹرولر ٹیسٹ
// src/modules/contacts/contacts.controller.spec.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Test } from '@nestjs/testing';
import { ContactsController } from './contacts.controller';
import { ContactsService } from './contacts.service';
describe('ContactsController', () => {
let controller: ContactsController;
const mockService = {
findAll: vi.fn(),
create: vi.fn(),
update: vi.fn(),
remove: vi.fn(),
};
beforeEach(async () => {
vi.clearAllMocks();
const module = await Test.createTestingModule({
controllers: [ContactsController],
providers: [
{ provide: ContactsService, useValue: mockService },
],
}).compile();
controller = module.get<ContactsController>(ContactsController);
});
it('calls service.findAll with organizationId from authenticated user', async () => {
const mockContacts = [{ id: '1', name: 'Test' }];
mockService.findAll.mockResolvedValue(mockContacts);
const mockUser = { id: 'user_1', organizationId: 'org_1', role: 'admin' };
const result = await controller.findAll(
{ user: mockUser } as AuthenticatedRequest,
1,
20
);
expect(result).toEqual(mockContacts);
expect(mockService.findAll).toHaveBeenCalledWith('org_1', 1, 20);
});
});
سیکیورٹی ٹیسٹ
// src/modules/__security__/injection.security.spec.ts
import { describe, it, expect } from 'vitest';
import request from 'supertest';
describe('SQL Injection Prevention', () => {
const injectionPayloads = [
"'; DROP TABLE contacts; --",
"' OR '1'='1",
"1; SELECT * FROM users",
"' UNION SELECT * FROM refresh_tokens --",
];
it.each(injectionPayloads)(
'should safely handle SQL injection attempt: %s',
async (payload) => {
const response = await request(app.getHttpServer())
.get('/contacts')
.query({ search: payload })
.set('Authorization', `Bearer ${validToken}`);
// Should return 200 with empty results or 400 for invalid input
// Should NEVER return 500 (which would indicate unhandled SQL error)
expect(response.status).not.toBe(500);
expect(response.body).not.toMatchObject({
message: expect.stringContaining('syntax error'),
});
}
);
});
describe('Authentication Bypass Prevention', () => {
it('should reject requests without authentication', async () => {
const response = await request(app.getHttpServer())
.get('/admin/contacts');
expect(response.status).toBe(401);
});
it('should reject tampered JWT', async () => {
const tamperedToken = validToken.slice(0, -5) + 'xxxxx';
const response = await request(app.getHttpServer())
.get('/contacts')
.set('Authorization', `Bearer ${tamperedToken}`);
expect(response.status).toBe(401);
});
it('should reject expired JWT', async () => {
// Token with exp in the past
const expiredToken = generateTokenWithExpiry(-3600);
const response = await request(app.getHttpServer())
.get('/contacts')
.set('Authorization', `Bearer ${expiredToken}`);
expect(response.status).toBe(401);
});
});
CI پائپ لائن کنفیگریشن
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 10
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm build --filter=@ecosire/db --filter=@ecosire/types
- name: Run unit tests
run: cd apps/api && npx vitest run --coverage
env:
NODE_ENV: test
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: apps/api/coverage/lcov.info
integration-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_DB: ecosire_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- run: pnpm install --frozen-lockfile
- run: cd apps/api && npx vitest run src/modules/__integration__
env:
TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/ecosire_test
اکثر پوچھے گئے سوالات
NestJS کے لیے Jest پر کیوں ووٹ دیں؟
Vitest Babel کے بجائے Vite کے تیز بنڈلر (esbuild/Rollup) کا استعمال کرتا ہے، جس سے 2-5x تیزی سے ٹیسٹ کی تکمیل ہوتی ہے۔ اس میں Jest-compatible API ہے — زیادہ تر Jest ٹیسٹ بغیر کسی تبدیلی کے منتقل ہو جاتے ہیں۔ Vitest میں مقامی ESM سپورٹ (جدید پیکجز کے لیے اہم)، ts-jest کے بغیر بلٹ ان TypeScript، اور بہتر واچ موڈ ہے۔ NestJS کے لیے، اہم فائدہ واچ موڈ میں ڈرامائی طور پر تیز فیڈ بیک لوپس ہے۔
فرضی فیکٹریوں کے لیے vi.hoisted() کیوں استعمال کریں؟
vi.mock() کالز کو فائل کے اوپری حصے پر وائٹسٹ ٹرانسفارمر کے ذریعے لہرایا جاتا ہے۔ اگر آپ کی فرضی فیکٹری فائل میں بعد میں متعین کردہ متغیر کا حوالہ دیتی ہے (جیسے const mockDb = { ... })، یہ متغیر کے شروع ہونے سے پہلے چلتا ہے — ایک عارضی ڈیڈ زون کی خرابی۔ vi.hoisted() قدریں تخلیق کرتا ہے جو لہرایا بھی جاتا ہے، لہذا جب فرضی فیکٹری چلتی ہے تو وہ دستیاب ہوتی ہیں۔
میں NestJS گارڈز اور انٹرسیپٹرز کی جانچ کیسے کروں؟
تنہائی میں گارڈ کو نظرانداز کرنے یا جانچنے کے لیے createTestingModule کے ساتھ overrideGuard() کا استعمال کرتے ہوئے انٹیگریشن ٹیسٹ بنائیں۔ یہ جانچنے کے لیے کہ گارڈ دراصل غیر مجاز درخواستوں کو مسترد کرتا ہے، غلط یا گمشدہ ٹوکنز کے ساتھ مکمل ایپلیکیشن (app.getHttpServer()) کے خلاف Supertest استعمال کریں، اور درست 401/403 اسٹیٹس کوڈ پر زور دیں۔
کیا بوندا باندی کی فرضی زنجیریں .limit() یا .returning() کے ساتھ ختم ہونی چاہئیں؟
SELECT سوالات کے لیے، سلسلہ عام طور پر findMany()، findFirst()، یا .limit() کال کے ساتھ ختم ہوتا ہے۔ values().returning() کے ساتھ INSERT/UPDATE/DELETE کے لیے، سلسلہ .returning() کے ساتھ ختم ہوتا ہے۔ سلسلہ کے سب سے گہرے طریقہ کا مذاق اڑائیں — جو بھی سروس کوڈ awaits آخری ہو۔ پھر قابل پراکسی پیٹرن من مانی گہرائی کی زنجیروں کو ہینڈل کرتا ہے۔
میں طے شدہ NestJS ٹاسکس (@Cron decorators) کی جانچ کیسے کروں؟
شیڈولر کو اپنے ٹیسٹ ماڈیول میں داخل کریں اور کام کے طریقہ کار کو براہ راست کال کریں — کرون ٹائمر کا انتظار نہ کریں۔ اگر آپ کو ٹائمنگ رویے کو جانچنے کی ضرورت ہو تو jest.useFakeTimers() (vi.useFakeTimers() کے ساتھ Vitest میں وہی API) استعمال کریں۔ یہ جانچنے کے لیے کہ کوئی کام طے شدہ تھا، تصدیق کریں کہ @nestjs/schedule SchedulerRegistry میں متوقع جاب موجود ہے۔
NestJS کے لیے تجویز کردہ ٹیسٹ ڈائرکٹری ڈھانچہ کیا ہے؟
ماخذ فائلوں کے ساتھ اکائی کے ٹیسٹ کو تلاش کریں (contacts.service.spec.ts کے آگے contacts.service.ts)۔ انضمام کے ٹیسٹ src/modules/__integration__/ میں اور سیکورٹی ٹیسٹ src/modules/__security__/ میں رکھیں۔ یہ علیحدگی آپ کو ترقی (vitest run --exclude **/__integration__/**) کے دوران صرف یونٹ ٹیسٹ اور CI میں تمام ٹیسٹ چلانے دیتی ہے۔
اگلے اقدامات
ایک جامع ٹیسٹ سویٹ عیش و آرام کی چیز نہیں ہے — یہ وہ بنیادی ڈھانچہ ہے جو آپ کو اعتماد کے ساتھ تیزی سے جہاز بھیجنے کی اجازت دیتا ہے۔ Vitest کے 30 سیکنڈ سے کم میں 1,300 ٹیسٹ چلانے کے ساتھ، آگے بڑھانے سے پہلے ٹیسٹ رن کو چھوڑنے کا کوئی عذر نہیں ہے۔
ECOSIRE پہلے دن سے مکمل ٹیسٹ کوریج کے ساتھ NestJS بیک اینڈز بناتا ہے — 76 ٹیسٹ فائلیں، 1,301 یونٹ اور انٹیگریشن ٹیسٹ، اور انجیکشن، تصدیق بائی پاس، اور شرح کو محدود کرنے کے لیے سیکیورٹی ٹیسٹ کوریج۔ ہماری بیک اینڈ انجینئرنگ سروسز کو دریافت کریں یہ جاننے کے لیے کہ ہم کس طرح آزمائشی، پروڈکشن کے لیے تیار APIs فراہم کرتے ہیں۔
تحریر
ECOSIRE Research and Development Team
ECOSIRE میں انٹرپرائز گریڈ ڈیجیٹل مصنوعات بنانا۔ Odoo انٹیگریشنز، ای کامرس آٹومیشن، اور AI سے چلنے والے کاروباری حل پر بصیرت شیئر کرنا۔
متعلقہ مضامین
AI + ERP Integration: How AI is Transforming Enterprise Resource Planning
Learn how AI is transforming ERP systems in 2026—from intelligent automation and predictive analytics to natural language interfaces and autonomous operations.
All-in-One vs Best-of-Breed: The Software Stack Decision
All-in-one vs best-of-breed software strategy for 2026: integration complexity, total cost, vendor risk, and when each approach is right for your business.
The API Economy: Building an Integration-First Business
How to leverage the API economy for competitive advantage—building integration-first architecture, monetizing APIs, selecting iPaaS platforms, and creating ecosystem value.