فارسی

بر الگوهای تست پیشرفته Jest مسلط شوید تا نرم‌افزاری قابل اعتمادتر و با قابلیت نگهداری بالاتر بسازید. تکنیک‌هایی مانند mock، تست snapshot، matchers سفارشی و موارد دیگر را برای تیم‌های توسعه جهانی کاوش کنید.

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

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

چرا باید از تست‌های واحد پایه فراتر برویم؟

تست‌های واحد پایه، کامپوننت‌های مجزا را به صورت ایزوله تأیید می‌کنند. با این حال، برنامه‌های کاربردی در دنیای واقعی سیستم‌های پیچیده‌ای هستند که کامپوننت‌ها با یکدیگر تعامل دارند. الگوهای تست پیشرفته با فراهم کردن امکانات زیر به این پیچیدگی‌ها رسیدگی می‌کنند:

تسلط بر Mocking و Spies

Mocking برای ایزوله کردن واحد تحت تست با جایگزین کردن وابستگی‌های آن با جایگزین‌های کنترل‌شده، بسیار حیاتی است. Jest ابزارهای قدرتمندی برای این کار فراهم می‌کند:

jest.fn(): بنیاد Mockها و Spyها

jest.fn() یک تابع mock ایجاد می‌کند. شما می‌توانید فراخوانی‌ها، آرگومان‌ها و مقادیر بازگشتی آن را ردیابی کنید. این بلوک ساختمانی برای استراتژی‌های پیچیده‌تر mocking است.

مثال: ردیابی فراخوانی توابع

// component.js
export const fetchData = () => {
  // Simulates an API call
  return Promise.resolve({ data: 'some data' });
};

export const processData = async (fetcher) => {
  const result = await fetcher();
  return `Processed: ${result.data}`;
};

// component.test.js
import { processData } from './component';

test('should process data correctly', async () => {
  const mockFetcher = jest.fn().mockResolvedValue({ data: 'mocked data' });
  const result = await processData(mockFetcher);
  expect(result).toBe('Processed: mocked data');
  expect(mockFetcher).toHaveBeenCalledTimes(1);
  expect(mockFetcher).toHaveBeenCalledWith();
});

jest.spyOn(): مشاهده بدون جایگزینی

jest.spyOn() به شما اجازه می‌دهد تا فراخوانی‌های یک متد بر روی یک شیء موجود را مشاهده کنید، بدون اینکه لزوماً پیاده‌سازی آن را جایگزین کنید. در صورت نیاز می‌توانید پیاده‌سازی را نیز mock کنید.

مثال: جاسوسی (Spying) بر روی یک متد ماژول

// logger.js
export const logInfo = (message) => {
  console.log(`INFO: ${message}`);
};

// service.js
import { logInfo } from './logger';

export const performTask = (taskName) => {
  logInfo(`Starting task: ${taskName}`);
  // ... task logic ...
  logInfo(`Task ${taskName} completed.`);
};

// service.test.js
import { performTask } from './service';
import * as logger from './logger';

test('should log task start and completion', () => {
  const logSpy = jest.spyOn(logger, 'logInfo');

  performTask('backup');

  expect(logSpy).toHaveBeenCalledTimes(2);
  expect(logSpy).toHaveBeenCalledWith('Starting task: backup');
  expect(logSpy).toHaveBeenCalledWith('Task backup completed.');

  logSpy.mockRestore(); // بازگرداندن پیاده‌سازی اصلی مهم است
});

شبیه‌سازی (Mocking) ورودی‌های ماژول

قابلیت‌های mocking ماژول در Jest بسیار گسترده است. شما می‌توانید کل ماژول‌ها یا خروجی‌های خاصی را mock کنید.

مثال: Mock کردن یک کلاینت API خارجی

// api.js
import axios from 'axios';

export const getUser = async (userId) => {
  const response = await axios.get(`/api/users/${userId}`);
  return response.data;
};

// user-service.js
import { getUser } from './api';

