Vitest के साथ NestJS का परीक्षण: यूनिट और एकीकरण पैटर्न
परीक्षण के बिना NestJS एप्लिकेशन एक दायित्व है। आप आत्मविश्वास से रिफैक्टर नहीं कर सकते, आप मौजूदा व्यवहार को तोड़े बिना सुविधाएँ नहीं जोड़ सकते, और आप उत्पादन तक पहुँचने से पहले प्रतिगमन को नहीं पकड़ सकते। लेकिन खराब परीक्षण लिखना, बिना परीक्षण के लिखने से भी बदतर है - धीमे परीक्षण जो कोई भी नहीं चलाता है, कार्यान्वयन विवरण के साथ भंगुर परीक्षण, और नकली परीक्षण जो वास्तविक व्यवहार को प्रतिबिंबित नहीं करते हैं, ये सभी डेवलपर के विश्वास को कमजोर करते हैं।
यह मार्गदर्शिका बताती है कि विटेस्ट का उपयोग करके NestJS के लिए तेज़, विश्वसनीय परीक्षण कैसे लिखें - आधुनिक जेस्ट प्रतिस्थापन जो 1,300+ परीक्षणों के लिए 30 सेकंड से कम समय में चलता है। हम पूर्ण स्पेक्ट्रम को कवर करते हैं: नकली निर्भरता के साथ शुद्ध इकाई परीक्षण, वास्तविक डेटाबेस लेनदेन के साथ एकीकरण परीक्षण, सुरक्षा परीक्षण और सीआई कॉन्फ़िगरेशन जो इसे हर प्रतिबद्धता पर चलाता है।
मुख्य बातें
- विटेस्ट जेस्ट-संगत है लेकिन 2-5 गुना तेज है - समान एपीआई, कोई माइग्रेशन घर्षण नहीं
vi.mock()के साथ अस्थायी मृत क्षेत्र के मुद्दों से बचने के लिए नकली कारखानों के लिएvi.hoisted()का उपयोग करें- व्यवहार का परीक्षण करें, कार्यान्वयन नहीं - आउटपुट और साइड इफेक्ट्स पर जोर दें, आंतरिक विधि कॉल पर नहीं
- ड्रिज़ल सेलेक्ट मॉक चेन
.limit()के साथ समाप्त होनी चाहिए, INSERT/अपडेट.returning()के साथ होनी चाहिए- एकीकरण परीक्षणों में प्रत्येक परीक्षण के बाद वास्तविक डेटाबेस लेनदेन का उपयोग किया जाना चाहिए
- उचित DI परीक्षण के लिए NestJS से
createTestingModuleका उपयोग करें, न कि प्रत्यक्ष वर्ग इन्स्टेन्शियेशन का- सुरक्षा परीक्षण (इंजेक्शन, ऑथ बायपास, दर सीमा) एक अलग
__security__निर्देशिका में हैं- सर्विस लेयर के लिए लक्ष्य 80%+ लाइन कवरेज; प्रमाणीकरण और बिलिंग मॉड्यूल के लिए 100%
विटेस्ट कॉन्फ़िगरेशन
// 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);
});
});
सीआई पाइपलाइन कॉन्फ़िगरेशन
# .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
अक्सर पूछे जाने वाले प्रश्न
नेस्टजेएस के लिए जेस्ट की तुलना में विटेस्ट क्यों?
विटेस्ट बैबेल के बजाय वाइट के तेज़ बंडलर (एसबिल्ड/रोलअप) का उपयोग करता है, जो 2-5x तेज़ परीक्षण निष्पादन देता है। इसमें एक जेस्ट-संगत एपीआई है - अधिकांश जेस्ट परीक्षण बिना किसी बदलाव के माइग्रेट होते हैं। विटेस्ट में मूल ईएसएम समर्थन (आधुनिक पैकेजों के लिए महत्वपूर्ण), ts-jest के बिना अंतर्निहित टाइपस्क्रिप्ट और बेहतर वॉच मोड है। NestJS के लिए, मुख्य लाभ वॉच मोड में नाटकीय रूप से तेज़ फीडबैक लूप है।
नकली कारखानों के लिए vi.hoisted() का उपयोग क्यों करें?
vi.mock() कॉल्स को विटेस्ट ट्रांसफार्मर द्वारा फ़ाइल के शीर्ष पर फहराया जाता है। यदि आपकी मॉक फ़ैक्टरी फ़ाइल में बाद में परिभाषित एक वेरिएबल (जैसे const mockDb = { ... }) का संदर्भ देती है, तो यह वेरिएबल आरंभ होने से पहले चलता है - एक अस्थायी डेड ज़ोन त्रुटि। vi.hoisted() ऐसे मान बनाता है जिन्हें फहराया भी जाता है, इसलिए जब मॉक फ़ैक्टरी चलती है तो वे उपलब्ध होते हैं।
मैं NestJS गार्ड और इंटरसेप्टर का परीक्षण कैसे करूं?
गार्ड को बायपास करने या अलगाव में परीक्षण करने के लिए overrideGuard() के साथ createTestingModule का उपयोग करके एक एकीकरण परीक्षण बनाएं। यह जांचने के लिए कि कोई गार्ड वास्तव में अनधिकृत अनुरोधों को अस्वीकार करता है, अमान्य या गायब टोकन वाले पूर्ण एप्लिकेशन (app.getHttpServer()) के खिलाफ सुपरटेस्ट का उपयोग करें, और सही 401/403 स्थिति कोड का दावा करें।
क्या ड्रिज़ल मॉक चेन का अंत .limit() या .returning() के साथ होना चाहिए?
SELECT क्वेरीज़ के लिए, श्रृंखला आम तौर पर findMany(), findFirst(), या .limit() कॉल के साथ समाप्त होती है। values().returning() के साथ सम्मिलित/अद्यतन/हटाएं के लिए, श्रृंखला .returning() के साथ समाप्त होती है। श्रृंखला में सबसे गहरी विधि का अनुकरण करें - चाहे सेवा कोड awaits अंतिम हो। तत्कालीन प्रॉक्सी पैटर्न मनमानी गहराई की श्रृंखलाओं को संभालता है।
मैं निर्धारित NestJS कार्यों (@Cron डेकोरेटर्स) का परीक्षण कैसे करूं?
शेड्यूलर को अपने परीक्षण मॉड्यूल में इंजेक्ट करें और कार्य विधि को सीधे कॉल करें - क्रॉन टाइमर की प्रतीक्षा न करें। यदि आपको समय व्यवहार का परीक्षण करने की आवश्यकता है तो jest.useFakeTimers() (विटेस्ट में vi.useFakeTimers() के साथ समान एपीआई) का उपयोग करें। यह जांचने के लिए कि कोई कार्य शेड्यूल किया गया था, सत्यापित करें कि @nestjs/schedule शेड्यूलर रजिस्ट्री में अपेक्षित कार्य शामिल है।
NestJS के लिए अनुशंसित परीक्षण निर्देशिका संरचना क्या है?
स्रोत फ़ाइलों के साथ इकाई परीक्षणों का सह-स्थान लगाएं (contacts.service.spec.ts contacts.service.ts के आगे)। src/modules/__integration__/ में एकीकरण परीक्षण और src/modules/__security__/ में सुरक्षा परीक्षण रखें। यह पृथक्करण आपको विकास के दौरान केवल इकाई परीक्षण (vitest run --exclude **/__integration__/**) और CI में सभी परीक्षण चलाने की सुविधा देता है।
अगले चरण
एक व्यापक परीक्षण सूट कोई विलासिता नहीं है - यह बुनियादी ढांचा है जो आपको आत्मविश्वास के साथ तेजी से जहाज चलाने की अनुमति देता है। विटेस्ट द्वारा 30 सेकंड से कम समय में 1,300 परीक्षण चलाने के साथ, आगे बढ़ने से पहले परीक्षण चलाने को छोड़ने का कोई बहाना नहीं है।
ECOSIRE पहले दिन से पूर्ण परीक्षण कवरेज के साथ NestJS बैकएंड बनाता है - 76 परीक्षण फ़ाइलें, 1,301 इकाई और एकीकरण परीक्षण, और इंजेक्शन, ऑथ बायपास और दर सीमित करने के लिए सुरक्षा परीक्षण कवरेज। हमारी बैकएंड इंजीनियरिंग सेवाओं का अन्वेषण करें यह जानने के लिए कि हम परीक्षणित, उत्पादन-तैयार एपीआई कैसे वितरित करते हैं।
लेखक
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.