تست‌نویسی در جاوا اسکریپت: راهنمای Jest و Mocha

فهرست مطالب

تست‌نویسی در جاوا اسکریپت: راهنمای جامع Jest و Mocha برای توسعه‌دهندگان پیشرفته

در اکوسیستم پویای جاوا اسکریپت، جایی که کتابخانه‌ها و فریمورک‌ها با سرعت نور در حال تکامل هستند، اطمینان از کیفیت و پایداری کد بیش از هر زمان دیگری حیاتی است. تست‌نویسی، نه تنها یک اقدام پیشگیرانه برای جلوگیری از باگ‌ها و خطاهاست، بلکه ستونی اساسی برای حفظ قابلیت نگهداری، تسهیل بازآرایی (refactoring) و افزایش اعتماد به نفس توسعه‌دهندگان در فرآیند استقرار محسوب می‌شود. یک codebase تست‌شده به خوبی، به تیم‌ها امکان می‌دهد تا با اطمینان خاطر ویژگی‌های جدید اضافه کنند، کدهای موجود را بهینه سازند و با تغییرات وابستگی‌ها یا الزامات کسب‌وکار سازگار شوند، بدون اینکه نگران شکستن عملکردهای حیاتی باشند.

در این راهنمای جامع و تخصصی، ما به عمق مفهوم تست‌نویسی در جاوا اسکریپت خواهیم رفت. هدف ما فراتر از معرفی ابزارهاست؛ ما قصد داریم معماری تست، الگوهای طراحی تست، و رویکردهای پیشرفته‌ای را بررسی کنیم که به شما کمک می‌کند تست‌های مؤثر، قابل نگهداری و قابل اعتماد بنویسید. تمرکز اصلی ما بر روی دو فریمورک پیشرو در حوزه تست جاوا اسکریپت، یعنی Jest و Mocha خواهد بود. در حالی که هر دو ابزارهای قدرتمندی برای اجرای تست‌ها ارائه می‌دهند، رویکردها، فلسفه‌ها و اکوسیستم‌های حمایتی آن‌ها تفاوت‌های کلیدی دارند که درک آن‌ها برای انتخاب ابزار مناسب برای پروژه شما ضروری است.

این مطلب برای توسعه‌دهندگانی طراحی شده است که درک پایه‌ای از جاوا اسکریپت و مفاهیم توسعه نرم‌افزار دارند و اکنون به دنبال تعمیق دانش خود در زمینه تست‌نویسی هستند. ما از مفاهیم پایه تا تکنیک‌های پیشرفته، از جمله تست واحدهای ناهمگام (asynchronous unit testing)، شبیه‌سازی (mocking)، جاسوسی (spying) و تست اسنپ‌شات (snapshot testing) را پوشش خواهیم داد. با ما همراه باشید تا به دنیای تست‌نویسی جاوا اسکریپت شیرجه بزنیم و کدی بنویسیم که نه تنها کار می‌کند، بلکه قابل اعتماد و پایدار است.

درک مبانی تست‌نویسی در جاوا اسکریپت: چرا، چه و چگونه

قبل از اینکه به جزئیات پیاده‌سازی Jest و Mocha بپردازیم، لازم است درک محکمی از مبانی تست‌نویسی در جاوا اسکریپت و چرایی اهمیت آن داشته باشیم. تست‌نویسی یک فرآیند ایزوله نیست، بلکه جزئی جدایی‌ناپذیر از چرخه عمر توسعه نرم‌افزار مدرن است.

چرا تست می‌کنیم؟ مزایای استراتژیک

  • افزایش اعتماد به نفس: تست‌ها به عنوان یک شبکه ایمنی عمل می‌کنند. هنگامی که کد را تغییر می‌دهید، بازآرایی می‌کنید یا ویژگی‌های جدید اضافه می‌کنید، تست‌های موجود به شما اطمینان می‌دهند که تغییرات شما به طور ناخواسته عملکردهای موجود را مختل نکرده‌اند.
  • کاهش باگ‌ها و هزینه‌ها: تشخیص باگ‌ها در مراحل اولیه توسعه بسیار ارزان‌تر از کشف آن‌ها پس از استقرار در محیط پروداکشن است. تست‌های خودکار باگ‌ها را سریع‌تر شناسایی می‌کنند.
  • مستندسازی زنده: تست‌ها، به ویژه تست‌های واحد، می‌توانند به عنوان مستندات اجرایی برای کدهای شما عمل کنند. آن‌ها نشان می‌دهند که هر واحد از کد چه کاری انجام می‌دهد و چگونه باید از آن استفاده کرد.
  • طراحی بهتر کد: برای اینکه یک قطعه کد قابل تست باشد، معمولاً باید دارای coupling کم و cohesion بالا باشد. این منجر به طراحی ماژولارتر و خواناتر می‌شود.
  • تسهیل بازآرایی: با وجود پوشش تست مناسب، می‌توانید با اطمینان خاطر کدهای قدیمی را بهینه‌سازی یا ساختار آن‌ها را تغییر دهید، زیرا تست‌ها هرگونه رگرسیون را آشکار خواهند کرد.
  • پشتیبانی از توسعه تیم: در تیم‌های بزرگ، تست‌ها به هماهنگی کمک می‌کنند. تغییرات یک توسعه‌دهنده در یک ماژول، توسط تست‌ها بررسی می‌شود تا اطمینان حاصل شود که با تغییرات دیگر تداخل ندارد.

هرم تست: استراتژی‌های تست‌نویسی