export const getUserFullName = async (userId) => {
  const user = await getUser(userId);
  return `${user.firstName} ${user.lastName}`;
};

// user-service.test.js
import { getUserFullName } from './user-service';
import * as api from './api';

// کل ماژول api را mock کنید
jest.mock('./api');

test('should get full name using mocked API', async () => {
  // تابع مشخصی از ماژول mock شده را mock کنید
  api.getUser.mockResolvedValue({ id: 1, firstName: 'Ada', lastName: 'Lovelace' });

  const fullName = await getUserFullName(1);

  expect(fullName).toBe('Ada Lovelace');
  expect(api.getUser).toHaveBeenCalledTimes(1);
  expect(api.getUser).toHaveBeenCalledWith(1);
});

Mocking خودکار در مقابل Mocking دستی

Jest به طور خودکار ماژول‌های Node.js را mock می‌کند. برای ماژول‌های ES یا ماژول‌های سفارشی، ممکن است به jest.mock() نیاز داشته باشید. برای کنترل بیشتر، می‌توانید دایرکتوری‌های __mocks__ ایجاد کنید.

پیاده‌سازی‌های Mock

شما می‌توانید پیاده‌سازی‌های سفارشی برای mockهای خود ارائه دهید.

مثال: Mocking با یک پیاده‌سازی سفارشی

// math.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// calculator.js
import { add, subtract } from './math';

export const calculate = (operation, a, b) => {
  if (operation === 'add') {
    return add(a, b);
  } else if (operation === 'subtract') {
    return subtract(a, b);
  }
  return null;
};

// calculator.test.js
import { calculate } from './calculator';
import * as math from './math';

// کل ماژول math را mock کنید
jest.mock('./math');

test('should perform addition using mocked math add', () => {
  // یک پیاده‌سازی mock برای تابع 'add' ارائه دهید
  math.add.mockImplementation((a, b) => a + b + 10); // ۱۰ را به نتیجه اضافه کنید
  math.subtract.mockReturnValue(5); // تابع subtract را نیز mock کنید

  const result = calculate('add', 5, 3);

  expect(math.add).toHaveBeenCalledWith(5, 3);
  expect(result).toBe(18); // 5 + 3 + 10

  const subResult = calculate('subtract', 10, 2);
  expect(math.subtract).toHaveBeenCalledWith(10, 2);
  expect(subResult).toBe(5);
});

تست Snapshot: حفظ رابط کاربری و پیکربندی

تست‌های Snapshot یک ویژگی قدرتمند برای ثبت خروجی کامپوننت‌ها یا پیکربندی‌های شما هستند. آنها به ویژه برای تست UI یا تأیید ساختارهای داده پیچیده مفید هستند.

تست Snapshot چگونه کار می‌کند

اولین باری که یک تست snapshot اجرا می‌شود، Jest یک فایل .snap حاوی یک نمایش سریال‌سازی شده از مقدار تست شده ایجاد می‌کند. در اجراهای بعدی، Jest خروجی فعلی را با snapshot ذخیره شده مقایسه می‌کند. اگر آنها متفاوت باشند، تست ناموفق می‌شود و شما را از تغییرات ناخواسته آگاه می‌کند. این ویژگی برای شناسایی رگرسیون در کامپوننت‌های UI در مناطق یا زبان‌های مختلف بسیار ارزشمند است.

مثال: Snapshot گرفتن از یک کامپوننت React

فرض کنید یک کامپوننت React دارید:

// UserProfile.js
import React from 'react';

const UserProfile = ({ name, email, isActive }) => (
  <div>
    <h2>{name}</h2>
    <p><strong>Email:</strong> {email}</p>
    <p><strong>Status:</strong> {isActive ? 'Active' : 'Inactive'}</p>
  </div>
);

export default UserProfile;

// UserProfile.test.js
import React from 'react';
import renderer from 'react-test-renderer'; // For React component snapshots
import UserProfile from './UserProfile';

