T-CREATOR

Jest と Enzyme を併用するパターンとその注意点

Jest と Enzyme を併用するパターンとその注意点

React アプリケーションのテストにおいて、Jest と Enzyme の組み合わせは非常に強力なツールです。Jest がテストランナーとアサーション機能を提供し、Enzyme が React コンポーネントのテストを簡単にするユーティリティを提供します。

この記事では、Jest と Enzyme を併用する際の実践的なパターンと、よくあるエラーとその解決策について詳しく解説します。実際のプロジェクトで遭遇する問題を想定し、具体的なコード例とエラーメッセージを含めて説明していきます。

Jest と Enzyme の役割分担

Jest と Enzyme はそれぞれ異なる役割を持ち、組み合わせることで包括的なテスト環境を構築できます。

Jest の役割:

  • テストランナーとしての機能
  • モック機能(jest.fn(), jest.mock())
  • アサーション(expect)
  • カバレッジレポート
  • スナップショットテスト

Enzyme の役割:

  • React コンポーネントのレンダリング
  • DOM 要素の検索と操作
  • イベントのシミュレーション
  • コンポーネントの状態確認

この組み合わせにより、単体テストから統合テストまで幅広いテストケースを効率的に書くことができます。

基本的なセットアップと環境構築

まず、Jest と Enzyme の基本的なセットアップから始めましょう。

必要なパッケージのインストール

bashyarn add --dev jest @types/jest enzyme enzyme-adapter-react-16 @types/enzyme

React 18 を使用する場合は、適切なアダプターを選択してください:

bashyarn add --dev enzyme-adapter-react-18

Jest 設定ファイルの作成

jest.config.jsファイルを作成して、Enzyme との連携を設定します:

javascriptmodule.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  moduleNameMapping: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
      '<rootDir>/__mocks__/fileMock.js',
  },
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.d.ts',
    '!src/index.tsx',
  ],
};

Enzyme 設定ファイルの作成

src​/​setupTests.jsファイルで Enzyme の設定を行います:

javascriptimport { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

configure({ adapter: new Adapter() });

React 18 を使用する場合:

javascriptimport { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-18';

configure({ adapter: new Adapter() });

TypeScript 対応の設定

TypeScript を使用する場合は、tsconfig.jsonにテスト用の設定を追加します:

json{
  "compilerOptions": {
    "types": ["jest", "enzyme", "node"]
  },
  "include": ["src/**/*", "**/*.test.ts", "**/*.test.tsx"]
}

パターン 1: コンポーネントのレンダリングテスト

Enzyme のshallowmountを使い分けて、効率的なコンポーネントテストを実装しましょう。

shallow vs mount の使い分け

shallowは子コンポーネントをレンダリングせず、単体テストに適しています。一方、mountは完全な DOM ツリーをレンダリングし、統合テストに適しています。

javascriptimport React from 'react';
import { shallow, mount } from 'enzyme';
import Button from '../components/Button';

// shallow: 単体テスト(子コンポーネントはレンダリングしない)
describe('Button Component - Shallow', () => {
  it('renders without crashing', () => {
    const wrapper = shallow(<Button>Click me</Button>);
    expect(wrapper.exists()).toBe(true);
  });

  it('displays the correct text', () => {
    const buttonText = 'Click me';
    const wrapper = shallow(<Button>{buttonText}</Button>);
    expect(wrapper.text()).toBe(buttonText);
  });
});

// mount: 統合テスト(完全なDOMツリーをレンダリング)
describe('Button Component - Mount', () => {
  it('renders with all child components', () => {
    const wrapper = mount(<Button>Click me</Button>);
    expect(wrapper.find('button').exists()).toBe(true);
  });
});

コンポーネントの存在確認

コンポーネントが正しくレンダリングされているかを確認するテストです:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import UserProfile from '../components/UserProfile';

describe('UserProfile Component', () => {
  it('renders user profile section', () => {
    const user = {
      name: 'John Doe',
      email: 'john@example.com',
    };
    const wrapper = shallow(<UserProfile user={user} />);

    // コンポーネントが存在することを確認
    expect(wrapper.find('.user-profile')).toHaveLength(1);
    expect(wrapper.find('.user-name')).toHaveLength(1);
    expect(wrapper.find('.user-email')).toHaveLength(1);
  });
});

props の受け渡しテスト

props が正しく渡され、表示されているかをテストします:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import ProductCard from '../components/ProductCard';

describe('ProductCard Component', () => {
  const mockProduct = {
    id: 1,
    name: 'Test Product',
    price: 1000,
    image: 'test-image.jpg',
  };

  it('displays product information correctly', () => {
    const wrapper = shallow(
      <ProductCard product={mockProduct} />
    );

    expect(wrapper.find('.product-name').text()).toBe(
      mockProduct.name
    );
    expect(wrapper.find('.product-price').text()).toBe(
      ${mockProduct.price}`
    );
    expect(wrapper.find('.product-image').prop('src')).toBe(
      mockProduct.image
    );
  });

  it('handles missing product gracefully', () => {
    const wrapper = shallow(<ProductCard product={null} />);

    expect(wrapper.find('.product-not-found')).toHaveLength(
      1
    );
  });
});

子コンポーネントのモック化

子コンポーネントをモック化して、親コンポーネントのテストを効率化します:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import ProductList from '../components/ProductList';

// 子コンポーネントをモック化
jest.mock('../components/ProductCard', () => {
  return function MockProductCard({ product }) {
    return (
      <div className='mock-product-card'>
        {product.name}
      </div>
    );
  };
});

describe('ProductList Component', () => {
  const mockProducts = [
    { id: 1, name: 'Product 1' },
    { id: 2, name: 'Product 2' },
  ];

  it('renders correct number of product cards', () => {
    const wrapper = shallow(
      <ProductList products={mockProducts} />
    );

    expect(wrapper.find('.mock-product-card')).toHaveLength(
      2
    );
  });
});

パターン 2: ユーザーインタラクションのテスト

ユーザーの操作をシミュレーションし、コンポーネントの反応をテストします。

クリックイベントのシミュレーション

ボタンクリックなどの基本的なインタラクションをテストします:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import Counter from '../components/Counter';

describe('Counter Component', () => {
  it('increments count when increment button is clicked', () => {
    const wrapper = shallow(<Counter />);
    const initialCount = wrapper.find('.count').text();

    // インクリメントボタンをクリック
    wrapper.find('.increment-btn').simulate('click');

    const newCount = wrapper.find('.count').text();
    expect(parseInt(newCount)).toBe(
      parseInt(initialCount) + 1
    );
  });

  it('calls onCountChange when count changes', () => {
    const mockOnCountChange = jest.fn();
    const wrapper = shallow(
      <Counter onCountChange={mockOnCountChange} />
    );

    wrapper.find('.increment-btn').simulate('click');

    expect(mockOnCountChange).toHaveBeenCalledWith(1);
  });
});

フォーム入力のテスト

フォームの入力と送信をテストします:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import LoginForm from '../components/LoginForm';

describe('LoginForm Component', () => {
  it('updates email input value', () => {
    const wrapper = shallow(<LoginForm />);
    const emailInput = wrapper.find('input[name="email"]');

    emailInput.simulate('change', {
      target: { name: 'email', value: 'test@example.com' },
    });

    expect(
      wrapper.find('input[name="email"]').prop('value')
    ).toBe('test@example.com');
  });

  it('submits form with correct data', () => {
    const mockOnSubmit = jest.fn();
    const wrapper = shallow(
      <LoginForm onSubmit={mockOnSubmit} />
    );

    // フォームに値を入力
    wrapper.find('input[name="email"]').simulate('change', {
      target: { name: 'email', value: 'test@example.com' },
    });
    wrapper
      .find('input[name="password"]')
      .simulate('change', {
        target: { name: 'password', value: 'password123' },
      });

    // フォームを送信
    wrapper
      .find('form')
      .simulate('submit', { preventDefault: jest.fn() });

    expect(mockOnSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123',
    });
  });
});

キーボードイベントの処理

キーボードイベントをテストします:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import SearchInput from '../components/SearchInput';

describe('SearchInput Component', () => {
  it('triggers search on Enter key press', () => {
    const mockOnSearch = jest.fn();
    const wrapper = shallow(
      <SearchInput onSearch={mockOnSearch} />
    );

    wrapper
      .find('input')
      .simulate('keypress', { key: 'Enter' });

    expect(mockOnSearch).toHaveBeenCalled();
  });

  it('does not trigger search on other key presses', () => {
    const mockOnSearch = jest.fn();
    const wrapper = shallow(
      <SearchInput onSearch={mockOnSearch} />
    );

    wrapper
      .find('input')
      .simulate('keypress', { key: 'A' });

    expect(mockOnSearch).not.toHaveBeenCalled();
  });
});

パターン 3: 条件付きレンダリングのテスト

条件に応じて表示が変わるコンポーネントをテストします。

条件分岐による表示切り替え

props や state に応じて表示が変わる場合のテストです:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import UserStatus from '../components/UserStatus';

describe('UserStatus Component', () => {
  it('shows online status when user is online', () => {
    const wrapper = shallow(<UserStatus isOnline={true} />);

    expect(wrapper.find('.status-online')).toHaveLength(1);
    expect(wrapper.find('.status-offline')).toHaveLength(0);
    expect(wrapper.find('.status-indicator').text()).toBe(
      'オンライン'
    );
  });

  it('shows offline status when user is offline', () => {
    const wrapper = shallow(
      <UserStatus isOnline={false} />
    );

    expect(wrapper.find('.status-offline')).toHaveLength(1);
    expect(wrapper.find('.status-online')).toHaveLength(0);
    expect(wrapper.find('.status-indicator').text()).toBe(
      'オフライン'
    );
  });
});

ローディング状態のテスト

非同期処理中のローディング表示をテストします:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import DataLoader from '../components/DataLoader';

describe('DataLoader Component', () => {
  it('shows loading spinner when loading is true', () => {
    const wrapper = shallow(
      <DataLoader loading={true} data={null} />
    );

    expect(wrapper.find('.loading-spinner')).toHaveLength(
      1
    );
    expect(wrapper.find('.data-content')).toHaveLength(0);
  });

  it('shows data content when loading is false', () => {
    const mockData = { id: 1, name: 'Test Data' };
    const wrapper = shallow(
      <DataLoader loading={false} data={mockData} />
    );

    expect(wrapper.find('.loading-spinner')).toHaveLength(
      0
    );
    expect(wrapper.find('.data-content')).toHaveLength(1);
    expect(wrapper.find('.data-name').text()).toBe(
      mockData.name
    );
  });
});

エラー状態の表示確認

エラーが発生した場合の表示をテストします:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import ErrorBoundary from '../components/ErrorBoundary';

describe('ErrorBoundary Component', () => {
  it('shows error message when error occurs', () => {
    const wrapper = shallow(<ErrorBoundary />);
    const error = new Error('Test error');

    // エラーを発生させる
    wrapper
      .instance()
      .componentDidCatch(error, { componentStack: '' });
    wrapper.setState({ hasError: true, error });

    expect(wrapper.find('.error-message')).toHaveLength(1);
    expect(wrapper.find('.error-details').text()).toContain(
      'Test error'
    );
  });

  it('shows children when no error occurs', () => {
    const wrapper = shallow(
      <ErrorBoundary>
        <div className='child-component'>Child Content</div>
      </ErrorBoundary>
    );

    expect(wrapper.find('.child-component')).toHaveLength(
      1
    );
    expect(wrapper.find('.error-message')).toHaveLength(0);
  });
});

パターン 4: 非同期処理のテスト

API 呼び出しや非同期処理を含むコンポーネントをテストします。

API 呼び出しのモック化

外部 API の呼び出しをモック化してテストします:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import UserList from '../components/UserList';

// API関数をモック化
jest.mock('../api/userApi', () => ({
  fetchUsers: jest.fn(),
}));

import { fetchUsers } from '../api/userApi';

describe('UserList Component', () => {
  beforeEach(() => {
    fetchUsers.mockClear();
  });

  it('loads and displays users', async () => {
    const mockUsers = [
      { id: 1, name: 'User 1' },
      { id: 2, name: 'User 2' },
    ];

    fetchUsers.mockResolvedValue(mockUsers);

    const wrapper = shallow(<UserList />);

    // コンポーネントのマウントを待つ
    await wrapper.instance().componentDidMount();
    wrapper.update();

    expect(fetchUsers).toHaveBeenCalled();
    expect(wrapper.find('.user-item')).toHaveLength(2);
  });

  it('handles API errors gracefully', async () => {
    const error = new Error('API Error');
    fetchUsers.mockRejectedValue(error);

    const wrapper = shallow(<UserList />);

    await wrapper.instance().componentDidMount();
    wrapper.update();

    expect(wrapper.find('.error-message')).toHaveLength(1);
    expect(wrapper.find('.error-message').text()).toContain(
      'API Error'
    );
  });
});

Promise/async-await の処理

非同期処理の完了を待ってテストを実行します:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import AsyncComponent from '../components/AsyncComponent';

describe('AsyncComponent', () => {
  it('updates state after async operation', async () => {
    const wrapper = shallow(<AsyncComponent />);

    // 非同期処理を開始
    wrapper.find('.async-button').simulate('click');

    // 非同期処理の完了を待つ
    await new Promise((resolve) => setTimeout(resolve, 0));
    wrapper.update();

    expect(wrapper.find('.result').text()).toBe(
      'Completed'
    );
  });
});

パターン 5: ルーティングとナビゲーションのテスト

React Router を使用したナビゲーション機能をテストします。

React Router との連携

ルーティング機能を含むコンポーネントをテストします:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import Navigation from '../components/Navigation';

describe('Navigation Component', () => {
  it('renders navigation links', () => {
    const wrapper = shallow(
      <MemoryRouter>
        <Navigation />
      </MemoryRouter>
    );

    expect(wrapper.find('Link')).toHaveLength(3);
    expect(wrapper.find('Link[to="/"]')).toHaveLength(1);
    expect(wrapper.find('Link[to="/about"]')).toHaveLength(
      1
    );
    expect(
      wrapper.find('Link[to="/contact"]')
    ).toHaveLength(1);
  });

  it('navigates to correct route when link is clicked', () => {
    const wrapper = shallow(
      <MemoryRouter>
        <Navigation />
      </MemoryRouter>
    );

    const aboutLink = wrapper.find('Link[to="/about"]');
    aboutLink.simulate('click');

    // ナビゲーション後の状態を確認
    expect(
      wrapper.find('Navigation').props().location.pathname
    ).toBe('/about');
  });
});

ページ遷移のシミュレーション

プログラムによるページ遷移をテストします:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import LoginPage from '../components/LoginPage';

// useHistoryフックをモック化
jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  useHistory: () => ({
    push: jest.fn(),
    replace: jest.fn(),
  }),
}));

describe('LoginPage Component', () => {
  it('redirects to dashboard after successful login', () => {
    const mockHistory = { push: jest.fn() };
    jest
      .spyOn(require('react-router-dom'), 'useHistory')
      .mockReturnValue(mockHistory);

    const wrapper = shallow(<LoginPage />);

    // ログイン処理を実行
    wrapper
      .find('form')
      .simulate('submit', { preventDefault: jest.fn() });

    expect(mockHistory.push).toHaveBeenCalledWith(
      '/dashboard'
    );
  });
});

パターン 6: グローバル状態管理のテスト

Redux や Context API を使用した状態管理をテストします。

Redux との連携

Redux を使用したコンポーネントをテストします:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import UserProfile from '../components/UserProfile';

const mockStore = configureStore([]);

describe('UserProfile Component with Redux', () => {
  let store;

  beforeEach(() => {
    store = mockStore({
      user: {
        profile: {
          id: 1,
          name: 'John Doe',
          email: 'john@example.com',
        },
        loading: false,
        error: null,
      },
    });
  });

  it('displays user profile from Redux store', () => {
    const wrapper = shallow(
      <Provider store={store}>
        <UserProfile />
      </Provider>
    );

    expect(wrapper.find('.user-name').text()).toBe(
      'John Doe'
    );
    expect(wrapper.find('.user-email').text()).toBe(
      'john@example.com'
    );
  });

  it('dispatches action when update button is clicked', () => {
    const wrapper = shallow(
      <Provider store={store}>
        <UserProfile />
      </Provider>
    );

    wrapper.find('.update-button').simulate('click');

    const actions = store.getActions();
    expect(actions).toContainEqual({
      type: 'UPDATE_USER_PROFILE',
      payload: { name: 'Jane Doe' },
    });
  });
});

Context API との連携

React Context を使用したコンポーネントをテストします:

javascriptimport React from 'react';
import { shallow } from 'enzyme';
import { ThemeProvider } from '../contexts/ThemeContext';
import ThemedButton from '../components/ThemedButton';

describe('ThemedButton Component', () => {
  it('applies light theme styles', () => {
    const wrapper = shallow(
      <ThemeProvider value={{ theme: 'light' }}>
        <ThemedButton>Click me</ThemedButton>
      </ThemeProvider>
    );

    expect(
      wrapper.find('button').hasClass('theme-light')
    ).toBe(true);
  });

  it('applies dark theme styles', () => {
    const wrapper = shallow(
      <ThemeProvider value={{ theme: 'dark' }}>
        <ThemedButton>Click me</ThemedButton>
      </ThemeProvider>
    );

    expect(
      wrapper.find('button').hasClass('theme-dark')
    ).toBe(true);
  });
});

よくあるエラーとトラブルシューティング

実際の開発で遭遇するエラーとその解決策を紹介します。

セットアップ時のエラー

エラー 1: Enzyme adapter not found

vbnetError: Enzyme Internal Error: Enzyme expects an adapter to be configured, but found none.

解決策:

javascript// setupTests.jsでアダプターを正しく設定
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'; // または適切なバージョン

configure({ adapter: new Adapter() });

エラー 2: Cannot find module 'enzyme'

javascriptError: Cannot find module 'enzyme' from 'setupTests.js'

解決策:

bashyarn add --dev enzyme enzyme-adapter-react-16

テスト実行時の問題

エラー 3: ShallowWrapper::dive() can only be called on components

sqlError: ShallowWrapper::dive() can only be called on components

解決策:

javascript// 間違った使い方
const wrapper = shallow(<div>Hello</div>);
wrapper.dive(); // エラー

// 正しい使い方
const wrapper = shallow(<MyComponent />);
wrapper.dive(); // OK

エラー 4: Method "simulate" is only meant to be run on a single node

vbnetError: Method "simulate" is only meant to be run on a single node. 0 found instead.

解決策:

javascript// 間違った使い方
const wrapper = shallow(<MyComponent />);
wrapper.find('.button').simulate('click'); // 要素が見つからない場合エラー

// 正しい使い方
const wrapper = shallow(<MyComponent />);
const button = wrapper.find('.button');
if (button.exists()) {
  button.simulate('click');
}

パフォーマンスの問題

問題: テストが遅い

解決策:

javascript// テストファイルの先頭でモックを設定
jest.mock('heavy-library', () => ({
  heavyFunction: jest.fn(),
}));

// 不要なテストをスキップ
describe.skip('Slow Test Suite', () => {
  // 重いテスト
});

デバッグのコツ

デバッグ用のヘルパー関数:

javascript// デバッグ用のユーティリティ
const debugWrapper = (wrapper, name = 'Wrapper') => {
  console.log(`${name} HTML:`, wrapper.debug());
  console.log(`${name} Props:`, wrapper.props());
  console.log(`${name} State:`, wrapper.state());
};

// テストで使用
it('debugs component state', () => {
  const wrapper = shallow(<MyComponent />);
  debugWrapper(wrapper, 'MyComponent');

  // テストロジック
});

ベストプラクティスと最適化

効率的で保守性の高いテストを書くためのベストプラクティスを紹介します。

テストの構造化

テストファイルを整理して読みやすくします:

javascriptimport React from 'react';
import { shallow, mount } from 'enzyme';
import UserComponent from '../components/UserComponent';

describe('UserComponent', () => {
  // テストデータの定義
  const defaultProps = {
    user: { id: 1, name: 'Test User' },
    onUpdate: jest.fn(),
    onDelete: jest.fn(),
  };

  // ヘルパー関数
  const createWrapper = (props = {}) => {
    return shallow(
      <UserComponent {...defaultProps} {...props} />
    );
  };

  // テストケース
  describe('Rendering', () => {
    it('renders user information correctly', () => {
      const wrapper = createWrapper();
      expect(wrapper.find('.user-name').text()).toBe(
        'Test User'
      );
    });
  });

  describe('User Interactions', () => {
    it('calls onUpdate when edit button is clicked', () => {
      const wrapper = createWrapper();
      wrapper.find('.edit-button').simulate('click');
      expect(defaultProps.onUpdate).toHaveBeenCalled();
    });
  });

  describe('Error Handling', () => {
    it('shows error message when user data is invalid', () => {
      const wrapper = createWrapper({ user: null });
      expect(wrapper.find('.error-message')).toHaveLength(
        1
      );
    });
  });
});

再利用可能なヘルパー関数

共通のテストロジックをヘルパー関数として抽出します:

javascript// test-utils.js
import React from 'react';
import { shallow, mount } from 'enzyme';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';

const mockStore = configureStore([]);

export const createMockStore = (initialState = {}) => {
  return mockStore(initialState);
};

export const renderWithRedux = (
  component,
  initialState = {}
) => {
  const store = createMockStore(initialState);
  return {
    wrapper: mount(
      <Provider store={store}>{component}</Provider>
    ),
    store,
  };
};

export const renderWithRouter = (
  component,
  { route = '/' } = {}
) => {
  window.history.pushState({}, 'Test page', route);
  return mount(component);
};

// テストファイルで使用
import {
  renderWithRedux,
  renderWithRouter,
} from '../test-utils';

describe('Component with Redux', () => {
  it('works with Redux store', () => {
    const { wrapper, store } = renderWithRedux(
      <MyComponent />,
      {
        user: { name: 'Test' },
      }
    );

    expect(wrapper.find('.user-name').text()).toBe('Test');
  });
});

テストデータの管理

テストデータを効率的に管理します:

javascript// test-data.js
export const mockUsers = [
  { id: 1, name: 'User 1', email: 'user1@example.com' },
  { id: 2, name: 'User 2', email: 'user2@example.com' },
];

export const mockProducts = [
  { id: 1, name: 'Product 1', price: 1000 },
  { id: 2, name: 'Product 2', price: 2000 },
];

export const createMockUser = (overrides = {}) => ({
  id: 1,
  name: 'Test User',
  email: 'test@example.com',
  ...overrides,
});

// テストファイルで使用
import { mockUsers, createMockUser } from '../test-data';

describe('UserList', () => {
  it('renders users', () => {
    const wrapper = shallow(<UserList users={mockUsers} />);
    expect(wrapper.find('.user-item')).toHaveLength(2);
  });

  it('handles single user', () => {
    const user = createMockUser({ name: 'Custom User' });
    const wrapper = shallow(<UserList users={[user]} />);
    expect(wrapper.find('.user-name').text()).toBe(
      'Custom User'
    );
  });
});

CI/CD での活用

継続的インテグレーションでのテスト実行を最適化します:

json// package.json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:ci": "jest --ci --coverage --watchAll=false"
  },
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}",
      "!src/**/*.d.ts",
      "!src/index.tsx"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

まとめ

Jest と Enzyme の併用により、React アプリケーションの包括的なテスト環境を構築できます。各パターンを適切に使い分けることで、効率的で保守性の高いテストを書くことができます。

主要なポイント:

  1. shallowmountの使い分けでテストの粒度を調整
  2. モック化を活用して外部依存を排除
  3. 非同期処理は適切に待機してテスト
  4. エラーハンドリングも含めてテストケースを作成
  5. ヘルパー関数でテストコードの重複を削減

チーム開発での運用:

  • テストの命名規則を統一
  • カバレッジ目標を設定
  • コードレビューでテストも含めて確認
  • CI/CD パイプラインで自動テスト実行

今後の展望:

React Testing Library の台頭により、Enzyme の使用は減少傾向にありますが、既存プロジェクトでの保守や、特定のテストケースでは依然として有用です。新しいプロジェクトでは、React Testing Library の使用も検討することをお勧めします。

関連リンク