هرم تست (Test Pyramid) یک استعاره محبوب است که انواع مختلف تست‌ها و تناسب آن‌ها را در یک پروژه نشان می‌دهد:

  1. تست‌های واحد (Unit Tests):
    • تعریف: کوچکترین بخش‌های قابل تست برنامه (مانند یک تابع، یک کامپوننت، یک ماژول) را به صورت ایزوله بررسی می‌کنند.
    • ویژگی‌ها: سریع، ایزوله، تعداد بالا، هزینه کم.
    • هدف: اطمینان از صحت عملکرد هر جزء به تنهایی.
  2. تست‌های یکپارچه‌سازی (Integration Tests):
    • تعریف: نحوه تعامل چندین واحد یا سیستم با یکدیگر را بررسی می‌کنند. به عنوان مثال، تعامل یک سرویس با پایگاه داده یا یک کامپوننت UI با یک API.
    • ویژگی‌ها: کندتر از تست‌های واحد، تعداد متوسط، هزینه متوسط.
    • هدف: اطمینان از اینکه بخش‌های مختلف برنامه به درستی با هم کار می‌کنند.
  3. تست‌های End-to-End (E2E Tests):
    • تعریف: کل جریان کار کاربر را از ابتدا تا انتها شبیه‌سازی می‌کنند، از رابط کاربری گرفته تا پایگاه داده و سرویس‌های خارجی.
    • ویژگی‌ها: بسیار کند، تعداد کم، هزینه بالا، نیازمند محیط واقعی یا نزدیک به واقعی.
    • هدف: اطمینان از اینکه کل سیستم به درستی از دیدگاه کاربر نهایی کار می‌کند.

پایین هرم (تست واحد) بیشترین تعداد تست را با سریعترین زمان اجرا نشان می‌دهد، در حالی که بالای هرم (تست E2E) کمترین تعداد تست را با طولانی‌ترین زمان اجرا دارد. یک استراتژی تست‌نویسی مؤثر باید این تعادل را رعایت کند.

مفاهیم کلیدی در تست‌نویسی

  • Assertion Library (کتابخانه ادعا): ابزاری که برای نوشتن “ادعاها” یا “گزاره‌ها” استفاده می‌شود، یعنی بررسی اینکه آیا یک مقدار یا وضعیت خاص، مطابق انتظار است یا خیر. مثال‌ها: Chai، Jest’s built-in matchers.
  • Test Runner (اجراکننده تست): برنامه‌ای که تست‌ها را کشف و اجرا می‌کند و نتایج را گزارش می‌دهد. مثال‌ها: Jest (خودش یک رانر دارد)، Mocha.
  • Test Doubles (اشیاء تست): اشیائی که برای جایگزینی وابستگی‌های واقعی در محیط تست استفاده می‌شوند. اینها شامل موارد زیر هستند:
    • Mocks (ماک‌ها): اشیائی که رفتار آن‌ها از قبل برنامه‌ریزی شده است و می‌توانند بررسی کنند که آیا متد خاصی با آرگومان‌های خاصی فراخوانی شده است یا خیر.
    • Stubs (استاب‌ها): اشیائی که حداقل پیاده‌سازی را برای جایگزینی یک وابستگی ارائه می‌دهند و معمولاً نتایج از پیش تعریف شده‌ای را برمی‌گردانند.
    • Spies (جاسوس‌ها): توابعی که می‌توانند فراخوانی‌ها، آرگومان‌ها و مقادیر بازگشتی توابع واقعی را رصد کنند بدون اینکه رفتار اصلی آن‌ها را تغییر دهند.

TDD در مقابل BDD: رویکردهای توسعه

  • TDD (Test-Driven Development – توسعه تست‌محور):
    • چرخه: قرمز (نوشتن تست شکست‌خورده) -> سبز (نوشتن حداقل کد برای پاس شدن تست) -> بازآرایی (بهبود کد بدون شکستن تست).
    • تمرکز: بر طراحی کد از طریق نوشتن تست‌ها قبل از کد اصلی.
    • نتیجه: کد تمیزتر، ماژولارتر و با پوشش تست بالا.
  • BDD (Behavior-Driven Development – توسعه رفتارمحور):
    • گسترش TDD: بر توصیف رفتار سیستم از دیدگاه کاربر یا ذینفع تمرکز دارد.
    • زبان: از زبان طبیعی برای توصیف سناریوهای تست استفاده می‌کند (Given-When-Then).
    • هدف: بهبود ارتباط بین توسعه‌دهندگان، QA و ذینفعان کسب‌وکار. ابزارهایی مانند Cucumber یا Gherkin در اینجا کاربرد دارند، هرچند فریمورک‌های تست جاوا اسکریپت نیز از این ساختار پیروی می‌کنند.

غوص عمیق در Jest: قدرت، سادگی و اکوسیستم یکپارچه

Jest یک فریمورک تست جاوا اسکریپت است که توسط فیس‌بوک توسعه یافته و به دلیل سهولت استفاده، سرعت بالا و اکوسیستم یکپارچه‌اش محبوبیت زیادی پیدا کرده است. برخلاف Mocha که نیازمند ترکیب با کتابخانه‌های ادعا و شبیه‌سازی خارجی است، Jest “همه چیز در یک جعبه” را ارائه می‌دهد: یک اجراکننده تست، یک کتابخانه ادعا و قابلیت‌های شبیه‌سازی قدرتمند، همه به صورت داخلی تعبیه شده‌اند.

چرا Jest؟ مزایا و ویژگی‌های کلیدی

  • شروع سریع و آسان: با نصب تنها یک پکیج، آماده شروع تست‌نویسی هستید.
  • قابلیت‌های Mocking داخلی: Jest ابزارهای شبیه‌سازی بسیار قدرتمندی را ارائه می‌دهد که نیاز به کتابخانه‌های جداگانه مانند Sinon.js را از بین می‌برد.
  • تست موازی (Parallel Testing): Jest تست‌ها را به صورت موازی اجرا می‌کند که به طور قابل توجهی زمان اجرای تست‌ها را کاهش می‌دهد.
  • تست اسنپ‌شات (Snapshot Testing): یک ویژگی منحصر به فرد برای تست کامپوننت‌های UI یا ساختارهای داده پیچیده.
  • پوشش کد داخلی: Jest دارای قابلیت تولید گزارش پوشش کد (code coverage) داخلی است.
  • Watch Mode: به طور خودکار تست‌ها را در صورت تغییر فایل‌ها اجرا می‌کند.
  • پشتیبانی عالی از تایپ‌اسکریپت: Jest به خوبی با تایپ‌اسکریپت کار می‌کند.
  • فیلترینگ تست‌های تغییر یافته (Only changed files): Jest می‌تواند تنها تست‌های مربوط به فایل‌هایی که تغییر کرده‌اند را اجرا کند، که در پروژه‌های بزرگ بسیار مفید است.