test('renders UserProfile correctly', () => {
  const user = {
    name: 'Jane Doe',
    email: 'jane.doe@example.com',
    isActive: true,
  };
  const component = renderer.create(
    <UserProfile {...user} />
  );
  const tree = component.toJSON();
  expect(tree).toMatchSnapshot();
});

test('renders inactive UserProfile correctly', () => {
  const user = {
    name: 'John Smith',
    email: 'john.smith@example.com',
    isActive: false,
  };
  const component = renderer.create(
    <UserProfile {...user} />
  );
  const tree = component.toJSON();
  expect(tree).toMatchSnapshot('inactive user profile'); // اسنپ‌شات نام‌گذاری شده
});

پس از اجرای تست‌ها، Jest یک فایل UserProfile.test.js.snap ایجاد خواهد کرد. هنگامی که کامپوننت را به‌روزرسانی می‌کنید، باید تغییرات را بررسی کرده و احتمالاً با اجرای Jest با پرچم --updateSnapshot یا -u، اسنپ‌شات را به‌روز کنید.

بهترین شیوه‌ها برای تست Snapshot

Matcherهای سفارشی: بهبود خوانایی تست

Matcherهای داخلی Jest گسترده هستند، اما گاهی اوقات شما نیاز به تأیید شرایط خاصی دارید که پوشش داده نشده‌اند. Matcherهای سفارشی به شما امکان می‌دهند منطق تأیید خود را ایجاد کنید و تست‌های خود را خواناتر و گویاتر کنید.

ایجاد Matcherهای سفارشی

شما می‌توانید شیء expect در Jest را با matcherهای خود گسترش دهید.

مثال: بررسی فرمت صحیح ایمیل

در فایل راه‌اندازی Jest خود (مثلاً jest.setup.js که در jest.config.js پیکربندی شده است):

// jest.setup.js

expect.extend({
  toBeValidEmail(received) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const pass = emailRegex.test(received);

    if (pass) {
      return {
        message: () => `expected ${received} not to be a valid email`,
        pass: true,
      };
    } else {
      return {
        message: () => `expected ${received} to be a valid email`,
        pass: false,
      };
    }
  },
});

// In your jest.config.js
// module.exports = { setupFilesAfterEnv: ['/jest.setup.js'] };

در فایل تست شما:

// validation.test.js

test('should validate email formats', () => {
  expect('test@example.com').toBeValidEmail();
  expect('invalid-email').not.toBeValidEmail();
  expect('another.test@sub.domain.co.uk').toBeValidEmail();
});

مزایای Matcherهای سفارشی

تست عملیات ناهمگام (Asynchronous)

جاوا اسکریپت به شدت ناهمگام است. Jest پشتیبانی عالی برای تست promiseها و async/await فراهم می‌کند.

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

این روش مدرن و خواناترین راه برای تست کدهای ناهمگام است.

مثال: تست یک تابع ناهمگام

// dataService.js
export const fetchUserData = async (userId) => {
  // Simulate fetching data after a delay
  await new Promise(resolve => setTimeout(resolve, 50));
  if (userId === 1) {
    return { id: 1, name: 'Alice' };
  } else {
    throw new Error('User not found');
  }
};

// dataService.test.js
import { fetchUserData } from './dataService';

test('fetches user data correctly', async () => {
  const user = await fetchUserData(1);
  expect(user).toEqual({ id: 1, name: 'Alice' });
});

test('throws error for non-existent user', async () => {
  await expect(fetchUserData(2)).rejects.toThrow('User not found');
});

استفاده از .resolves و .rejects

این matcherها تست promiseهای موفق (resolution) و ناموفق (rejection) را ساده می‌کنند.

مثال: استفاده از .resolves/.rejects

// dataService.test.js (continued)

test('fetches user data with .resolves', () => {
  return expect(fetchUserData(1)).resolves.toEqual({ id: 1, name: 'Alice' });
});

test('throws error for non-existent user with .rejects', () => {
  return expect(fetchUserData(2)).rejects.toThrow('User not found');
});

مدیریت تایمرها

برای توابعی که از setTimeout یا setInterval استفاده می‌کنند، Jest کنترل تایمر را فراهم می‌کند.

