jpskill.com
🛠️ 開発・MCP コミュニティ

rn-testing

Testing patterns for React Native with Jest and React Native Testing Library. Use when writing tests, mocking Expo modules, testing Zustand stores, or debugging test failures.

⚡ おすすめ: コマンド1行でインストール(60秒)

下記のコマンドをコピーしてターミナル(Mac/Linux)または PowerShell(Windows)に貼り付けてください。 ダウンロード → 解凍 → 配置まで全自動。

🍎 Mac / 🐧 Linux
mkdir -p ~/.claude/skills && cd ~/.claude/skills && curl -L -o rn-testing.zip https://jpskill.com/download/17863.zip && unzip -o rn-testing.zip && rm rn-testing.zip
🪟 Windows (PowerShell)
$d = "$env:USERPROFILE\.claude\skills"; ni -Force -ItemType Directory $d | Out-Null; iwr https://jpskill.com/download/17863.zip -OutFile "$d\rn-testing.zip"; Expand-Archive "$d\rn-testing.zip" -DestinationPath $d -Force; ri "$d\rn-testing.zip"

完了後、Claude Code を再起動 → 普通に「動画プロンプト作って」のように話しかけるだけで自動発動します。

💾 手動でダウンロードしたい(コマンドが難しい人向け)
  1. 1. 下の青いボタンを押して rn-testing.zip をダウンロード
  2. 2. ZIPファイルをダブルクリックで解凍 → rn-testing フォルダができる
  3. 3. そのフォルダを C:\Users\あなたの名前\.claude\skills\(Win)または ~/.claude/skills/(Mac)へ移動
  4. 4. Claude Code を再起動

⚠️ ダウンロード・利用は自己責任でお願いします。当サイトは内容・動作・安全性について責任を負いません。

🎯 このSkillでできること

下記の説明文を読むと、このSkillがあなたに何をしてくれるかが分かります。Claudeにこの分野の依頼をすると、自動で発動します。

📦 インストール方法 (3ステップ)

  1. 1. 上の「ダウンロード」ボタンを押して .skill ファイルを取得
  2. 2. ファイル名の拡張子を .skill から .zip に変えて展開(macは自動展開可)
  3. 3. 展開してできたフォルダを、ホームフォルダの .claude/skills/ に置く
    • · macOS / Linux: ~/.claude/skills/
    • · Windows: %USERPROFILE%\.claude\skills\

Claude Code を再起動すれば完了。「このSkillを使って…」と話しかけなくても、関連する依頼で自動的に呼び出されます。

詳しい使い方ガイドを見る →
最終更新
2026-05-18
取得日時
2026-05-18
同梱ファイル
1

📖 Skill本文(日本語訳)

※ 原文(英語/中国語)を Gemini で日本語化したものです。Claude 自身は原文を読みます。誤訳がある場合は原文をご確認ください。

React Native テスト

問題提起

React Native のテストでは、ネイティブモジュールの広範なモック、非同期操作の慎重な取り扱い、および Zustand ストアのテストパターンの理解が必要です。このコードベースには、確立されたパターンを持つ 30 以上のテストファイルがあります。


パターン: Zustand ストアのテスト

問題: ストアの状態がテスト間で永続化され、不安定なテストが発生します。

import { useAssessmentStore } from '@/stores/assessmentStore';

const initialState = {
  userAnswers: {},
  completedAssessmentAnswers: {},
  retakeAreas: new Set<string>(),
  loading: false,
};

describe('Assessment Store', () => {
  // 各テストの前にストアをリセット
  beforeEach(() => {
    useAssessmentStore.setState(initialState, true); // true = 状態全体を置き換える
  });

  it('saves answer to store', async () => {
    const store = useAssessmentStore.getState();

    await store.saveAnswer('q1', 4);

    expect(useAssessmentStore.getState().userAnswers['q1']).toBe(4);
  });

  it('enables retake for skill area', async () => {
    const store = useAssessmentStore.getState();

    await store.enableSkillAreaRetake('fundamentals');

    expect(useAssessmentStore.getState().retakeAreas.has('fundamentals')).toBe(true);
  });
});

重要なポイント:

  • setState(initialState, true) を使用して状態を置き換える(マージしない)
  • 非同期操作の後に getState() で新しい状態を取得する
  • ストアのテストでコンポーネントの再レンダリングに依存しない

パターン: 非同期ストア操作

問題: 適切な待機を伴う非同期 Zustand アクションのテスト。

import { act, waitFor } from '@testing-library/react-native';

it('loads completed answers', async () => {
  const store = useAssessmentStore.getState();

  // 非同期ストア操作を act でラップする
  await act(async () => {
    await store.loadCompletedAssessmentAnswers('assessment-123');
  });

  // 非同期完了後に状態を検証する
  await waitFor(() => {
    const state = useAssessmentStore.getState();
    expect(Object.keys(state.completedAssessmentAnswers).length).toBeGreaterThan(0);
  });
});

