0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsアプリケーションにおけるテスト戦略:Jest・React Testing Library・モック活用術

Posted at

Next.jsアプリケーションにおけるテスト戦略:Jest・React Testing Library・モック活用術

はじめに

モダンなWebアプリケーション開発において、テストは品質保証の要です。特にNext.jsのようなフルスタックフレームワークでは、APIルートからReactコンポーネントまで幅広い領域をカバーする必要があります。

本記事では、実際の多言語対応機能の実装を例に、Jest・React Testing Library・モックを活用した実践的なテスト戦略を解説します。

Jestとは何か

Jestは、Meta(旧Facebook)が開発したJavaScriptテスティングフレームワークです。

なぜJestを選ぶのか

// Jestの特徴的な機能
describe('Jestの利点', () => {
  it('設定がシンプル', () => {
    // ゼロコンフィグで動作
    expect(true).toBe(true);
  });

  it('豊富なマッチャー', () => {
    expect(value).toBeNull();
    expect(value).toBeDefined();
    expect(value).toHaveLength(3);
  });

  it('自動モック機能', () => {
    jest.mock('./module');
    // モジュールが自動的にモック化される
  });
});

Jestの主要機能

機能 説明 使用例
スナップショットテスト UIの変更を検知 コンポーネントの出力を記録・比較
カバレッジレポート テストカバー率を自動計測 jest --coverage
並列実行 テストを並列で高速実行 デフォルトで有効
ウォッチモード ファイル変更を監視して再実行 jest --watch

フロントエンドとバックエンドのテスト

Next.jsはフルスタックフレームワークのため、両方のテストが必要です。

テストの違い

観点 フロントエンド バックエンド
テスト対象 React Components, Hooks, UI API Routes, ビジネスロジック
環境 jsdom (ブラウザ環境をエミュレート) node (Node.js環境)
ツール React Testing Library Supertest, node-fetch
モック対象 DOM API, ブラウザAPI データベース、外部API
検証内容 レンダリング、ユーザー操作 HTTPレスポンス、データ処理

環境設定の例

// フロントエンドテスト(デフォルト)
// jest.config.js
module.exports = {
  testEnvironment: 'jest-environment-jsdom',
  // ...
};

// バックエンドテスト(APIルート)
/**
 * @jest-environment node
 */
// ファイルの先頭でnode環境を指定

Next.jsにおける3つのテスト対象

1. APIルートのテスト

// src/__tests__/api/levels.test.js
describe('/api/levels', () => {
  it('言語パラメータに基づいてレベルを返す', async () => {
    const mockLevels = [
      {
        id: 'beginner-ja',
        levelCode: 'beginner',
        lang: 'ja',
        displayName: '初級',
      },
    ];

    prisma.level.findMany.mockResolvedValue(mockLevels);
    prisma.content.count.mockResolvedValue(5);

    const request = { url: 'http://localhost:3000/api/levels?lang=ja' };
    const response = await GET(request);
    const data = await response.json();

    expect(prisma.level.findMany).toHaveBeenCalledWith({
      where: { lang: 'ja' },
      orderBy: { orderIndex: 'asc' },
    });
    expect(data[0]._count.contents).toBe(5);
  });
});

2. Reactコンポーネントのテスト

// src/__tests__/components/LevelManager.test.js
import { render, screen, fireEvent, waitFor } from '@testing-library/react';

describe('LevelManager Component', () => {
  it('新しいレベルを作成', async () => {
    render(<LevelManager />);

    const addButton = await screen.findByText('レベルを追加');
    fireEvent.click(addButton);

    const levelCodeInput = screen.getByPlaceholderText('beginner');
    fireEvent.change(levelCodeInput, { target: { value: 'expert' } });

    const createButton = screen.getByText('作成');
    fireEvent.click(createButton);

    await waitFor(() => {
      expect(fetch).toHaveBeenCalledWith('/api/levels', 
        expect.objectContaining({
          method: 'POST',
          body: expect.stringContaining('expert'),
        })
      );
    });
  });
});

3. カスタムフックのテスト

// src/__tests__/hooks/useLevels.test.js
import { renderHook, waitFor } from '@testing-library/react';
import { useLevels } from '@/hooks/useLevels';

describe('useLevels Hook', () => {
  it('APIエラー時にフォールバックレベルを使用', async () => {
    fetch.mockRejectedValueOnce(new Error('API Error'));

    const { result } = renderHook(() => useLevels('en'));

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.levels).toHaveLength(3);
    expect(result.current.levels[0].displayName).toBe('Pre-Intermediate');
  });
});

モック(Mock)の重要性と実装

モックとは

モックは、テスト実行時に実際の依存関係を置き換えるテストダブルです。

なぜモックが必要か

  1. 外部依存の分離 - データベースやAPIに依存しない
  2. テストの高速化 - 実際のネットワーク通信を避ける
  3. 予測可能な結果 - 常に同じ結果を返す
  4. エッジケースの再現 - エラー状態を意図的に作れる

実装例:next-intlのモック

