راهنمای تست اپلیکیشن‌های React Native در ۲۰۲۶: از Jest تا Detox و Maestro

راهنمای عملی تست‌نویسی React Native در ۲۰۲۶. از تست واحد با Jest و تست کامپوننت با RNTL تا تست سرتاسری با Detox و Maestro، همراه با نمونه کد واقعی و مقایسه ابزارها.

مقدمه

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

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

خبر خوب اینکه در سال ۲۰۲۶، اکوسیستم تست React Native به بلوغ قابل توجهی رسیده. ابزارهایی مثل Jest، React Native Testing Library (RNTL)، Detox و Maestro هر کدام بخشی از هرم تست را پوشش می‌دهند و با ترکیب آن‌ها می‌توانید اطمینان بالایی از کیفیت اپلیکیشنتان داشته باشید. در این راهنما، تمام سطوح تست را از تست واحد (Unit Test) تا تست سرتاسری (E2E) با مثال‌های عملی و کدهای واقعی بررسی می‌کنیم.

هرم تست در React Native

قبل از اینکه دست به کد شویم، بیایید یک نگاه به هرم تست (Testing Pyramid) بیندازیم. این مدل کمک می‌کند نسبت صحیح انواع مختلف تست در پروژه‌تان را تعیین کنید:

  • تست واحد (Unit Tests) - پایه هرم: سریع‌ترین و ارزان‌ترین نوع تست. توابع و منطق کسب‌وکار را به صورت ایزوله تست می‌کند و باید بیشترین تعداد تست‌ها را تشکیل دهد (حدود ۷۰٪).
  • تست کامپوننت و یکپارچگی (Component/Integration Tests) - میانه هرم: تعامل بین کامپوننت‌ها و رندر صحیح رابط کاربری را بررسی می‌کند. تعادلی بین سرعت و اطمینان ارائه می‌دهد (حدود ۲۰٪).
  • تست سرتاسری (E2E Tests) - رأس هرم: کل جریان کاربر را از ابتدا تا انتها شبیه‌سازی می‌کند. بالاترین سطح اطمینان، اما کندترین و شکننده‌ترین نوع تست (حدود ۱۰٪).

تست واحد با Jest

Jest موتور تست پیش‌فرض React Native است که توسط Meta توسعه یافته. از نسخه ۰.۳۸ به بعد، Jest به صورت خودکار در پروژه‌های React Native پیکربندی شده و با قابلیت‌هایی مثل اجرای موازی تست‌ها، پوشش کد (Code Coverage) و Mocking داخلی، واقعاً یک ابزار قدرتمند برای تست واحد محسوب می‌شود.

راه‌اندازی Jest در پروژه Expo

اگر از Expo استفاده می‌کنید، کار خیلی ساده است. پکیج jest-expo تنظیمات لازم را فراهم می‌کند:

npm install --save-dev jest-expo @types/jest

بعد در فایل package.json تنظیمات زیر را اضافه کنید:

{
  "scripts": {
    "test": "jest"
  },
  "jest": {
    "preset": "jest-expo",
    "transformIgnorePatterns": [
      "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg)"
    ]
  }
}

نوشتن اولین تست واحد

خب، بیایید اولین تست واحدمان را بنویسیم. فرض کنید یک تابع کمکی برای فرمت‌بندی قیمت داریم:

// utils/formatPrice.ts
export function formatPrice(price: number, currency: string = 'ریال'): string {
  if (price < 0) throw new Error('قیمت نمی‌تواند منفی باشد');
  const formatted = price.toLocaleString('fa-IR');
  return `${formatted} ${currency}`;
}

export function calculateDiscount(price: number, percent: number): number {
  if (percent < 0 || percent > 100) {
    throw new Error('درصد تخفیف باید بین ۰ تا ۱۰۰ باشد');
  }
  return price * (1 - percent / 100);
}

و حالا تستش:

// utils/__tests__/formatPrice.test.ts
import { formatPrice, calculateDiscount } from '../formatPrice';