راه‌اندازی و اولین تست با Jest

برای شروع، Jest را به پروژه خود اضافه کنید:

npm install --save-dev jest

در package.json خود، اسکریپت تست را اضافه کنید:

{
  "scripts": {
    "test": "jest"
  }
}

فرض کنید یک تابع ساده برای جمع دو عدد داریم: math.js

// math.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;

و فایل تست آن: math.test.js

// math.test.js
const sum = require('./math');

describe('sum function', () => {
  test('adds 1 + 2 to equal 3', () => {
    expect(sum(1, 2)).toBe(3);
  });

  test('adds 0 + 0 to equal 0', () => {
    expect(sum(0, 0)).toBe(0);
  });

  test('adds negative numbers correctly', () => {
    expect(sum(-1, -5)).toBe(-6);
  });
});

برای اجرای تست‌ها، در ترمینال خود npm test را تایپ کنید.

Jest Matchers: ادعاهای قدرتمند

Jest یک مجموعه غنی از “matchers” (ادعاها) را با expect() ارائه می‌دهد:

  • .toBe(value): برای برابری دقیق مقادیر اولیه (primitives) یا بررسی ارجاع شیء.
  • .toEqual(value): برای مقایسه عمیق شیء یا آرایه‌ها.
  • .not.toBe(value): نفی یک ادعا.
  • .toBeTruthy() / .toBeFalsy(): برای بررسی مقادیر بولی.
  • .toBeNull() / .toBeUndefined() / .toBeDefined(): برای بررسی null/undefined.
  • .toContain(item): برای بررسی وجود یک آیتم در آرایه.
  • .toThrow(error): برای تست کردن توابعی که خطا پرتاب می‌کنند.
  • .toHaveBeenCalled() / .toHaveBeenCalledWith(): برای توابع شبیه‌سازی شده (mocks/spies).

تست ناهمگام (Asynchronous Testing) در Jest

جاوا اسکریپت به شدت به کد ناهمگام (asynchronous) متکی است. Jest ابزارهای مختلفی برای تست این نوع کد ارائه می‌دهد:

استفاده از done() (Callback):

test('the data is peanut butter', done => {
  function callback(data) {
    try {
      expect(data).toBe('peanut butter');
      done(); // Call done() when the async operation finishes
    } catch (error) {
      done(error); // Pass error to done() if assertion fails
    }
  }
  // Simulate an async operation
  setTimeout(() => callback('peanut butter'), 100);
});

استفاده از Promises:

test('the data is peanut butter (promise)', () => {
  return new Promise(resolve => {
    setTimeout(() => {
      expect('peanut butter').toBe('peanut butter');
      resolve();
    }, 100);
  });
});

// Or using .resolves / .rejects matchers
test('resolves to peanut butter', () => {
  return expect(Promise.resolve('peanut butter')).resolves.toBe('peanut butter');
});

test('rejects with error', () => {
  return expect(Promise.reject(new Error('fail'))).rejects.toThrow('fail');
});

استفاده از async/await:

test('the data is peanut butter (async/await)', async () => {
  async function fetchData() {
    return new Promise(resolve => setTimeout(() => resolve('peanut butter'), 100));
  }
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

test('async function throws error', async () => {
  async function fetchDataWithError() {
    return new Promise((_, reject) => setTimeout(() => reject(new Error('network error')), 100));
  }
  await expect(fetchDataWithError()).rejects.toThrow('network error');
});

Mocking در Jest: کنترل وابستگی‌ها

Mocking یکی از قوی‌ترین ویژگی‌های Jest است که به شما امکان می‌دهد وابستگی‌های خارجی را ایزوله و کنترل کنید. این کار برای تست واحدهای کد که به APIها، پایگاه‌های داده، یا ماژول‌های پیچیده دیگر متکی هستند، ضروری است.

jest.fn(): ساخت توابع Mock

test('mock function called', () => {
  const mockCallback = jest.fn(x => 42 + x);
  mockCallback(0);
  mockCallback(1);

  expect(mockCallback.mock.calls.length).toBe(2);
  expect(mockCallback.mock.calls[0][0]).toBe(0);
  expect(mockCallback.mock.calls[1][0]).toBe(1);
  expect(mockCallback.mock.results[0].value).toBe(42);
});

jest.mock(): شبیه‌سازی ماژول‌ها

این روش برای شبیه‌سازی کل ماژول‌ها استفاده می‌شود. فرض کنید یک ماژول api.js دارید:

// api.js
const axios = require('axios');

async function fetchData(id) {
  const response = await axios.get(`https://api.example.com/data/${id}`);
  return response.data;
}
module.exports = { fetchData };

و می‌خواهید axios را شبیه‌سازی کنید:

// api.test.js
const { fetchData } = require('./api');
const axios = require('axios'); // Jest automatically hoists mock for imported modules

// Mock the entire axios module
jest.mock('axios');

describe('fetchData', () => {
  test('should fetch data successfully', async () => {
    // Configure the mock implementation for axios.get
    axios.get.mockResolvedValueOnce({ data: { id: 1, name: 'Test Data' } });

    const data = await fetchData(1);
    expect(data).toEqual({ id: 1, name: 'Test Data' });
    expect(axios.get).toHaveBeenCalledWith('https://api.example.com/data/1');
  });

  test('should handle fetch error', async () => {
    axios.get.mockRejectedValueOnce(new Error('Network Error'));

    await expect(fetchData(2)).rejects.toThrow('Network Error');
  });
});

jest.spyOn(): جاسوسی روی متدهای موجود

این متد به شما امکان می‌دهد روی یک متد از یک شیء موجود “جاسوسی” کنید بدون اینکه پیاده‌سازی اصلی آن را تغییر دهید. می‌توانید بررسی کنید که آیا متد فراخوانی شده است یا چه آرگومان‌هایی دریافت کرده است، و حتی پیاده‌سازی آن را به طور موقت تغییر دهید.

class UserService {
  getUser(id) {
    // Imagine this makes a network request
    return { id: id, name: `User ${id}` };
  }
}

describe('UserService', () => {
  let userService;
  beforeEach(() => {
    userService = new UserService();
  });

  test('getUser should return correct user', () => {
    // Spy on the getUser method
    const getUserSpy = jest.spyOn(userService, 'getUser');

    const user = userService.getUser(1);
    expect(user).toEqual({ id: 1, name: 'User 1' });
    expect(getUserSpy).toHaveBeenCalledTimes(1);
    expect(getUserSpy).toHaveBeenCalledWith(1);

    // Restore the original implementation after test (important!)
    getUserSpy.mockRestore();
  });

  test('getUser can be mocked temporarily', () => {
    // Mock the implementation for this specific test
    jest.spyOn(userService, 'getUser').mockReturnValueOnce({ id: 99, name: 'Mocked User' });

    const user = userService.getUser(1);
    expect(user).toEqual({ id: 99, name: 'Mocked User' });
    expect(userService.getUser).toHaveBeenCalledWith(1);
  });
});

تست اسنپ‌شات (Snapshot Testing)

تست اسنپ‌شات یک ابزار قدرتمند برای اطمینان از عدم تغییرات ناخواسته در خروجی‌های بزرگ و پیچیده مانند کامپوننت‌های React یا ساختارهای داده JSON است. Jest یک “اسنپ‌شات” از خروجی کامپوننت یا داده ایجاد می‌کند و آن را در یک فایل ذخیره می‌کند. در اجرای‌های بعدی، خروجی جدید با اسنپ‌شات ذخیره‌شده مقایسه می‌شود. اگر تطابق نداشته باشند، تست شکست می‌خورد.

// Assuming you have a React component and @testing-library/react
import React from 'react';
import { render } from '@testing-library/react';
import MyComponent from './MyComponent'; // A simple React component

test('MyComponent renders correctly', () => {
  const { asFragment } = render();
  expect(asFragment()).toMatchSnapshot();
});

هنگام اجرای اولین بار، Jest یک فایل .snap در کنار فایل تست شما ایجاد می‌کند. اگر در آینده خروجی کامپوننت به عمد تغییر کند، باید با jest -u اسنپ‌شات را به‌روزرسانی کنید.

پیکربندی Jest: jest.config.js

برای پروژه‌های پیچیده‌تر، می‌توانید یک فایل jest.config.js برای تنظیمات Jest ایجاد کنید. برخی از تنظیمات رایج شامل:

  • collectCoverage: فعال کردن جمع‌آوری پوشش کد.
  • testEnvironment: محیط تست (مثلاً jsdom برای مرورگر، node برای Node.js).
  • moduleNameMapper: برای مدیریت aliasهای مسیر.
  • setupFilesAfterEnv: فایل‌هایی که قبل از هر فایل تست اجرا می‌شوند (مثلاً برای تنظیمات جهانی یا extends matchers).
  • transform: برای تبدیل کد (مثلاً Babel یا TypeScript).
// jest.config.js
module.exports = {
  testEnvironment: 'jsdom', // or 'node'
  setupFilesAfterEnv: ['/jest.setup.js'],
  moduleNameMapper: {
    '^@/(.*)$': '/src/$1',
  },
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coveragePathIgnorePatterns: [
    '/node_modules/',
    '/dist/',
  ],
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
  },
};

غوص عمیق در Mocha و Chai: انعطاف‌پذیری و قدرت ترکیبی

Mocha یک فریمورک تست جاوا اسکریپت است که بر انعطاف‌پذیری و ماژولار بودن تأکید دارد. برخلاف Jest، Mocha یک راهکار جامع “همه در یک” نیست؛ بلکه یک “رانر تست” است که به شما اجازه می‌دهد کتابخانه‌های ادعا، شبیه‌سازی و سایر ابزارها را به انتخاب خود با آن ترکیب کنید. این انعطاف‌پذیری، Mocha را به انتخابی عالی برای پروژه‌هایی تبدیل می‌کند که نیاز به کنترل دقیق بر روی هر جزء از پشته تست خود دارند.

چرا Mocha؟ مزایا و ویژگی‌های کلیدی

  • انعطاف‌پذیری بالا: می‌توانید کتابخانه‌های ادعا (مانند Chai)، شبیه‌سازی (مانند Sinon.js)، و حتی رانرهای سرصفحه (headless browsers) را به دلخواه خود انتخاب کنید.
  • پشتیبانی از انواع سبک‌های ادعا: Chai که معمولاً با Mocha استفاده می‌شود، از سبک‌های BDD (expect و should) و TDD (assert) پشتیبانی می‌کند.
  • سیستم هوک‌های قدرتمند: before, after, beforeEach, afterEach به شما امکان می‌دهند محیط تست را به دقت آماده و پاک‌سازی کنید.
  • گزارش‌دهی انعطاف‌پذیر: Mocha از انواع مختلف گزارش‌دهنده‌ها پشتیبانی می‌کند.
  • قدمت و جامعه: به عنوان یک فریمورک قدیمی‌تر، Mocha یک جامعه بزرگ و مستندات گسترده دارد.

راه‌اندازی Mocha با Chai و Sinon.js

برای شروع، Mocha، Chai و Sinon.js را نصب کنید:

npm install --save-dev mocha chai sinon

در package.json خود، اسکریپت تست را اضافه کنید:

{
  "scripts": {
    "test": "mocha --require @babel/register --recursive \"test/**/*.js\""
    // --recursive: looks for test files in subdirectories
    // --require @babel/register: for ES Modules or advanced JS features
  }
}

برای استفاده از Chai، معمولاً آن را در هر فایل تست یا یک فایل تنظیمات جهانی ایمپورت می‌کنید:

// test/math.test.js
const assert = require('chai').assert; // TDD style
const expect = require('chai').expect; // BDD style

const sum = require('../src/math'); // Assuming math.js is in src/

describe('sum function', () => {
  it('should return the correct sum using assert', () => {
    assert.equal(sum(2, 3), 5, '2 + 3 should be 5');
  });

  it('should return the correct sum using expect', () => {
    expect(sum(2, 3)).to.equal(5);
    expect(sum(-1, 1)).to.equal(0);
  });
});

برای اجرای تست‌ها، npm test را تایپ کنید.

Assertion Styles در Chai

Chai سه سبک اصلی برای نوشتن ادعاها ارائه می‌دهد:

  • Assert Style (TDD): سنتی‌تر، شبیه به Node.js built-in assert.
  • const assert = require('chai').assert;
    assert.equal(foo, 'bar');
    assert.lengthOf(baz, 3, 'baz has a length of 3');
  • Expect Style (BDD): زنجیره‌پذیر و خوانا، شبیه به RSpec یا Jasmine.
  • const expect = require('chai').expect;
    expect(foo).to.be.a('string');
    expect(foo).to.equal('bar');
    expect(baz).to.have.lengthOf(3);
  • Should Style (BDD): افزودن ویژگی به پروتوتایپ Object، که کد را کمی خواناتر می‌کند اما می‌تواند مشکل‌ساز باشد.
  • const should = require('chai').should(); // execute as a function to enable
    foo.should.be.a('string');
    foo.should.equal('bar');
    baz.should.have.lengthOf(3);

معمولاً سبک expect به دلیل خوانایی و عدم تغییر پروتوتایپ جهانی، محبوب‌ترین است.

تست ناهمگام (Asynchronous Testing) در Mocha

Mocha نیز مانند Jest، چندین راه برای تست کد ناهمگام ارائه می‌دهد:

استفاده از done() (Callback):

it('should complete an async operation', (done) => {
  setTimeout(() => {
    expect(true).to.be.true;
    done(); // Call done() when the async operation finishes
  }, 100);
});

it('should handle async errors', (done) => {
  setTimeout(() => {
    try {
      expect(false).to.be.true; // This will fail
      done();
    } catch (e) {
      done(e); // Pass error to done()
    }
  }, 100);
});

استفاده از Promises:

it('should resolve a promise', () => {
  return new Promise(resolve => {
    setTimeout(() => {
      expect('data').to.equal('data');
      resolve();
    }, 100);
  });
});

it('should reject a promise', () => {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error('something bad happened'));
    }, 100);
  }).catch(err => {
    expect(err).to.be.an('error');
    expect(err.message).to.equal('something bad happened');
  });
});