مثال: کنترل تایمرها

// delayedGreeter.js
export const greetAfterDelay = (name, callback) => {
  setTimeout(() => {
    callback(`Hello, ${name}!`);
  }, 1000);
};

// delayedGreeter.test.js
import { greetAfterDelay } from './delayedGreeter';

jest.useFakeTimers(); // فعال کردن تایمرهای ساختگی

test('greets after delay', () => {
  const mockCallback = jest.fn();
  greetAfterDelay('World', mockCallback);

  // پیش بردن تایمرها به اندازه ۱۰۰۰ میلی‌ثانیه
  jest.advanceTimersByTime(1000);

  expect(mockCallback).toHaveBeenCalledTimes(1);
  expect(mockCallback).toHaveBeenCalledWith('Hello, World!');
});

// در صورت نیاز، تایمرهای واقعی را بازیابی کنید
jest.useRealTimers();

سازماندهی و ساختار تست

با رشد مجموعه تست شما، سازماندهی برای قابلیت نگهداری حیاتی می‌شود.

بلوک‌های Describe و It

از describe برای گروه‌بندی تست‌های مرتبط و از it (یا test) برای موارد تست فردی استفاده کنید. این ساختار، ماژولار بودن برنامه را منعکس می‌کند.

مثال: تست‌های ساختاریافته

describe('User Authentication Service', () => {
  let authService;

  beforeEach(() => {
    // راه‌اندازی mockها یا نمونه‌های سرویس قبل از هر تست
    authService = require('./authService');
    jest.spyOn(authService, 'login').mockImplementation(() => Promise.resolve({ token: 'fake_token' }));
  });

  afterEach(() => {
    // پاکسازی mockها
    jest.restoreAllMocks();
  });

  describe('login functionality', () => {
    it('should successfully log in a user with valid credentials', async () => {
      const result = await authService.login('user@example.com', 'password123');
      expect(result.token).toBeDefined();
      // ... more assertions ...
    });

    it('should fail login with invalid credentials', async () => {
      jest.spyOn(authService, 'login').mockRejectedValue(new Error('Invalid credentials'));
      await expect(authService.login('user@example.com', 'wrong_password')).rejects.toThrow('Invalid credentials');
    });
  });

  describe('logout functionality', () => {
    it('should clear user session', async () => {
      // Test logout logic...
    });
  });
});

هوک‌های راه‌اندازی (Setup) و پاکسازی (Teardown)

این هوک‌ها برای راه‌اندازی داده‌های mock، اتصالات پایگاه داده یا پاکسازی منابع بین تست‌ها ضروری هستند.

تست برای مخاطبان جهانی

هنگام توسعه برنامه‌ها برای مخاطبان جهانی، ملاحظات تست گسترش می‌یابد:

بین‌المللی‌سازی (i18n) و محلی‌سازی (l10n)

اطمینان حاصل کنید که UI و پیام‌های شما به درستی با زبان‌ها و فرمت‌های منطقه‌ای مختلف سازگار می‌شوند.

مثال: تست فرمت‌بندی تاریخ محلی‌سازی شده

// dateUtils.js
export const formatLocalizedDate = (date, locale) => {
  return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', day: 'numeric' }).format(date);
};

// dateUtils.test.js
import { formatLocalizedDate } from './dateUtils';

test('formats date correctly for US locale', () => {
  const date = new Date(2023, 10, 15); // November 15, 2023
  expect(formatLocalizedDate(date, 'en-US')).toBe('11/15/2023');
});

test('formats date correctly for German locale', () => {
  const date = new Date(2023, 10, 15);
  expect(formatLocalizedDate(date, 'de-DE')).toBe('15.11.2023');
});

آگاهی از منطقه زمانی

تست کنید که برنامه شما چگونه مناطق زمانی مختلف را مدیریت می‌کند، به خصوص برای ویژگی‌هایی مانند زمان‌بندی یا به‌روزرسانی‌های بلادرنگ. Mock کردن ساعت سیستم یا استفاده از کتابخانه‌هایی که مناطق زمانی را انتزاعی می‌کنند، می‌تواند مفید باشد.