describe('formatPrice', () => {
  it('باید قیمت را با فرمت فارسی و واحد پولی برگرداند', () => {
    expect(formatPrice(50000)).toBe('۵۰٬۰۰۰ ریال');
  });

  it('باید واحد پولی سفارشی را پشتیبانی کند', () => {
    expect(formatPrice(1000, 'تومان')).toBe('۱٬۰۰۰ تومان');
  });

  it('باید برای قیمت منفی خطا پرتاب کند', () => {
    expect(() => formatPrice(-100)).toThrow('قیمت نمی‌تواند منفی باشد');
  });
});

describe('calculateDiscount', () => {
  it('باید تخفیف ۲۰ درصدی را صحیح محاسبه کند', () => {
    expect(calculateDiscount(100000, 20)).toBe(80000);
  });

  it('باید برای تخفیف ۰ درصد قیمت اصلی را برگرداند', () => {
    expect(calculateDiscount(50000, 0)).toBe(50000);
  });

  it('باید برای درصد نامعتبر خطا پرتاب کند', () => {
    expect(() => calculateDiscount(100, 150)).toThrow();
  });
});

Mocking در Jest

در تست واحد، وابستگی‌های خارجی مثل API‌ها، دیتابیس و ماژول‌های نیتیو باید Mock بشوند. اینجاست که Jest واقعاً می‌درخشد:

// services/__tests__/api.test.ts
import { fetchUserProfile } from '../api';

// Mock کردن fetch سراسری
global.fetch = jest.fn();

describe('fetchUserProfile', () => {
  beforeEach(() => {
    (fetch as jest.Mock).mockClear();
  });

  it('باید پروفایل کاربر را با موفقیت دریافت کند', async () => {
    const mockUser = { id: 1, name: 'علی', email: '[email protected]' };

    (fetch as jest.Mock).mockResolvedValueOnce({
      ok: true,
      json: async () => mockUser,
    });

    const result = await fetchUserProfile(1);
    expect(result).toEqual(mockUser);
    expect(fetch).toHaveBeenCalledWith(
      expect.stringContaining('/users/1')
    );
  });

  it('باید در صورت خطای سرور، خطا پرتاب کند', async () => {
    (fetch as jest.Mock).mockResolvedValueOnce({
      ok: false,
      status: 500,
    });

    await expect(fetchUserProfile(1)).rejects.toThrow();
  });
});

Mock کردن ماژول‌های نیتیو

یکی از چالش‌های خاص تست در React Native (که احتمالاً خیلی‌ها باهاش دست و پنجه نرم کرده‌اند) Mock کردن ماژول‌های نیتیو است. برای حل این مشکل، یک فایل setup در ریشه پروژه بسازید:

// jest.setup.js
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');

jest.mock('@react-native-async-storage/async-storage', () =>
  require('@react-native-async-storage/async-storage/jest/async-storage-mock')
);

jest.mock('react-native-reanimated', () => {
  const Reanimated = require('react-native-reanimated/mock');
  Reanimated.default.call = () => {};
  return Reanimated;
});

تست کامپوننت با React Native Testing Library

React Native Testing Library (RNTL) یک کتابخانه سبک و قدرتمند از Callstack است که فلسفه‌اش ساده است: «تست کنید همان‌طور که کاربر با اپلیکیشن تعامل می‌کند.» صادقانه بگویم، این رویکرد تست‌ها را خیلی معنادارتر می‌کند.

در سال ۲۰۲۶، RNTL جایگزین کامل react-test-renderer منسوخ‌شده شده است. react-test-renderer از React 19 به بعد دیگر پشتیبانی نمی‌شود، پس اگر هنوز ازش استفاده می‌کنید، وقت مهاجرت رسیده.

نصب RNTL

npm install --save-dev @testing-library/react-native @testing-library/jest-native

بعد matcher‌های jest-native را در فایل setup اضافه کنید:

// jest.setup.js
import '@testing-library/jest-native/extend-expect';

نوشتن تست کامپوننت