// 複雑なフローの場合は、各ステップを検証する
it('completes retake flow', async () => {
  const store = useAssessmentStore.getState();

  // ステップ 1
  await act(async () => {
    await store.loadCompletedAssessmentAnswers('assessment-123');
  });
  expect(useAssessmentStore.getState().completedAssessmentAnswers).toBeDefined();

  // ステップ 2
  await act(async () => {
    await store.enableSkillAreaRetake('fundamentals');
  });
  expect(useAssessmentStore.getState().retakeAreas.has('fundamentals')).toBe(true);

  // ステップ 3
  await act(async () => {
    await store.saveAnswer('q1', 4);
  });
  expect(useAssessmentStore.getState().userAnswers['q1']).toBe(4);
});

パターン: Expo モジュールのモック

問題: Expo モジュールには Jest 用のモックが必要です。

// __mocks__/expo-router.ts (または jest.setup.js 内)
jest.mock('expo-router', () => ({
  useRouter: () => ({
    push: jest.fn(),
    replace: jest.fn(),
    back: jest.fn(),
    dismiss: jest.fn(),
  }),
  useLocalSearchParams: () => ({}),
  useSegments: () => [],
  usePathname: () => '/',
  Link: ({ children }: { children: React.ReactNode }) => children,
  Stack: {
    Screen: () => null,
  },
}));

// expo-secure-store
jest.mock('expo-secure-store', () => ({
  getItemAsync: jest.fn(),
  setItemAsync: jest.fn(),
  deleteItemAsync: jest.fn(),
}));

// expo-constants
jest.mock('expo-constants', () => ({
  expoConfig: {
    extra: {
      apiUrl: 'http://test-api.local',
    },
  },
}));

// expo-haptics
jest.mock('expo-haptics', () => ({
  impactAsync: jest.fn(),
  notificationAsync: jest.fn(),
  selectionAsync: jest.fn(),
}));

jest.setup.js を確認してください - 多くのモックがグローバルに設定されています。


パターン: React Query のテスト

問題: React Query を使用するコンポーネントには QueryClientProvider が必要です。

// コードベースの既存のユーティリティを使用する
import { createTestQueryClient, QueryWrapper } from '@/__tests__/utils/react-query-test-utils';

// またはラッパーを作成する
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
        gcTime: 0,
      },
    },
  });
}

