تست نویسی با Jest در Node.js
Jest یکی از محبوبترین فریمورکهای تست برای اکوسیستم JavaScript و مخصوصاً Node.js است. در این مقاله به صورت عملی و کاربردی مفهوم تستهای واحد و یکپارچه، تنظیم محیط، تستهای همگام و ناهمگام، موکسازی (mocking)، تستهای Snapshot و نکات حرفهای برای نگهداری تستها را بررسی میکنیم.
چرا Jest برای Node.js؟
- راهاندازی ساده و صفر-پیکربندی برای بسیاری از پروژهها.
- پشتیبانی از Mocking داخلی و Coverage گزارش.
- سرعت خوب و اجرا در حالت watch برای توسعه سریع.
- سازگاری با TypeScript، Babel و ابزارهای CI.
نصب و پیکربندی اولیه
npm init -y
npm install --save-dev jest
# اگر از TypeScript استفاده میکنید:
npm install --save-dev ts-jest @types/jest
در فایل package.json کافی است script مربوط به تست را اضافه کنید:
{
"scripts": {
"test": "jest --coverage"
}
}این تنظیم ساده اجازه میدهد با اجرای npm test تستها اجرا شده و گزارش پوشش کد (coverage) تولید شود.
نوشتن اولین تست واحد
// sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
// sum.test.js
const sum = require('./sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
در این مثال تابع سادهای داریم و یک فایل تست که با تابع global jest یعنی test تعریف میشود. matcher های Jest مانند toBe برای مقایسه دقیق استفاده میشوند.
تست توابع ناهمگام (Promises / async-await)
// fetchData.js
const fetch = require('node-fetch');
async function fetchData(url) {
const res = await fetch(url);
return res.json();
}
module.exports = fetchData;
// fetchData.test.js
const fetchData = require('./fetchData');
test('fetches data from API', async () => {
const data = await fetchData('https://jsonplaceholder.typicode.com/todos/1');
expect(data).toHaveProperty('id', 1);
});
در تست بالا از async/await استفاده شده تا فراخوانی ناهمگام به شکل ساده تست شود. در تستهای مبتنی بر Promise میتوان از return یا resolves/rejects نیز استفاده کرد.
موکسازی: از توابع ساده تا ماژولها
// db.js
module.exports = {
getUser: async (id) => {
// تماس با دیتابیس واقعی
}
};
// userService.js
const db = require('./db');
async function getUserName(id) {
const user = await db.getUser(id);
return user.name;
}
module.exports = getUserName;
// userService.test.js
jest.mock('./db');
const db = require('./db');
const getUserName = require('./userService');
test('returns user name from mocked db', async () => {
db.getUser.mockResolvedValue({ id: 1, name: 'Ali' });
const name = await getUserName(1);
expect(name).toBe('Ali');
});
با jest.mock یک ماژول را جایگزین میکنیم و رفتار توابعش را کنترل میکنیم. این کار باعث سریعتر و ایزولهتر شدن تستها میشود و وابستگی به دیتابیس یا سرویس خارجی را حذف میکند.
تست مسیرهای HTTP با Supertest
// app.js
const express = require('express');
const app = express();
app.get('/ping', (req, res) => res.json({ message: 'pong' }));
module.exports = app;
// app.test.js
const request = require('supertest');
const app = require('./app');
test('GET /ping returns pong', async () => {
const res = await request(app).get('/ping');
expect(res.statusCode).toBe(200);
expect(res.body).toEqual({ message: 'pong' });
});
برای تست APIهای Express، supertest ابزار مناسبی است که امکان اجرای درخواست و بررسی پاسخ بدون باز کردن پورت واقعی را میدهد.
تست Snapshot
// renderer.js
function renderUser(user) {
return `User: ${user.name} (${user.id})`;
}
module.exports = renderUser;
// renderer.test.js
const renderUser = require('./renderer');
test('renders user snapshot', () => {
const output = renderUser({ id: 1, name: 'Sara' });
expect(output).toMatchSnapshot();
});
Snapshot برای تست ساختار خروجی (بهخصوص در ریاکت یا خروجیهای رشتهای بزرگ) مفید است. در اولین اجرا فایل snapshot ایجاد میشود و اجرای بعدی اختلافها را نشان میدهد.
گزارش پوشش کد (Coverage)
اجرای jest با فلگ –coverage یک گزارش پوشش تولید میکند. میتوانید آن را در CI بگذارید و حداقل پوشش مورد نیاز را تعریف کنید.
| ابزار | هدف |
|---|---|
| Jest | اجرای تستها، mocking، snapshot |
| Supertest | تست endpoint های HTTP |
نکات حرفهای و بهترین رویهها
- هر تست باید مستقل باشد؛ از دادههای سراسری مشترک خودداری کنید.
- فایلهای تست را با پسوند .test.js یا .spec.js نامگذاری کنید و ساختار پوشهای منظمی داشته باشید.
- از mock کردن external API و دیتابیس استفاده کنید تا تستها سریع و قابل پیشبینی بمانند.
- برای تستهای وقتی-حسگر (time-sensitive) از jest.useFakeTimers() استفاده کنید تا تایمرها قابل کنترل شوند.
- در CI، اجرای coverage و fail کردن pipeline در صورت کمتر بودن از حد آستانه میتواند کیفیت را تضمین کند.
مثال پیشرفته: استفاده از timers موکشده
// timer.js
function delayedHello(cb) {
setTimeout(() => {
cb('hello');
}, 1000);
}
module.exports = delayedHello;
// timer.test.js
const delayedHello = require('./timer');
jest.useFakeTimers();
test('calls callback after 1 second', () => {
const cb = jest.fn();
delayedHello(cb);
expect(cb).not.toBeCalled();
jest.advanceTimersByTime(1000);
expect(cb).toBeCalledWith('hello');
});
در این تست تایمرها را موک میکنیم تا بدون انتظار واقعی بتوانیم رفتار تابع را بررسی کنیم. این الگو در تست زمانبندی و بکآفها بسیار مفید است.
سازماندهی و نگهداری تستها
بهمرور زمان تعداد تستها زیاد میشود. برخی توصیهها:
- تستهای کند را جدا کنید و تنها در حالت CI یا با فلگ خاص اجرا کنید.
- از fixture ها برای مقداردهی اولیه استفاده کنید و setup/teardown را در beforeEach/afterEach مدیریت کنید.
- خطاهای ناپایدار (flaky tests) را به سرعت اصلاح کنید، زیرا اعتماد به مجموعه تست کاهش مییابد.
جمعبندی و منابع
Jest یک ابزار کامل برای تست در Node.js است که با ابزارهای دیگر بهخوبی ترکیب میشود. با استفاده از Mocking، تستهای ناهمگام، Snapshot و Coverage میتوانید یک مجموعه تست قابلاعتماد و سریع بسازید. برای یادگیری بیشتر مستندات رسمی Jest و مثالهای پیشرفته را مطالعه کنید.
اگر بخش خاصی از پروژهتان را میخواهید با Jest تستکنید (مثلاً Express، TypeScript یا microservices)، میتوانم نمونههای اختصاصی و تنظیمات CI برای شما آماده کنم.
آیا این مطلب برای شما مفید بود ؟




