testing-patterns について
React Native と Jest を使う開発者向けのテストパターン集です。テスト先行と振る舞いの検証を重視し、ファクトリー関数やカスタムレンダーでテストの一貫性を保ちます。
主な機能
- 01テスト駆動開発の手順
- 02振る舞いと公開 API の検証
- 03ファクトリー関数によるテストデータ作成
- 04必要な Provider を包むカスタムレンダー
- 05モジュールと GraphQL Hook のモック
- 06非同期クエリとユーザー操作のテスト
活用シーン
- 01コンポーネントの props とテストデータ用ファクトリー作成
- 02Provider を含む React Native 画面のテスト
- 03GraphQL クエリと非同期状態の検証
- 04フォーム操作と振る舞いのテスト作成
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ davila7/ claude-code-templates --skill testing-patternsインストール後、Claude Code で/testing-patternsを入力して読み込まれたか確認します。
詳細
テストパターンとユーティリティ
テストの考え方
テスト駆動開発 (TDD):
- 最初に失敗するテストを書く
- テストを通す最小限のコードを実装する
- グリーンになった後にリファクタリングする
- 失敗するテストなしにプロダクションコードを書かない
振る舞い駆動テスト:
- 実装ではなく振る舞いをテストする
- パブリックAPIとビジネス要件に焦点を当てる
- 実装の詳細をテストしない
- 振る舞いを説明する、わかりやすいテスト名を使う
ファクトリーパターン:
getMockX(overrides?: Partial<X>)関数を作成する- 適切なデフォルト値を用意する
- 特定のプロパティを上書きできるようにする
- テストを DRY で保守しやすく保つ
テストユーティリティ
カスタム render 関数
必要なプロバイダーでコンポーネントをラップするカスタム render を作成します:
// src/utils/testUtils.tsx
import { render } from '@testing-library/react-native';
import { ThemeProvider } from './theme';
export const renderWithTheme = (ui: React.ReactElement) => {
return render(
<ThemeProvider>{ui}</ThemeProvider>
);
};
使い方:
import { renderWithTheme } from 'utils/testUtils';
import { screen } from '@testing-library/react-native';
it('should render component', () => {
renderWithTheme(<MyComponent />);
expect(screen.getByText('Hello')).toBeTruthy();
});
ファクトリーパターン
コンポーネントプロパティのファクトリー
import { ComponentProps } from 'react';
const getMockMyComponentProps = (
overrides?: Partial<ComponentProps<typeof MyComponent>>
) => {
return {
title: 'Default Title',
count: 0,
onPress: jest.fn(),
isLoading: false,
...overrides,
};
};
// Usage in tests
it('should render with custom title', () => {
const props = getMockMyComponentProps({ title: 'Custom Title' });
renderWithTheme(<MyComponent {...props} />);
expect(screen.getByText('Custom Title')).toBeTruthy();
});
データファクトリー
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}
const getMockUser = (overrides?: Partial<User>): User => {
return {
id: '123',
name: 'John Doe',
email: '[email protected]',
role: 'user',
...overrides,
};
};
// Usage
it('should display admin badge for admin users', () => {
const user = getMockUser({ role: 'admin' });
renderWithTheme(<UserCard user={user} />);
expect(screen.getByText('Admin')).toBeTruthy();
});
モックパターン
モジュールのモック
// Mock entire module
jest.mock('utils/analytics');
// Mock with factory function
jest.mock('utils/analytics', () => ({
Analytics: {
logEvent: jest.fn(),
},
}));
// Access mock in test
const mockLogEvent = jest.requireMock('utils/analytics').Analytics.logEvent;
GraphQL フックのモック
jest.mock('./GetItems.generated', () => ({
useGetItemsQuery: jest.fn(),
}));
const mockUseGetItemsQuery = jest.requireMock(
'./GetItems.generated'
).useGetItemsQuery as jest.Mock;
// In test
mockUseGetItemsQuery.mockReturnValue({
data: { items: [] },
loading: false,
error: undefined,
});
テスト構造
describe('ComponentName', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('Rendering', () => {
it('should render component with default props', () => {});
it('should render loading state when loading', () => {});
});
describe('User interactions', () => {
it('should call onPress when button is clicked', async () => {});
});
describe('Edge cases', () => {
it('should handle empty data gracefully', () => {});
});
});
クエリパターン
// Element must exist
expect(screen.getByText('Hello')).toBeTruthy();
// Element should not exist
expect(screen.queryByText('Goodbye')).toBeNull();
// Element appears asynchronously
await waitFor(() => {
expect(screen.findByText('Loaded')).toBeTruthy();
});
ユーザー操作パターン
import { fireEvent, screen } from '@testing-library/react-native';
it('should submit form on button click', async () => {
const onSubmit = jest.fn();
renderWithTheme(<LoginForm onSubmit={onSubmit} />);
fireEvent.changeText(screen.getByLabelText('Email'), '[email protected]');
fireEvent.changeText(screen.getByLabelText('Password'), 'password123');
fireEvent.press(screen.getByTestId('login-button'));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalled();
});
});
避けるべきアンチパターン
実際の振る舞いではなくモックの振る舞いをテストする
// Bad - testing the mock
expect(mockFetchData).toHaveBeenCalled();
// Good - testing actual behavior
expect(screen.getByText('John Doe')).toBeTruthy();
ファクトリーを使用しない
// Bad - duplicated, inconsistent test data
it('test 1', () => {
const user = { id: '1', name: 'John', email: '[email protected]', role: 'user' };
});
it('test 2', () => {
const user = { id: '2', name: 'Jane', email: '[email protected]' }; // Missing role!
});
// Good - reusable factory
const user = getMockUser({ name: 'Custom Name' });
ベストプラクティス
- プロパティやデータには必ずファクトリー関数を使用する
- 実装ではなく振る舞いをテストする
- わかりやすいテスト名を使う
- describe ブロックで整理する
- テスト間でモックをクリアする
- テストは焦点を絞る - 1 テストにつき 1 つの振る舞い
テストの実行
# Run all tests
npm test
# Run with coverage
npm run test:coverage
# Run specific file
npm test ComponentName.test.tsx
他のスキルとの連携
- react-ui-patterns: すべての UI 状態(読み込み中、エラー、空、成功)をテストする
- systematic-debugging: バグを修正する前に、そのバグを再現するテストを書く
コメント
Markdown 対応