function QueryWrapper({ children }: { children: React.ReactNode }) {
  const queryClient = createTestQueryClient();
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

// テストでの使用法
import { render, waitFor } from '@testing-library/react-native';

it('fetches and displays data', async () => {
  const { getByText } = render(
    <QueryWrapper>
      <MyComponent />
    </QueryWrapper>
  );

  await waitFor(() => {
    expect(getByText('Loaded data')).toBeTruthy();
  });
});

パターン: カスタムフックのテスト

import { renderHook, act, waitFor } from '@testing-library/react-native';

describe('useAuth', () => {
  it('signs in user', async () => {
    const { result } = renderHook(() => useAuth(), {
      wrapper: AuthProvider, // フックがコンテキストを必要とする場合
    });

    await act(async () => {
      await result.current.signIn('token', mockUser);
    });

    expect(result.current.user).toEqual(mockUser);
    expect(result.current.token).toBe('token');
  });
});

// Zustand を使用したフック
describe('useAssessmentAnswers', () => {
  beforeEach(() => {
    useAssessmentStore.setState(initialState, true);
  });

  it('returns current answers', () => {
    // ストアを事前に入力する
    useAssessmentStore.setState({ userAnswers: { q1: 4 } });

    const { result } = renderHook(() => useAssessmentAnswers());

    expect(result.current.answers).toEqual({ q1: 4 });
  });
});

パターン: コンポーネントのテスト


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

describe('SessionCard', () => {
  const mockSession = {
    id: '1',
    title: 'Serve Practice',
    totalDuration:

(原文がここで切り詰められています)
📜 原文 SKILL.md(Claudeが読む英語/中国語)を展開

React Native Testing

Problem Statement

React Native testing requires extensive mocking of native modules, careful handling of async operations, and understanding of Zustand store testing patterns. This codebase has 30+ test files with established patterns.


Pattern: Zustand Store Testing

Problem: Store state persists between tests, causing flaky tests.

import { useAssessmentStore } from '@/stores/assessmentStore';

const initialState = {
  userAnswers: {},
  completedAssessmentAnswers: {},
  retakeAreas: new Set<string>(),
  loading: false,
};

describe('Assessment Store', () => {
  // Reset store before each test
  beforeEach(() => {
    useAssessmentStore.setState(initialState, true); // true = replace entire state
  });

  it('saves answer to store', async () => {
    const store = useAssessmentStore.getState();

    await store.saveAnswer('q1', 4);

    expect(useAssessmentStore.getState().userAnswers['q1']).toBe(4);
  });

  it('enables retake for skill area', async () => {
    const store = useAssessmentStore.getState();

    await store.enableSkillAreaRetake('fundamentals');

    expect(useAssessmentStore.getState().retakeAreas.has('fundamentals')).toBe(true);
  });
});

Key points:

  • Use setState(initialState, true) to replace (not merge) state
  • Get fresh state with getState() after async operations
  • Don't rely on component re-renders in store tests

Pattern: Async Store Operations

Problem: Testing async Zustand actions with proper waiting.

import { act, waitFor } from '@testing-library/react-native';

it('loads completed answers', async () => {
  const store = useAssessmentStore.getState();

  // Wrap async store operations in act
  await act(async () => {
    await store.loadCompletedAssessmentAnswers('assessment-123');
  });

  // Verify state after async completes
  await waitFor(() => {
    const state = useAssessmentStore.getState();
    expect(Object.keys(state.completedAssessmentAnswers).length).toBeGreaterThan(0);
  });
});

// For complex flows, verify each step
it('completes retake flow', async () => {
  const store = useAssessmentStore.getState();

  // Step 1
  await act(async () => {
    await store.loadCompletedAssessmentAnswers('assessment-123');
  });
  expect(useAssessmentStore.getState().completedAssessmentAnswers).toBeDefined();

  // Step 2
  await act(async () => {
    await store.enableSkillAreaRetake('fundamentals');
  });
  expect(useAssessmentStore.getState().retakeAreas.has('fundamentals')).toBe(true);

  // Step 3
  await act(async () => {
    await store.saveAnswer('q1', 4);
  });
  expect(useAssessmentStore.getState().userAnswers['q1']).toBe(4);
});

Pattern: Expo Module Mocking

Problem: Expo modules require mocks for Jest.

// __mocks__/expo-router.ts (or in jest.setup.js)
jest.mock('expo-router', () => ({
  useRouter: () => ({
    push: jest.fn(),
    replace: jest.fn(),
    back: jest.fn(),
    dismiss: jest.fn(),
  }),
  useLocalSearchParams: () => ({}),
  useSegments: () => [],
  usePathname: () => '/',
  Link: ({ children }: { children: React.ReactNode }) => children,
  Stack: {
    Screen: () => null,
  },
}));

// expo-secure-store
jest.mock('expo-secure-store', () => ({
  getItemAsync: jest.fn(),
  setItemAsync: jest.fn(),
  deleteItemAsync: jest.fn(),
}));

// expo-constants
jest.mock('expo-constants', () => ({
  expoConfig: {
    extra: {
      apiUrl: 'http://test-api.local',
    },
  },
}));

// expo-haptics
jest.mock('expo-haptics', () => ({
  impactAsync: jest.fn(),
  notificationAsync: jest.fn(),
  selectionAsync: jest.fn(),
}));

Check jest.setup.js - many mocks are already configured globally.


Pattern: React Query Testing

Problem: Components using React Query need QueryClientProvider.

// Use existing utility from codebase
import { createTestQueryClient, QueryWrapper } from '@/__tests__/utils/react-query-test-utils';

// Or create wrapper
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

function createTestQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        retry: false,
        gcTime: 0,
      },
    },
  });
}

function QueryWrapper({ children }: { children: React.ReactNode }) {
  const queryClient = createTestQueryClient();
  return (
    <QueryClientProvider client={queryClient}>
      {children}
    </QueryClientProvider>
  );
}

// Usage in tests
import { render, waitFor } from '@testing-library/react-native';

it('fetches and displays data', async () => {
  const { getByText } = render(
    <QueryWrapper>
      <MyComponent />
    </QueryWrapper>
  );

  await waitFor(() => {
    expect(getByText('Loaded data')).toBeTruthy();
  });
});

Pattern: Custom Hook Testing

import { renderHook, act, waitFor } from '@testing-library/react-native';

describe('useAuth', () => {
  it('signs in user', async () => {
    const { result } = renderHook(() => useAuth(), {
      wrapper: AuthProvider, // If hook needs context
    });

    await act(async () => {
      await result.current.signIn('token', mockUser);
    });

    expect(result.current.user).toEqual(mockUser);
    expect(result.current.token).toBe('token');
  });
});

// Hook with Zustand
describe('useAssessmentAnswers', () => {
  beforeEach(() => {
    useAssessmentStore.setState(initialState, true);
  });

  it('returns current answers', () => {
    // Pre-populate store
    useAssessmentStore.setState({ userAnswers: { q1: 4 } });

    const { result } = renderHook(() => useAssessmentAnswers());

    expect(result.current.answers).toEqual({ q1: 4 });
  });
});