فرض کنید یک کامپوننت فرم ورود داریم (یکی از رایج‌ترین کامپوننت‌ها در هر اپلیکیشنی):

// components/LoginForm.tsx
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, ActivityIndicator } from 'react-native';

interface LoginFormProps {
  onSubmit: (email: string, password: string) => Promise<void>;
}

export function LoginForm({ onSubmit }: LoginFormProps) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleLogin = async () => {
    if (!email || !password) {
      setError('لطفاً ایمیل و رمز عبور را وارد کنید');
      return;
    }
    setLoading(true);
    setError('');
    try {
      await onSubmit(email, password);
    } catch (e) {
      setError('ورود ناموفق بود. لطفاً دوباره تلاش کنید.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <View>
      {error ? <Text testID="error-message">{error}</Text> : null}
      <TextInput
        testID="email-input"
        placeholder="ایمیل"
        value={email}
        onChangeText={setEmail}
        keyboardType="email-address"
        autoCapitalize="none"
      />
      <TextInput
        testID="password-input"
        placeholder="رمز عبور"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      <TouchableOpacity testID="login-button" onPress={handleLogin} disabled={loading}>
        {loading ? <ActivityIndicator /> : <Text>ورود</Text>}
      </TouchableOpacity>
    </View>
  );
}

حالا بیایید تست‌هایش را بنویسیم:

// components/__tests__/LoginForm.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { LoginForm } from '../LoginForm';

describe('LoginForm', () => {
  const mockOnSubmit = jest.fn();

  beforeEach(() => {
    mockOnSubmit.mockClear();
  });

  it('باید فیلدهای ایمیل و رمز عبور را نمایش دهد', () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);

    expect(screen.getByTestId('email-input')).toBeVisible();
    expect(screen.getByTestId('password-input')).toBeVisible();
    expect(screen.getByTestId('login-button')).toBeVisible();
  });

  it('باید در صورت خالی بودن فیلدها پیام خطا نمایش دهد', () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);

    fireEvent.press(screen.getByTestId('login-button'));

    expect(screen.getByTestId('error-message')).toHaveTextContent(
      'لطفاً ایمیل و رمز عبور را وارد کنید'
    );
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });

  it('باید با ایمیل و رمز صحیح، onSubmit را فراخوانی کند', async () => {
    mockOnSubmit.mockResolvedValueOnce(undefined);
    render(<LoginForm onSubmit={mockOnSubmit} />);

    fireEvent.changeText(screen.getByTestId('email-input'), '[email protected]');
    fireEvent.changeText(screen.getByTestId('password-input'), 'mypassword');
    fireEvent.press(screen.getByTestId('login-button'));

    await waitFor(() => {
      expect(mockOnSubmit).toHaveBeenCalledWith('[email protected]', 'mypassword');
    });
  });

  it('باید در صورت خطای سرور، پیام خطا نمایش دهد', async () => {
    mockOnSubmit.mockRejectedValueOnce(new Error('Server Error'));
    render(<LoginForm onSubmit={mockOnSubmit} />);

    fireEvent.changeText(screen.getByTestId('email-input'), '[email protected]');
    fireEvent.changeText(screen.getByTestId('password-input'), 'mypassword');
    fireEvent.press(screen.getByTestId('login-button'));

    await waitFor(() => {
      expect(screen.getByTestId('error-message')).toHaveTextContent(
        'ورود ناموفق بود'
      );
    });
  });
});

استفاده از userEvent برای تعامل واقعی‌تر

از نسخه ۱۲ به بعد، RNTL قابلیت userEvent را معرفی کرده که تعاملات کاربر را خیلی واقعی‌تر شبیه‌سازی می‌کند. برخلاف fireEvent که رویدادها را مستقیماً صدا می‌زند، userEvent تمام رویدادهای میانی را هم شبیه‌سازی می‌کند:

import { render, screen, userEvent, waitFor } from '@testing-library/react-native';