استفاده از async/await:

it('should work with async/await', async () => {
  async function fetchData() {
    return new Promise(resolve => setTimeout(() => resolve('async data'), 100));
  }
  const data = await fetchData();
  expect(data).to.equal('async data');
});

it('should handle async/await errors', async () => {
  async function fetchDataWithError() {
    return new Promise((_, reject) => setTimeout(() => reject(new Error('async error')), 100));
  }
  let error;
  try {
    await fetchDataWithError();
  } catch (err) {
    error = err;
  }
  expect(error).to.be.an('error');
  expect(error.message).to.equal('async error');
});

Mocking و Stubbing با Sinon.js (در ترکیب با Mocha)

در حالی که Jest قابلیت‌های شبیه‌سازی داخلی دارد، Mocha اغلب با Sinon.js برای شبیه‌سازی، جاسوسی و استابینگ (stubbing) استفاده می‌شود. Sinon یک کتابخانه قدرتمند است که ابزارهای جداگانه برای Sypies، Stubs و Mocks فراهم می‌کند.

Spying با Sinon

Spying به شما امکان می‌دهد روی توابع موجود جاسوسی کنید، بدون اینکه رفتار اصلی آن‌ها را تغییر دهید. این برای بررسی اینکه آیا یک تابع فراخوانی شده است، چند بار فراخوانی شده است و با چه آرگومان‌هایی، مفید است.

const sinon = require('sinon');
const expect = require('chai').expect;

class MyLogger {
  log(message) {
    console.log(message);
  }
}

describe('MyLogger', () => {
  let logger;
  beforeEach(() => {
    logger = new MyLogger();
  });

  it('should call log method', () => {
    const logSpy = sinon.spy(logger, 'log'); // Spy on the log method

    logger.log('Hello');
    expect(logSpy.calledOnce).to.be.true;
    expect(logSpy.calledWith('Hello')).to.be.true;

    logSpy.restore(); // Restore the original method
  });
});

Stubbing با Sinon

Stubbing به شما امکان می‌دهد یک تابع یا متد را با یک پیاده‌سازی جایگزین و کنترل‌شده جایگزین کنید. این برای ایزوله کردن کد تحت تست از وابستگی‌های خارجی (مانند فراخوانی‌های شبکه یا دسترسی به فایل سیستم) ایده‌آل است.

const sinon = require('sinon');
const expect = require('chai').expect;

class DataService {
  fetchUser(id) {
    // Imagine this makes a real API call
    return { id, name: `User ${id}` };
  }
}

describe('DataService', () => {
  let dataService;
  beforeEach(() => {
    dataService = new DataService();
  });

  it('fetchUser should return mocked data', () => {
    // Stub the fetchUser method to return predefined data
    const fetchUserStub = sinon.stub(dataService, 'fetchUser');
    fetchUserStub.returns({ id: 99, name: 'Mocked User' });

    const user = dataService.fetchUser(1);
    expect(user).to.deep.equal({ id: 99, name: 'Mocked User' });
    expect(fetchUserStub.calledOnceWith(1)).to.be.true;

    fetchUserStub.restore(); // Restore the original method
  });

  it('fetchUser can return promises', async () => {
    const fetchUserStub = sinon.stub(dataService, 'fetchUser');
    fetchUserStub.resolves({ id: 100, name: 'Async Mocked User' });

    const user = await dataService.fetchUser(2);
    expect(user).to.deep.equal({ id: 100, name: 'Async Mocked User' });
    fetchUserStub.restore();
  });
});