Pattern: Component Testing

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

describe('SessionCard', () => {
  const mockSession = {
    id: '1',
    title: 'Serve Practice',
    totalDuration: 45, // Backend-calculated
  };

  it('displays session data from backend', () => {
    const { getByText } = render(<SessionCard session={mockSession} />);

    expect(getByText('Serve Practice')).toBeTruthy();
    expect(getByText('45 min')).toBeTruthy();
  });

  it('calls onPress when tapped', () => {
    const onPress = jest.fn();
    const { getByTestId } = render(
      <SessionCard session={mockSession} onPress={onPress} />
    );

    fireEvent.press(getByTestId('session-card'));

    expect(onPress).toHaveBeenCalledWith(mockSession.id);
  });
});

Pattern: Navigation Testing

import { useRouter } from 'expo-router';

jest.mock('expo-router');

describe('SettingsScreen', () => {
  const mockPush = jest.fn();

  beforeEach(() => {
    jest.clearAllMocks();
    (useRouter as jest.Mock).mockReturnValue({
      push: mockPush,
      back: jest.fn(),
    });
  });

  it('navigates to profile on button press', () => {
    const { getByText } = render(<SettingsScreen />);

    fireEvent.press(getByText('Edit Profile'));

    expect(mockPush).toHaveBeenCalledWith('/profile/edit');
  });
});

// Testing with route params
jest.mock('expo-router', () => ({
  useLocalSearchParams: () => ({ id: 'test-assessment-123' }),
  useRouter: () => ({ push: jest.fn() }),
}));

Pattern: Avoiding act() Warnings

Problem: "Warning: An update inside a test was not wrapped in act(...)"

// WRONG - state update happens after test
it('loads data', () => {
  render(<DataComponent />);
  // Component fetches data async, updates state after test ends
});

// CORRECT - wait for async completion
it('loads data', async () => {
  const { getByText } = render(<DataComponent />);

  // Wait for loading to complete
  await waitFor(() => {
    expect(getByText('Data loaded')).toBeTruthy();
  });
});

// CORRECT - use findBy* (has built-in waitFor)
it('loads data', async () => {
  const { findByText } = render(<DataComponent />);

  const element = await findByText('Data loaded');
  expect(element).toBeTruthy();
});

Pattern: Snapshot Testing

When to use:

  • UI components with stable structure
  • Design system components
  • Components where visual regression matters

When to avoid:

  • Components with dynamic content
  • Components that change frequently
  • Large component trees (brittle)
// Good snapshot candidate - stable UI component
it('renders correctly', () => {
  const tree = render(<Button title="Submit" />);
  expect(tree.toJSON()).toMatchSnapshot();
});

// Bad snapshot candidate - dynamic content
it('renders user list', () => {
  // Don't snapshot - list content varies
  // Instead, test specific behaviors
});

Pattern: Mocking API Calls

// Mock Orval-generated hooks
jest.mock('@/api/generated/assessments', () => ({
  useGetAssessment: jest.fn(() => ({
    data: mockAssessment,
    isLoading: false,
    error: null,
  })),
  useSubmitAssessment: jest.fn(() => ({
    mutate: jest.fn(),
    isLoading: false,
  })),
}));

// Mock with different states
import { useGetAssessment } from '@/api/generated/assessments';

it('shows loading state', () => {
  (useGetAssessment as jest.Mock).mockReturnValue({
    data: null,
    isLoading: true,
    error: null,
  });

  const { getByTestId } = render(<AssessmentScreen />);
  expect(getByTestId('loading-spinner')).toBeTruthy();
});

it('shows error state', () => {
  (useGetAssessment as jest.Mock).mockReturnValue({
    data: null,
    isLoading: false,
    error: new Error('Failed to load'),
  });

  const { getByText } = render(<AssessmentScreen />);
  expect(getByText('Failed to load')).toBeTruthy();
});

Recommended Test Utilities

__tests__/utils/react-query-test-utils.tsx  # QueryClient wrapper
jest.setup.js                                # Global mocks

Test Commands

npm test                      # Run all tests
npm test -- --watch           # Watch mode
npm test -- --coverage        # Coverage report
npm test -- SessionCard       # Run specific test file
npm test -- --updateSnapshot  # Update snapshots

Common Issues

Issue Solution
"Cannot find module" Check jest.setup.js module mappings
act() warning Wrap state updates in act(), use waitFor/findBy
Store state bleeding Add beforeEach with setState reset
Async test timeout Increase timeout or check for hanging promises
Mock not working Verify mock path matches import path exactly

Relationship to Other Skills

  • rn-async-patterns: Use post-condition validation in tests to verify async flows
  • rn-zustand-patterns: Understand getState() behavior for store tests
  • rn-state-flows: Integration tests should cover entire flows, not just units