// src/__mocks__/next-intl.js
export const useTranslations = () => {
  return (key) => {
    const translations = {
      'levels.title': 'レベル管理',
      'levels.levelCode': 'レベルコード',
      'validations.requiredLevelCode': 'レベルコードは必須です',
    };
    return translations[key] || key;
  };
};

export const useLocale = () => 'ja';

Prismaクライアントのモック

jest.mock('@/lib/prisma', () => ({
  __esModule: true,
  default: {
    level: {
      findMany: jest.fn(),
      findFirst: jest.fn(),
      create: jest.fn(),
    },
    content: {
      count: jest.fn(),
    },
    $transaction: jest.fn(),
  },
}));

実践例:多言語対応機能のテスト

ビジネス要件

  • 3言語対応(日本語、英語、中国語)
  • レベル管理機能(初級、中級、上級)
  • 共通ID(levelCode)による言語横断的な管理

統合テストの実装

describe('多言語レベル機能', () => {
  describe('APIエンドポイント', () => {
    it('言語パラメータの検証', () => {
      const validLangs = ['ja', 'en', 'zh'];
      const invalidLang = 'kr';
      
      expect(validLangs).toContain('ja');
      expect(validLangs).not.toContain(invalidLang);
    });

    it('levelCodeの形式検証', () => {
      const isValidCode = (code) => /^[a-z0-9-]+$/.test(code);
      
      expect(isValidCode('beginner')).toBe(true);
      expect(isValidCode('Level 1')).toBe(false);  // 大文字NG
      expect(isValidCode('初級')).toBe(false);     // 日本語NG
    });
  });

  describe('ビジネスロジック', () => {
    it('levelCodeによるコンテンツフィルタリング', () => {
      const contents = [
        { id: 1, levelCode: 'beginner' },
        { id: 2, levelCode: 'intermediate' },
        { id: 3, levelCode: 'beginner' },
      ];
      
      const beginnerContents = contents.filter(
        c => c.levelCode === 'beginner'
      );
      
      expect(beginnerContents).toHaveLength(2);
    });
  });
});

テスト実行とカバレッジ

package.jsonの設定

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

カバレッジレポートの見方

$ npm test -- --coverage

PASS  src/__tests__/multilingual-levels.test.js
 多言語レベル機能
   ✓ 言語パラメータの検証 (1 ms)
   ✓ levelCodeの形式検証 (1 ms)
   ✓ 表示名の文字数制限
   ... (13 tests passed)

Coverage Summary:
File                 | % Stmts | % Branch | % Funcs | % Lines |
---------------------|---------|----------|---------|---------|
All files            |   85.71 |    78.26 |   90.00 |   85.00 |
 api/levels/route.js |   82.35 |    75.00 |  100.00 |   82.35 |
 hooks/useLevels.js  |   90.00 |    83.33 |   85.71 |   90.00 |

ベストプラクティス

1. テストの3A原則

it('should validate level code format', () => {
  // Arrange(準備)
  const validCode = 'beginner';
  const invalidCode = 'Invalid!';
  
  // Act(実行)
  const isValid = /^[a-z0-9-]+$/.test(validCode);
  const isInvalid = /^[a-z0-9-]+$/.test(invalidCode);
  
  // Assert(検証)
  expect(isValid).toBe(true);
  expect(isInvalid).toBe(false);
});

2. テストの独立性

beforeEach(() => {
  // 各テスト前にモックをリセット
  jest.clearAllMocks();
});

afterEach(() => {
  // クリーンアップ処理
  cleanup();
});

3. 意味のあるテスト名

// ❌ 悪い例
it('test1', () => {});

// ✅ 良い例
it('無効な言語コードの場合、デフォルトで日本語を使用', () => {});

トラブルシューティング

よくあるエラーと対処法

1. ESモジュールエラー

// jest.config.js
module.exports = {
  transformIgnorePatterns: [
    'node_modules/(?!(next-intl|use-intl)/)',
  ],
};

2. Next.js特有の問題

// Next.jsの特殊インポートをモック
moduleNameMapper: {
  '^@/(.*)$': '<rootDir>/src/$1',
  '^next-intl$': '<rootDir>/src/__mocks__/next-intl.js',
}

3. 非同期処理のテスト

// waitForを使用して非同期処理を待つ
await waitFor(() => {
  expect(result.current.loading).toBe(false);
});

まとめ

Next.jsアプリケーションのテストは、フロントエンドとバックエンドの両方をカバーする必要があります。Jestとモックを適切に活用することで、外部依存を排除した高速で信頼性の高いテストを実現できます。

重要なポイント:

  1. Jestは設定が簡単で機能が豊富なテスティングフレームワーク
  2. モックにより外部依存を分離してテストを安定化
  3. 3つのテスト対象(API、Component、Hook)それぞれに適した手法を選択
  4. カバレッジを意識しつつ、意味のあるテストを書く

テストは開発速度を落とすものではなく、長期的に開発効率を向上させる投資です。適切なテスト戦略により、リファクタリングや機能追加を自信を持って行えるようになります。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?