ظرافت‌های فرهنگی در داده‌ها

در نظر بگیرید که اعداد، ارزها و سایر نمایش‌های داده ممکن است در فرهنگ‌های مختلف چگونه درک یا انتظار داشته شوند. Matcherهای سفارشی در اینجا می‌توانند به ویژه مفید باشند.

تکنیک‌ها و استراتژی‌های پیشرفته

توسعه آزمون‌محور (TDD) و توسعه رفتارمحور (BDD)

Jest به خوبی با متدولوژی‌های TDD (قرمز-سبز-بازآفرینی) و BDD (داده-وقتی-آنگاه) هماهنگ است. تست‌هایی بنویسید که رفتار مورد نظر را قبل از نوشتن کد پیاده‌سازی توصیف می‌کنند. این کار تضمین می‌کند که کد از ابتدا با در نظر گرفتن قابلیت تست‌پذیری نوشته می‌شود.

تست یکپارچه‌سازی با Jest

در حالی که Jest در تست‌های واحد برتری دارد، می‌توان از آن برای تست‌های یکپارچه‌سازی نیز استفاده کرد. Mock کردن وابستگی‌های کمتر یا استفاده از ابزارهایی مانند گزینه runInBand در Jest می‌تواند کمک کند.

مثال: تست تعامل با API (ساده‌شده)

// apiService.js
import axios from 'axios';

const API_BASE_URL = 'https://api.example.com';

export const createProduct = async (productData) => {
  const response = await axios.post(`${API_BASE_URL}/products`, productData);
  return response.data;
};

// apiService.test.js (Integration test)
import axios from 'axios';
import { createProduct } from './apiService';

// Mock axios for integration tests to control the network layer
jest.mock('axios');

test('creates a product via API', async () => {
  const mockProduct = { id: 1, name: 'Gadget' };
  const responseData = { success: true, product: mockProduct };

  axios.post.mockResolvedValue({
    data: responseData,
    status: 201,
    headers: { 'content-type': 'application/json' },
  });

  const newProductData = { name: 'Gadget', price: 99.99 };
  const result = await createProduct(newProductData);

  expect(axios.post).toHaveBeenCalledWith(`${process.env.API_BASE_URL || 'https://api.example.com'}/products`, newProductData);
  expect(result).toEqual(responseData);
});

موازی‌سازی و پیکربندی

Jest می‌تواند تست‌ها را به صورت موازی اجرا کند تا سرعت اجرا را افزایش دهد. این را در فایل jest.config.js خود پیکربندی کنید. به عنوان مثال، تنظیم maxWorkers تعداد فرآیندهای موازی را کنترل می‌کند.

گزارش‌های پوشش تست (Coverage)

از گزارش‌دهی پوشش داخلی Jest برای شناسایی بخش‌هایی از کدبیس خود که تست نشده‌اند، استفاده کنید. تست‌ها را با --coverage اجرا کنید تا گزارش‌های دقیقی تولید شود.

jest --coverage

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

نتیجه‌گیری

تسلط بر الگوهای تست پیشرفته Jest گامی مهم در جهت ساخت نرم‌افزاری قابل اعتماد، با قابلیت نگهداری بالا و با کیفیت برای مخاطبان جهانی است. با استفاده مؤثر از mocking، تست snapshot، matchers سفارشی و تکنیک‌های تست ناهمگام، می‌توانید استحکام مجموعه تست خود را افزایش داده و اطمینان بیشتری نسبت به رفتار برنامه خود در سناریوها و مناطق مختلف کسب کنید. پذیرش این الگوها تیم‌های توسعه در سراسر جهان را قادر می‌سازد تا تجربیات کاربری استثنایی ارائه دهند.

همین امروز شروع به گنجاندن این تکنیک‌های پیشرفته در گردش کار خود کنید تا شیوه‌های تست جاوا اسکریپت خود را ارتقا دهید.