Mocking با Sinon (برای تأیید رفتار)

Mocks در Sinon اشیاء کاملاً شبیه‌سازی شده‌ای هستند که انتظارات خاصی از نحوه فراخوانی متدها را از قبل تعریف می‌کنند. اگر آن انتظارات برآورده نشوند، تست شکست می‌خورد.

const sinon = require('sinon');
const expect = require('chai').expect;

class NotificationService {
  sendEmail(user, message) {
    // imagine sending a real email
    return true;
  }
}

describe('NotificationService', () => {
  it('sendEmail should be called with correct arguments', () => {
    const notificationService = new NotificationService();
    // Create a mock for the entire notificationService object
    const mock = sinon.mock(notificationService);

    // Expect sendEmail to be called once with specific arguments
    mock.expects('sendEmail').once().withArgs('test@example.com', 'Hello');

    notificationService.sendEmail('test@example.com', 'Hello');

    mock.verify(); // Verify that all expectations were met
    mock.restore(); // Restore original object
  });
});

Hookها در Mocha: مدیریت محیط تست

Mocha مجموعه‌ای از Hookها را برای تنظیم و پاک‌سازی محیط تست در سطوح مختلف (describe block یا در سطح فایل) فراهم می‌کند:

  • before(): قبل از اجرای اولین تست در یک بلوک describe.
  • after(): بعد از اجرای آخرین تست در یک بلوک describe.
  • beforeEach(): قبل از اجرای هر تست (it یا test) در یک بلوک describe.
  • afterEach(): بعد از اجرای هر تست در یک بلوک describe.
describe('User API', () => {
  let dbConnection;
  let server;

  before(() => {
    // Runs once before all tests in this describe block
    console.log('Connecting to database...');
    dbConnection = 'connected'; // Simulate connection
    server = 'started'; // Simulate server start
  });

  after(() => {
    // Runs once after all tests in this describe block
    console.log('Closing database connection and stopping server...');
    dbConnection = null;
    server = null;
  });

  beforeEach(() => {
    // Runs before each 'it' test
    console.log('Resetting user data for new test...');
    // Clear or reset test data
  });

  afterEach(() => {
    // Runs after each 'it' test
    console.log('Cleaning up after test...');
    // Clean up temporary files or mocks
  });

  it('should fetch all users', () => {
    expect(dbConnection).to.equal('connected');
    expect(server).to.equal('started');
    console.log('  Test: Fetch all users');
    // Actual test logic
  });

  it('should create a new user', () => {
    expect(dbConnection).to.equal('connected');
    expect(server).to.equal('started');
    console.log('  Test: Create a new user');
    // Actual test logic
  });
});

مفاهیم پیشرفته تست‌نویسی و بهترین شیوه‌ها

فراتر از یادگیری نحو ابزارها، درک مفاهیم پیشرفته و پیروی از بهترین شیوه‌ها برای نوشتن تست‌های مؤثر، قابل نگهداری و مفید ضروری است. تست‌نویسی یک مهارت است که با تجربه و تمرین بهبود می‌یابد.

ساختاردهی تست‌ها

سازماندهی مناسب فایل‌های تست برای قابلیت نگهداری و مقیاس‌پذیری بسیار مهم است:

  • تست‌ها در کنار کد: یک رویکرد محبوب قرار دادن فایل‌های تست (.test.js یا .spec.js) در کنار فایل‌های منبع مربوطه است. این باعث می‌شود که پیدا کردن تست‌های یک ماژول خاص آسان‌تر باشد.
  • پوشه __tests__: برخی توسعه‌دهندگان ترجیح می‌دهند یک پوشه __tests__ در کنار هر ماژول یا کامپوننت ایجاد کنند و تمام تست‌های مربوط به آن ماژول را در آنجا قرار دهند.
  • پوشه test/ یا spec/ در ریشه: این رویکرد قدیمی‌تر است که تمام تست‌ها را در یک پوشه مرکزی در ریشه پروژه قرار می‌دهد. برای پروژه‌های کوچک‌تر یا ساده‌تر می‌تواند کارساز باشد.
  • بلوک‌های describe و it/test: از این ساختارها برای گروه‌بندی منطقی تست‌ها استفاده کنید. یک describe می‌تواند یک ماژول، یک کلاس یا یک ویژگی را توصیف کند، و it یا test باید یک رفتار خاص را توصیف کند.
// Bad:
test('user test 1', () => {});
test('user test 2', () => {});

// Good:
describe('User Model', () => {
  it('should create a new user with valid data', () => { /* ... */ });
  it('should throw an error if email is invalid', () => { /* ... */ });
  describe('User Authentication', () => {
    it('should authenticate user with correct credentials', () => { /* ... */ });
    it('should reject user with incorrect password', () => { /* ... */ });
  });
});

پوشش کد (Code Coverage)

پوشش کد معیاری است که نشان می‌دهد چه مقدار از کد شما توسط تست‌ها پوشش داده شده است. این شامل پوشش خط (line coverage)، پوشش تابع (function coverage)، پوشش شاخه (branch coverage) و پوشش دستور (statement coverage) می‌شود.

  • ابزارها: Jest دارای پوشش کد داخلی است. برای Mocha، می‌توانید از ابزارهایی مانند Istanbul/nyc استفاده کنید.
  • هدف: پوشش ۱۰۰٪ همیشه به معنای کد بدون باگ نیست، اما پوشش کم نشان‌دهنده ریسک بالاست. هدف شما باید رسیدن به یک پوشش معقول (مثلاً ۸۰-۹۰٪) در منطق کسب‌وکار اصلی باشد.
  • هشدار: به گزارش‌های پوشش کد به عنوان تنها معیار کیفیت اعتماد نکنید. تست‌های ضعیف با پوشش بالا همچنان می‌توانند منجر به باگ شوند.