it('باید تایپ کاربر را شبیه‌سازی کند', async () => {
  const user = userEvent.setup();
  render(<LoginForm onSubmit={mockOnSubmit} />);

  await user.type(screen.getByTestId('email-input'), '[email protected]');
  await user.type(screen.getByTestId('password-input'), 'secret123');
  await user.press(screen.getByTestId('login-button'));

  await waitFor(() => {
    expect(mockOnSubmit).toHaveBeenCalledWith('[email protected]', 'secret123');
  });
});

تست Snapshot

تست Snapshot یک روش سریع برای اطمینان از تغییر نکردن خروجی رندر کامپوننت‌هاست. ایده‌اش ساده است: Jest یک اسنپ‌شات از خروجی رندرشده می‌گیرد و در اجراهای بعدی مقایسه‌اش می‌کند.

// components/__tests__/UserCard.snapshot.test.tsx
import React from 'react';
import { render } from '@testing-library/react-native';
import { UserCard } from '../UserCard';

it('باید با اسنپ‌شات ذخیره‌شده مطابقت داشته باشد', () => {
  const tree = render(
    <UserCard
      name="علی احمدی"
      avatar="https://example.com/avatar.jpg"
      role="توسعه‌دهنده"
    />
  );

  expect(tree.toJSON()).toMatchSnapshot();
});

اگر تغییری عمدی در کامپوننت ایجاد کردید و اسنپ‌شات قدیمی شد:

npx jest --updateSnapshot

یک نکته مهم: تست Snapshot را جایگزین تست‌های رفتاری نکنید. این تست‌ها بیشتر نقش یک شبکه ایمنی (Safety Net) دارند و فقط تغییرات ناخواسته در UI را شناسایی می‌کنند. راستش، تجربه نشان داده خیلی از تیم‌ها بدون فکر اسنپ‌شات‌ها را آپدیت می‌کنند و این کار ارزش تست را از بین می‌برد.

تست سرتاسری (E2E) با Detox

Detox یک فریم‌ورک تست سرتاسری Gray-box است که توسط Wix مخصوص React Native ساخته شده. شرکت‌های بزرگی مثل Shopify و خود Wix ازش استفاده می‌کنند. تفاوت اصلی Detox با ابزارهای Black-box این است که فعالیت داخلی اپلیکیشن شما را رصد می‌کند و به طور خودکار منتظر آماده شدنش می‌ماند.

این یعنی خداحافظی با sleep و setTimeout‌های تصادفی در تست‌ها!

نصب و پیکربندی Detox

# نصب CLI سراسری
npm install -g detox-cli

# نصب وابستگی‌های پروژه
npm install --save-dev detox jest-circus

# ایجاد تنظیمات اولیه
npx detox init

فایل .detoxrc.js در ریشه پروژه ایجاد می‌شود. یک نمونه پیکربندی کامل:

// .detoxrc.js
/** @type {import('detox').DetoxConfig} */
module.exports = {
  logger: {
    level: process.env.CI ? 'debug' : undefined,
  },
  testRunner: {
    args: {
      config: 'e2e/jest.config.js',
      maxWorkers: process.env.CI ? 2 : undefined,
      _: ['e2e'],
    },
  },
  apps: {
    'ios.release': {
      type: 'ios.app',
      build:
        'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -derivedDataPath ios/build',
      binaryPath:
        'ios/build/Build/Products/Release-iphonesimulator/MyApp.app',
    },
    'android.release': {
      type: 'android.apk',
      build:
        'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release',
      binaryPath:
        'android/app/build/outputs/apk/release/app-release.apk',
    },
  },
  devices: {
    simulator: {
      type: 'ios.simulator',
      device: { type: 'iPhone 16' },
    },
    emulator: {
      type: 'android.emulator',
      device: { avdName: 'Pixel_7_API_35' },
    },
  },
  configurations: {
    'ios.release': {
      device: 'simulator',
      app: 'ios.release',
    },
    'android.release': {
      device: 'emulator',
      app: 'android.release',
    },
  },
};

نوشتن تست E2E با Detox

بیایید جریان ورود کاربر را تست کنیم:

// e2e/login.test.js
describe('جریان ورود', () => {
  beforeAll(async () => {
    await device.launchApp({ newInstance: true });
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('باید صفحه ورود را نمایش دهد', async () => {
    await expect(element(by.text('ورود به حساب کاربری'))).toBeVisible();
    await expect(element(by.id('email-input'))).toBeVisible();
    await expect(element(by.id('password-input'))).toBeVisible();
  });

  it('باید با اطلاعات صحیح وارد شود', async () => {
    await element(by.id('email-input')).typeText('[email protected]');
    await element(by.id('password-input')).typeText('password123');
    await element(by.id('login-button')).tap();

    // Detox خودکار منتظر اتمام درخواست شبکه و انیمیشن‌ها می‌ماند
    await expect(element(by.text('داشبورد'))).toBeVisible();
  });

  it('باید پیام خطا برای رمز اشتباه نمایش دهد', async () => {
    await element(by.id('email-input')).typeText('[email protected]');
    await element(by.id('password-input')).typeText('wrongpass');
    await element(by.id('login-button')).tap();

    await expect(
      element(by.text('ایمیل یا رمز عبور اشتباه است'))
    ).toBeVisible();
  });
});

اجرای تست‌های Detox

# ساخت اپلیکیشن برای تست
npx detox build --configuration ios.release

# اجرای تست‌ها
npx detox test --configuration ios.release

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

  • همگام‌سازی خودکار: Detox ترد جاوااسکریپت، صف‌های UI نیتیو و فعالیت شبکه را رصد می‌کند و خودکار منتظر آماده شدن اپلیکیشن می‌ماند. نتیجه؟ نرخ شکنندگی (Flakiness) تست‌ها به کمتر از ۲٪ می‌رسد.
  • ارزیابی سمت اپلیکیشن: Detox assertion‌ها را مستقیماً در اپلیکیشن روی دستگاه اجرا می‌کند، نه در Node.js. این تفاوت ظریف اما مهمی است.
  • پشتیبانی از هر دو پلتفرم: تست‌ها با جاوااسکریپت نوشته می‌شوند اما روی شبیه‌ساز iOS (XCUITest) و امولاتور Android (Espresso) اجرا می‌شوند.

تست سرتاسری با Maestro: جایگزین مدرن

Maestro یک فریم‌ورک تست UI موبایل است که در سال‌های اخیر خیلی سر و صدا کرده. با بیش از ۱۰,۸۰۰ ستاره در GitHub (تا فوریه ۲۰۲۶)، به یک رقیب جدی برای Detox تبدیل شده است.

بزرگ‌ترین مزیت Maestro؟ سادگی فوق‌العاده‌اش. تست‌ها با YAML نوشته می‌شوند و اصلاً نیازی به دانش برنامه‌نویسی ندارند.

نصب Maestro

# نصب Maestro CLI
curl -Ls "https://get.maestro.mobile.dev" | bash

# بررسی نصب
maestro -v

یک نکته جالب درباره Maestro: برخلاف Detox، هیچ تغییری در کد پروژه‌تان لازم نیست. نه ماژول نیتیو، نه وابستگی توسعه و نه pod install. یک ابزار CLI کاملاً مستقل است که از طریق خود دستگاه با اپلیکیشن تعامل می‌کند.

نوشتن اولین تست با YAML

یک پوشه .maestro در ریشه پروژه بسازید و اولین فایل تست را بنویسید:

# .maestro/login-flow.yaml
appId: com.myapp
---
- clearState
- launchApp

# بررسی نمایش صفحه ورود
- assertVisible: "ورود به حساب کاربری"

# وارد کردن ایمیل
- tapOn:
    id: "email-input"
- inputText: "[email protected]"

# وارد کردن رمز عبور
- tapOn:
    id: "password-input"
- inputText: "password123"

# کلیک روی دکمه ورود
- tapOn: "ورود"

# بررسی ورود موفق
- assertVisible: "داشبورد"
- assertVisible: "خوش آمدید"

ببینید چقدر خوانا و ساده است! هر کسی در تیم (حتی بدون دانش برنامه‌نویسی عمیق) می‌تواند این تست را بخواند و بفهمد چه کار می‌کند.

اجرای تست Maestro

# اجرای یک فایل تست
maestro test .maestro/login-flow.yaml

# اجرای تمام تست‌ها در پوشه
maestro test .maestro/

# حالت مداوم - تست‌ها با تغییر فایل دوباره اجرا می‌شوند
maestro test --continuous .maestro/login-flow.yaml

دستورات پرکاربرد Maestro

# .maestro/complete-flow.yaml
appId: com.myapp
---
- clearState
- launchApp

# ناوبری
- tapOn: "ثبت‌نام"
- assertVisible: "ایجاد حساب جدید"

# پر کردن فرم
- tapOn:
    id: "name-input"
- inputText: "علی احمدی"

- tapOn:
    id: "email-input"
- inputText: "[email protected]"

# اسکرول کردن
- scrollUntilVisible:
    element: "ارسال"
    direction: DOWN

# ضربه طولانی
- longPressOn: "پروفایل"

# بازگشت به عقب
- pressKey: back

# اسکرین‌شات
- takeScreenshot: "after-signup"

مقایسه Detox و Maestro

خب، سؤال اصلی: Detox یا Maestro؟ جواب بستگی به نیازهای تیم شما دارد. بیایید یک مقایسه صادقانه داشته باشیم:

  • پیچیدگی راه‌اندازی: Detox نیاز به پیکربندی نیتیو دارد که می‌تواند وقت‌گیر باشد. Maestro در عرض چند دقیقه آماده می‌شود.
  • زبان تست: Detox از JavaScript/TypeScript استفاده می‌کند، Maestro از YAML ساده.
  • شکنندگی: هر دو نرخ شکنندگی پایینی دارند. Detox کمتر از ۲٪ گزارش می‌کند و Maestro هم بسیار مقاوم عمل می‌کند.
  • پشتیبانی پلتفرم: Detox مختص React Native است. اما Maestro از React Native، Flutter، Swift، Kotlin و حتی وب پشتیبانی می‌کند.
  • مخاطب: Detox برای تیم‌هایی که به تعامل عمیق Gray-box نیاز دارند. Maestro برای تیم‌هایی که سرعت و سادگی اولویتشان است.
  • هزینه: هر دو رایگان و متن‌باز هستند. Maestro Cloud برای اجرای ابری تست‌ها هزینه جداگانه دارد.
  • CI/CD: هر دو با پایپلاین‌های CI/CD سازگارند، اما Detox برای تست iOS به یک agent مک‌او‌اس نیاز دارد.

نظر شخصی: اگر تیمتان کاملاً روی React Native متمرکز است و به تعامل عمیق Gray-box نیاز دارید، Detox انتخاب بهتری است. اگر سرعت راه‌اندازی، سادگی و پشتیبانی چندسکویی براتان مهم‌تر است، Maestro را امتحان کنید. جالب اینجاست که بسیاری از تیم‌ها در سال ۲۰۲۶ از ترکیب هر دو استفاده می‌کنند و صادقانه بگویم، این واقعاً ایده خوبی است.

بهترین شیوه‌های تست‌نویسی در React Native

الگوی Arrange-Act-Assert

تقریباً هر فریم‌ورک تستی از الگوی AAA پیروی می‌کند: اول محیط تست را آماده کنید (Arrange)، بعد عمل مورد نظر را انجام دهید (Act) و در نهایت نتیجه را بررسی کنید (Assert). رعایت این الگوی ساده، تست‌ها را خوانا و قابل نگهداری نگه می‌دارد.

استقلال تست‌ها

هر تست باید مستقل از بقیه اجرا شود. نتیجه یک تست نباید روی تست دیگر تأثیر بگذارد. از beforeEach برای بازنشانی وضعیت قبل از هر تست استفاده کنید.

این نکته ساده‌ای به نظر می‌رسد، اما تعداد باگ‌هایی که به خاطر وابستگی بین تست‌ها ایجاد می‌شود واقعاً باورنکردنی است.

testID به جای متن

در تست‌های E2E، حتماً از testID برای شناسایی المان‌ها استفاده کنید. انتخاب المان بر اساس متن قابل مشاهده شکننده است، چون با تغییر متن یا چندزبانه شدن اپلیکیشن، تست‌ها می‌شکنند.

پوشش کد معنادار

به جای تعقیب عدد جادویی ۱۰۰٪ پوشش کد، روی تست مسیرهای بحرانی تمرکز کنید: جریان ورود، پرداخت، ثبت‌نام و هر عملیاتی که خطا در آن هزینه بالایی دارد.

# اجرای تست‌ها با گزارش پوشش کد
npx jest --coverage

# تنظیم آستانه پوشش در jest.config.js
module.exports = {
  coverageThreshold: {
    global: {
      branches: 70,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

ادغام با CI/CD

تست‌ها فقط وقتی ارزش دارند که به طور مداوم اجرا شوند. یک تست که فقط روی لپ‌تاپ توسعه‌دهنده اجرا می‌شود، تقریباً بی‌فایده است. این یک نمونه پیکربندی GitHub Actions برای اجرای خودکار تست‌ها:

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]

jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx jest --coverage
      - uses: codecov/codecov-action@v4

  e2e-ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx detox build --configuration ios.release
      - run: npx detox test --configuration ios.release

سوالات متداول

آیا برای پروژه‌های کوچک React Native هم تست‌نویسی لازم است؟

بله، حتی در پروژه‌های کوچک. لازم نیست از همان ابتدا تست E2E بنویسید، اما حداقل تست‌های واحد برای منطق کسب‌وکار و تست‌های کامپوننت برای فرم‌ها و تعاملات کاربری را در نظر بگیرید. هزینه اضافه کردن تست بعد از توسعه همیشه بیشتر از نوشتنش همزمان با کد است.

تفاوت fireEvent و userEvent در RNTL چیست؟

fireEvent رویدادها را مستقیماً روی کامپوننت صدا می‌زند و سریع‌تر اجرا می‌شود. اما userEvent رفتار واقعی کاربر را شبیه‌سازی می‌کند و تمام رویدادهای میانی (مثل focus و keyDown) را هم اجرا می‌کند. برای تست‌هایی که دقت تعامل مهم است، userEvent بهتر است. برای تست‌های ساده‌تر، fireEvent کافی است.

Detox بهتر است یا Maestro برای تست E2E؟

هر کدام جای خودشان را دارند. Detox با رویکرد Gray-box تست‌های پایدار و دقیقی ارائه می‌دهد و برای تیم‌های فنی که با جاوااسکریپت راحت هستند عالی است. Maestro با YAML ساده و راه‌اندازی سریع، برای تیم‌هایی که سرعت و سادگی را ترجیح می‌دهند بهتر است. خیلی از تیم‌ها هم از هر دو به صورت مکمل استفاده می‌کنند.

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

ماژول‌های نیتیو مثل دوربین، GPS یا AsyncStorage در محیط Node.js قابل اجرا نیستند و باید Mock بشوند. بهترین روش، ساختن یک فایل jest.setup.js و تعریف Mock‌ها در آن است. خبر خوب اینکه بسیاری از کتابخانه‌های محبوب مثل react-native-reanimated و async-storage، فایل‌های Mock آماده دارند که می‌توانید مستقیماً ازشان استفاده کنید.

حداقل پوشش تست مناسب چقدر است؟

یک قاعده خوب: حداقل ۸۰٪ برای توابع و خطوط کد و ۷۰٪ برای شاخه‌ها (Branches). اما صادقانه بگویم، مهم‌تر از عدد، کیفیت تست‌هاست. ۸۰٪ پوشش با تست‌های معنادار خیلی ارزشمندتر از ۱۰۰٪ پوشش با تست‌های سطحی است. روی مسیرهای بحرانی مثل احراز هویت و پرداخت تمرکز کنید.

درباره نویسنده Editorial Team

Our team of expert writers and editors.