تست در CI/CD

ادغام تست‌ها در خط لوله CI/CD (ادغام پیوسته/استقرار پیوسته) حیاتی است. هر Pull Request یا Commit باید منجر به اجرای خودکار تست‌ها شود. این تضمین می‌کند که تغییرات جدید کد، هیچ رگرسیونی ایجاد نمی‌کنند و کد همیشه در وضعیت قابل استقرار قرار دارد.

  • سرعت: تست‌های واحد و یکپارچه‌سازی باید به سرعت اجرا شوند تا بازخورد سریع ارائه دهند. تست‌های E2E ممکن است زمان‌بر باشند و در فازهای بعدی CI اجرا شوند.
  • محیط ایزوله: مطمئن شوید که تست‌ها در یک محیط ایزوله و مستقل از سایر فرایندها اجرا می‌شوند تا از تداخل جلوگیری شود.

مدیریت وابستگی‌های خارجی (External Dependencies)

تست کدی که به سرویس‌های خارجی (APIها، پایگاه داده‌ها، سیستم فایل) وابسته است، یک چالش است. بهترین روش‌ها شامل موارد زیر است:

  • Mocking/Stubbing: برای تست‌های واحد و بسیاری از تست‌های یکپارچه‌سازی، بهتر است وابستگی‌های خارجی را شبیه‌سازی کنید تا از سرعت، قابلیت اطمینان و ایزوله بودن تست‌ها اطمینان حاصل شود.
  • In-memory Databases: برای تست‌های یکپارچه‌سازی با پایگاه داده، استفاده از پایگاه داده‌های در حافظه (مانند SQLite در Node.js) یا نسخه‌های سبک از پایگاه داده واقعی (مثلاً Dockerized PostgreSQL) می‌تواند مفید باشد.
  • Test Doubles for UI Interaction: در تست‌های UI، استفاده از ابزارهایی مانند @testing-library (با Jest) یا Enzyme (برای React) که تعاملات کاربر را شبیه‌سازی می‌کنند، بهتر از شبیه‌سازی کامپوننت‌ها به صورت دستی است.

نوشتن تست‌های مؤثر: اصول F.I.R.S.T

این اصول توسط Robert C. Martin (Uncle Bob) برای تست‌های خوب پیشنهاد شده‌اند:

  • F (Fast): تست‌ها باید سریع اجرا شوند. تست‌های کند، توسعه‌دهندگان را از اجرای مکرر آن‌ها باز می‌دارند.
  • I (Independent/Isolated): هر تست باید مستقل از تست‌های دیگر باشد. ترتیب اجرای تست‌ها نباید بر نتیجه تأثیر بگذارد.
  • R (Repeatable): تست‌ها باید در هر محیطی (لوکال، CI، پروداکشن) به صورت ثابت و قابل تکرار عمل کنند.
  • S (Self-validating): تست‌ها باید خروجی بولی (Pass/Fail) داشته باشند. نباید نیازی به بررسی دستی گزارش‌ها باشد.
  • T (Timely): تست‌ها باید به موقع نوشته شوند. ترجیحاً قبل از کد اصلی (TDD) یا همزمان با آن.

بازآرایی تست‌ها

تست‌ها نیز مانند کدهای تولیدی، باید قابل بازآرایی باشند. کد تکراری (boilerplate) در تست‌ها را شناسایی و با استفاده از هوک‌ها (beforeEach, afterEach) یا توابع کمکی (helper functions) کاهش دهید. مطمئن شوید که تست‌ها واضح و خوانا هستند.

Jest در مقابل Mocha: یک تحلیل مقایسه‌ای

انتخاب بین Jest و Mocha اغلب به نیازهای خاص پروژه، ترجیحات تیم و اکوسیستم موجود بستگی دارد. هر دو ابزارهای قدرتمندی هستند، اما رویکردهای متفاوتی دارند.

جدول مقایسه Jest و Mocha

ویژگی Jest Mocha (با Chai و Sinon)
نوع فریمورک تست “همه در یک” (Test Runner, Assertion, Mocking) Test Runner (نیاز به کتابخانه‌های جداگانه برای Assertion/Mocking)
سهولت راه‌اندازی بسیار آسان، پکیج واحد نیاز به نصب چندین پکیج (Mocha, Chai, Sinon) و پیکربندی
Assertion Library داخلی (با expect و matchers) خارجی (معمولاً Chai با سبک‌های expect, should, assert)
قابلیت‌های Mocking/Stubbing/Spying داخلی و بسیار قدرتمند (jest.fn, jest.mock, jest.spyOn) خارجی (معمولاً Sinon.js)
تست موازی بصورت داخلی پشتیبانی می‌شود، سریع‌تر در پروژه‌های بزرگ پشتیبانی نمی‌شود (مگر با ابزارهای خارجی)
تست اسنپ‌شات داخلی غیرموجود، نیاز به راهکارهای سفارشی
پوشش کد داخلی (از Istanbul استفاده می‌کند) نیاز به ابزار خارجی (معمولاً nyc/Istanbul)
محیط تست (Test Environment) JS DOM (پیش‌فرض برای وب) / Node Node (پیش‌فرض) / نیاز به JSDOM یا Browser برای محیط‌های مرورگر
پیکربندی فایل jest.config.js، جامع و یکپارچه خط فرمان، فایل .mocharc.js، و تنظیمات جداگانه برای Chai/Sinon
یادگیری و منحنی یادگیری آسان برای شروع، ویژگی‌های پیشرفته نیازمند یادگیری هستند. کمی پیچیده‌تر برای راه‌اندازی اولیه، اما هر جزء به صورت جداگانه ساده است.
جامعه و اکوسیستم بسیار فعال، به خصوص در اکوسیستم React و Vue فعال و تثبیت شده، محبوب در Node.js و پروژه‌های با نیاز به انعطاف بالا

چه زمانی Jest را انتخاب کنیم؟

  • پروژه‌های React/Vue: Jest به طور گسترده در اکوسیستم React و Vue استفاده می‌شود و با @testing-library/react یا vue-test-utils به خوبی کار می‌کند.
  • سرعت و سهولت راه‌اندازی: اگر به دنبال یک راه حل سریع، آسان و با کمترین پیکربندی اولیه هستید.
  • نیاز به تست اسنپ‌شات: اگر کامپوننت‌های UI زیادی دارید یا نیاز به اطمینان از عدم تغییرات ناخواسته در ساختارهای داده پیچیده دارید.
  • تیم‌های کوچک تا متوسط: برای تیم‌هایی که می‌خواهند با یک ابزار جامع کار کنند و نگران انتخاب و پیکربندی ابزارهای جداگانه نباشند.
  • پروژه‌های Node.js: Jest به همان اندازه که در فرانت‌اند قدرتمند است، در بک‌اند Node.js نیز عملکرد عالی دارد، به خصوص با قابلیت‌های Mocking داخلی‌اش.

چه زمانی Mocha را انتخاب کنیم؟

  • نیاز به انعطاف‌پذیری بالا: اگر می‌خواهید کنترل کاملی بر روی هر بخش از پشته تست خود داشته باشید و ابزارهای مورد نظر خود را ترکیب کنید.
  • پروژه‌های Node.js با نیازهای خاص: برای سرویس‌های بک‌اند که ممکن است نیاز به تعامل با سیستم فایل، پایگاه داده یا سیستم‌های خارجی به روش‌های بسیار خاص داشته باشند و ابزارهایی مانند Sinon.js برای شبیه‌سازی دقیق‌تر مناسب‌تر باشند.
  • مهاجرت از فریمورک‌های قدیمی: اگر در حال مهاجرت از یک فریمورک تست قدیمی‌تر هستید، معماری ماژولار Mocha ممکن است سازگاری بهتری داشته باشد.
  • محیط‌های مرورگر (بدون فریمورک‌های UI): اگر تست‌های مرورگر زیادی دارید که بر DOM خام یا ابزارهای تست UI خاصی غیر از React/Vue متکی هستند، Mocha انعطاف‌پذیری لازم را برای ادغام فراهم می‌کند.
  • پروژه‌هایی که از قبل با Chai/Sinon کار می‌کنند: اگر تیم شما از قبل با این ابزارها آشنایی دارد، ادامه کار با Mocha طبیعی است.

در نهایت، انتخاب بین Jest و Mocha یک تصمیم استراتژیک است. Jest با یکپارچگی و سهولت استفاده، یک انتخاب عالی برای اکثر پروژه‌ها، به ویژه در اکوسیستم فرانت‌اند، است. Mocha با ماژولار بودن و انعطاف‌پذیری خود، به توسعه‌دهندگان باتجربه اجازه می‌دهد تا پشته تست خود را دقیقاً مطابق با نیازهایشان سفارشی کنند.

نتیجه‌گیری: قدرت تست‌نویسی در دستان شما

تست‌نویسی در جاوا اسکریپت دیگر یک “گزینه” یا یک “کار اضافی” نیست؛ بلکه یک ضرورت استراتژیک برای توسعه نرم‌افزار مدرن و پایدار محسوب می‌شود. در این راهنمای جامع، ما به عمق مفاهیم تست‌نویسی، از چرایی و چگونگی آن گرفته تا کاربرد عملی دو فریمورک قدرتمند Jest و Mocha، پرداختیم. شما با مبانی هرم تست، اهمیت ادعاها، مفهوم شبیه‌سازی و جاسوسی، و همچنین تفاوت‌های ظریف در تست‌نویسی ناهمگام آشنا شدید.

یاد گرفتید که Jest چگونه با یکپارچگی و سادگی راه‌اندازی خود، به خصوص در اکوسیستم‌های فرانت‌اند مانند React، درخششی خاص دارد. قابلیت‌هایی مانند تست اسنپ‌شات و ابزارهای Mocking داخلی آن، فرآیند تست را بسیار کارآمدتر می‌کنند. از سوی دیگر، Mocha را به عنوان یک رانر تست انعطاف‌پذیر شناختید که به شما امکان می‌دهد کتابخانه‌های ادعا (مانند Chai) و شبیه‌سازی (مانند Sinon.js) را مطابق با نیازهای پروژه خود انتخاب و ترکیب کنید، و کنترل کاملی بر روی پشته تست خود داشته باشید.

همچنین، بهترین شیوه‌هایی مانند سازماندهی تست‌ها، اهمیت پوشش کد، ادغام تست در فرآیندهای CI/CD، مدیریت وابستگی‌های خارجی و اصول F.I.R.S.T را برای نوشتن تست‌های مؤثر، قابل نگهداری و قابل اعتماد بررسی کردیم. به یاد داشته باشید که تست‌نویسی تنها در مورد یافتن باگ‌ها نیست؛ بلکه در مورد اطمینان از کیفیت، بهبود طراحی کد، تسهیل بازآرایی و در نهایت، افزایش سرعت و اعتماد به نفس تیم توسعه است.

انتخاب بین Jest و Mocha به ترجیحات تیمی و ویژگی‌های خاص پروژه بستگی دارد. اما مهم‌تر از انتخاب ابزار، تعهد به فرهنگ تست‌نویسی است. با درک عمیق این ابزارها و مفاهیم، شما اکنون مجهز هستید تا نه تنها کدی بنویسید که کار می‌کند، بلکه کدی که قابل اعتماد، قابل نگهداری و آماده برای چالش‌های آینده توسعه نرم‌افزار است. تست‌نویسی را به بخشی جدایی‌ناپذیر از فرآیند توسعه خود تبدیل کنید و شاهد تفاوت چشمگیر آن در کیفیت و پایداری محصولات خود باشید.

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

"با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان