Jest と Monorepo 構成:複数パッケージでの活用法

モダンな開発環境では、複数のパッケージを効率的に管理する Monorepo 構成が注目を集めています。特に大規模なプロジェクトにおいて、コードの再利用性や依存関係の管理を向上させる手法として活用されています。そして、この Monorepo 環境でのテスト戦略において、Jest は非常に重要な役割を果たします。
本記事では、Monorepo と Jest を組み合わせた効果的なテスト環境の構築方法について、基礎知識から実践的な実装例まで段階的に解説いたします。Lerna や Yarn Workspaces といった主要なツールでの具体的な設定方法や、チーム開発での運用ノウハウもご紹介しますので、ぜひ最後までお読みください。
Monorepo と Jest の基礎知識
Monorepo 構成の特徴
Monorepo(モノレポ)は、複数のプロジェクトやパッケージを単一のリポジトリで管理する開発手法です。従来の 1 つのリポジトリに 1 つのプロジェクトという考え方とは異なり、関連性の高い複数のプロジェクトをまとめて管理できるのが特徴です。
以下の図で、Monorepo の基本構造を確認しましょう。
mermaidflowchart TD
root[プロジェクトルート] --> packages[packages/]
packages --> ui[ui/]
packages --> api[api/]
packages --> shared[shared/]
ui --> ui_pkg[package.json]
ui --> ui_src[src/]
ui --> ui_test[__tests__/]
api --> api_pkg[package.json]
api --> api_src[src/]
api --> api_test[__tests__/]
shared --> shared_pkg[package.json]
shared --> shared_src[src/]
shared --> shared_test[__tests__/]
root --> root_pkg[package.json]
root --> config[設定ファイル群]
この構造により、コードの共有や依存関係の管理が効率的に行えます。また、各パッケージが独立してバージョン管理されながらも、統一された開発環境を維持できるのです。
Monorepo の主な利点
# | 利点 | 説明 |
---|---|---|
1 | コード共有の効率化 | 共通のライブラリやユーティリティを複数のパッケージで利用可能 |
2 | 依存関係の可視化 | パッケージ間の依存関係が明確になり、影響範囲の把握が容易 |
3 | 統一された開発環境 | リンター、フォーマッター、テストツールなどの設定を統一可能 |
4 | 効率的な CI/CD | 変更があったパッケージのみをビルド・デプロイする最適化が可能 |
Jest の役割と利点
Jest は Facebook(現 Meta)が開発した JavaScript テストフレームワークで、Monorepo 環境において強力な機能を提供します。特に複数パッケージの複雑なテストシナリオを効率的に管理できる点が評価されています。
Jest の主要機能
javascript// Jest の基本的なテスト例
describe('Monorepo での Jest テスト', () => {
test('基本的なユニットテスト', () => {
const result = add(2, 3);
expect(result).toBe(5);
});
test('非同期処理のテスト', async () => {
const data = await fetchUserData(1);
expect(data.name).toBeDefined();
});
});
このように、Jest はシンプルな記法で様々なテストパターンに対応できます。
Monorepo における Jest の利点
# | 利点 | 詳細 |
---|---|---|
1 | 統一されたテスト環境 | 全パッケージで同じテストフレームワークを使用可能 |
2 | 効率的なテスト実行 | 変更されたファイルのみをテスト対象とする機能 |
3 | カバレッジの統合管理 | 複数パッケージのカバレッジを統合して表示 |
4 | 柔軟な設定管理 | パッケージごとの個別設定と共通設定の両立 |
Monorepo での Jest 活用時の課題
Monorepo 環境で Jest を活用する際には、従来の単一プロジェクトでは発生しない特有の課題に直面します。これらの課題を理解することで、適切な解決策を選択できるようになります。
複数パッケージ間の依存関係
Monorepo 環境では、パッケージ間の依存関係が複雑になりがちです。特にテスト実行時において、この依存関係が正しく解決されないと予期しない動作が発生することがあります。
以下の図で、パッケージ間の依存関係とテスト時の問題を示します。
mermaidsequenceDiagram
participant UI as UI パッケージ
participant Shared as 共通ライブラリ
participant API as API パッケージ
participant Test as テスト実行
Test->>UI: テスト開始
UI->>Shared: 共通関数呼び出し
Shared-->>UI: エラー(未ビルド)
UI-->>Test: テスト失敗
Note over Test: 依存関係が正しく<br/>解決されない場合
依存関係での主な問題点
- ビルド順序の問題: 依存先パッケージがビルドされていない状態でのテスト実行
- パスの解決エラー: TypeScript の型定義や相対パスの解決に失敗
- 循環依存の検出: パッケージ間で循環依存が発生している場合の処理
テスト実行の複雑さ
Monorepo 環境では、どのパッケージのテストを実行するかの判断が複雑になります。全パッケージを毎回テストするのは非効率的ですが、必要なテストが実行されない可能性もあります。
テスト実行時の課題
bash# 全パッケージのテスト実行(非効率)
yarn test
# 特定パッケージのみのテスト(依存関係の考慮が必要)
yarn workspace @myorg/ui test
# 変更されたパッケージのみのテスト(複雑な判定ロジック)
yarn test --since origin/main
この例のように、効率的かつ確実なテスト実行には適切な戦略が必要です。
設定ファイルの管理問題
複数のパッケージそれぞれに設定ファイルを配置すると、メンテナンスコストが増大します。一方で、完全に統一された設定では、パッケージ固有の要件に対応できない場合があります。
設定管理の課題
# | 課題 | 影響 |
---|---|---|
1 | 設定の重複 | 同じ設定を複数箇所で管理する必要 |
2 | 設定の不整合 | パッケージごとに異なる設定による動作の違い |
3 | 更新の困難さ | 設定変更時に全パッケージの修正が必要 |
4 | デバッグの複雑化 | どの設定が適用されているかの把握が困難 |
これらの課題を解決するためには、Jest の設定継承機能や、プロジェクト機能を活用した戦略的なアプローチが重要になります。
Jest の Monorepo 対応解決策
前述の課題を解決するため、Jest には Monorepo 環境に特化した機能が提供されています。これらの機能を適切に活用することで、効率的で保守性の高いテスト環境を構築できます。
Workspace 設定の活用
Jest 28 以降では、プロジェクト機能(projects)を使用して複数のパッケージを効率的に管理できます。この機能により、各パッケージの設定を統合しつつ、個別の要件にも対応可能です。
プロジェクト設定の基本構造
javascript// jest.config.js (ルート)
module.exports = {
// プロジェクト一覧を定義
projects: [
'<rootDir>/packages/*/jest.config.js',
// または直接設定を記述
{
displayName: 'ui',
testMatch: [
'<rootDir>/packages/ui/**/*.test.{js,ts}',
],
setupFilesAfterEnv: ['<rootDir>/setup/ui-setup.js'],
},
],
// 全体の設定
collectCoverageFrom: [
'<rootDir>/packages/*/src/**/*.{js,ts}',
'!**/*.d.ts',
],
};
この設定により、各パッケージのテストを統合管理しながら、個別の設定も維持できます。
共通設定ファイルの管理
設定ファイルの重複を避けるため、共通設定を基底クラスとして定義し、各パッケージで継承する方式が効果的です。
共通設定の実装例
javascript// config/jest.base.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
// 共通のsetupファイル
setupFilesAfterEnv: [
'<rootDir>/../../config/jest.setup.js',
],
// 共通のモジュールマッピング
moduleNameMapping: {
'^@myorg/(.*)$': '<rootDir>/../$1/src',
},
// 共通のカバレッジ設定
collectCoverageFrom: [
'src/**/*.{js,ts}',
'!src/**/*.d.ts',
'!src/**/*.stories.{js,ts}',
],
};
javascript// packages/ui/jest.config.js
const base = require('../../config/jest.base');
module.exports = {
...base,
displayName: 'UI Components',
// パッケージ固有の設定
testEnvironment: 'jsdom', // UIコンポーネント用
setupFilesAfterEnv: [
...base.setupFilesAfterEnv,
'<rootDir>/src/test-utils/setup.js',
],
};
このアプローチにより、共通設定の保守性を向上させつつ、パッケージ固有の要件にも対応できます。
パッケージごとのテスト戦略
各パッケージの性質に応じて、最適なテスト戦略を選択することが重要です。以下の図で、パッケージタイプ別のテスト戦略を示します。
mermaidflowchart LR
packages[パッケージ分類] --> ui[UI Components]
packages --> lib[ライブラリ]
packages --> api[API Services]
packages --> utils[ユーティリティ]
ui --> ui_test[Jest + Testing Library<br/>Visual Testing<br/>Storybook連携]
lib --> lib_test[Unit Testing<br/>Integration Testing<br/>型チェック]
api --> api_test[API Testing<br/>E2E Testing<br/>モック戦略]
utils --> utils_test[Pure Function Testing<br/>パフォーマンステスト<br/>エッジケーステスト]
パッケージタイプ別テスト設定例
javascript// UI コンポーネント用の設定
const uiConfig = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@testing-library/jest-dom'],
moduleNameMapping: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
};
// API サービス用の設定
const apiConfig = {
testEnvironment: 'node',
setupFilesAfterEnv: ['./src/test-utils/api-setup.js'],
testTimeout: 10000, // API テスト用の長めのタイムアウト
};
これらの設定により、各パッケージの特性に最適化されたテスト環境を構築できます。
実装例:Lerna + Jest 構成
Lerna は、Monorepo プロジェクトでよく使用される管理ツールです。Jest と Lerna を組み合わせることで、効率的なテスト環境を構築できます。実際のプロジェクト構造から設定ファイルまで、段階的に実装していきましょう。
プロジェクト構造の作成
まずは、Lerna を使用した Monorepo プロジェクトの基本構造を作成します。
bash# Lernaプロジェクトの初期化
npx lerna init
# 必要なパッケージのインストール
yarn add -W -D jest @types/jest ts-jest
yarn add -W -D lerna @testing-library/jest-dom
以下のプロジェクト構造を作成します。
luamy-monorepo/
├── packages/
│ ├── ui/
│ │ ├── src/
│ │ ├── __tests__/
│ │ ├── package.json
│ │ └── jest.config.js
│ ├── api/
│ │ ├── src/
│ │ ├── __tests__/
│ │ ├── package.json
│ │ └── jest.config.js
│ └── shared/
│ ├── src/
│ ├── __tests__/
│ ├── package.json
│ └── jest.config.js
├── config/
│ ├── jest.base.js
│ └── jest.setup.js
├── jest.config.js
├── lerna.json
└── package.json
設定ファイルの実装
Lerna 設定ファイル
json{
"version": "independent",
"npmClient": "yarn",
"useWorkspaces": true,
"packages": ["packages/*"],
"command": {
"test": {
"stream": true
},
"run": {
"npmClient": "yarn"
}
}
}
ルートパッケージの設定
json{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:changed": "lerna run test --since HEAD~1"
},
"devDependencies": {
"jest": "^29.0.0",
"@types/jest": "^29.0.0",
"ts-jest": "^29.0.0",
"lerna": "^6.0.0"
}
}
Jest 基本設定ファイル
javascript// config/jest.base.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
// ルートディレクトリの設定
rootDir: process.cwd(),
// テストファイルのパターン
testMatch: [
'<rootDir>/src/**/*.test.{js,ts}',
'<rootDir>/__tests__/**/*.{js,ts}',
],
// セットアップファイル
setupFilesAfterEnv: [
'<rootDir>/../../config/jest.setup.js',
],
// モジュール解決の設定
moduleNameMapping: {
'^@myorg/(.*)$': '<rootDir>/../$1/src',
'^~/(.*)$': '<rootDir>/src/$1',
},
// カバレッジ設定
collectCoverageFrom: [
'src/**/*.{js,ts}',
'!src/**/*.d.ts',
'!src/**/*.stories.{js,ts}',
'!src/**/index.{js,ts}',
],
// テストタイムアウト
testTimeout: 5000,
};
テストスクリプトの作成
効率的なテスト実行スクリプト
javascript// scripts/test-runner.js
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
async function runTests() {
try {
// 変更されたパッケージを取得
const { stdout } = await execAsync(
'lerna changed --json'
);
const changedPackages = JSON.parse(stdout);
if (changedPackages.length === 0) {
console.log('変更されたパッケージがありません');
return;
}
// 変更されたパッケージのテストを実行
for (const pkg of changedPackages) {
console.log(`Testing ${pkg.name}...`);
await execAsync(`lerna run test --scope=${pkg.name}`);
}
console.log('すべてのテストが完了しました');
} catch (error) {
console.error(
'テスト実行中にエラーが発生しました:',
error
);
process.exit(1);
}
}
runTests();
パッケージ別設定の実装
javascript// packages/ui/jest.config.js
const base = require('../../config/jest.base');
module.exports = {
...base,
displayName: 'UI Components',
// UI コンポーネント用の設定
testEnvironment: 'jsdom',
setupFilesAfterEnv: [
...base.setupFilesAfterEnv,
'@testing-library/jest-dom',
],
// CSS モジュールのモック
moduleNameMapping: {
...base.moduleNameMapping,
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
'\\.(jpg|jpeg|png|gif|svg)$':
'<rootDir>/src/test-utils/file-mock.js',
},
};
javascript// packages/api/jest.config.js
const base = require('../../config/jest.base');
module.exports = {
...base,
displayName: 'API Services',
// API テスト用の設定
testEnvironment: 'node',
testTimeout: 10000,
// APIテスト用のセットアップ
setupFilesAfterEnv: [
...base.setupFilesAfterEnv,
'<rootDir>/src/test-utils/api-setup.js',
],
};
この構成により、Lerna を使用した Monorepo 環境で効率的な Jest テストが実行できるようになります。
実装例:Yarn Workspaces + Jest 構成
Yarn Workspaces は、Yarn が提供する Monorepo 管理機能で、依存関係の解決やパッケージ管理を効率的に行えます。Jest と Yarn Workspaces を組み合わせることで、よりシンプルで高性能なテスト環境を構築できます。
Workspace 設定
ルートパッケージの設定
json{
"name": "my-yarn-monorepo",
"private": true,
"workspaces": {
"packages": ["packages/*", "apps/*"],
"nohoist": ["**/react", "**/react-dom"]
},
"scripts": {
"test": "jest --projects packages/*/jest.config.js",
"test:watch": "jest --watch --projects packages/*/jest.config.js",
"test:coverage": "jest --coverage --projects packages/*/jest.config.js",
"test:workspace": "yarn workspace",
"test:changed": "jest --changedSince=main"
},
"devDependencies": {
"jest": "^29.0.0",
"@types/jest": "^29.0.0",
"ts-jest": "^29.0.0"
}
}
Workspace 内パッケージの設定
json{
"name": "@myorg/ui",
"version": "1.0.0",
"main": "dist/index.js",
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"build": "tsc",
"dev": "tsc --watch"
},
"dependencies": {
"@myorg/shared": "*"
},
"devDependencies": {
"@testing-library/react": "^13.0.0",
"@testing-library/jest-dom": "^5.0.0"
}
}
以下の図で、Yarn Workspaces での依存関係とテスト実行フローを示します。
mermaidflowchart TD
root[ルートワークスペース] --> yarn_test[yarn test]
yarn_test --> jest_projects[Jest Projects機能]
jest_projects --> ui_pkg[UI パッケージ]
jest_projects --> api_pkg[API パッケージ]
jest_projects --> shared_pkg[共通パッケージ]
ui_pkg --> ui_deps[依存関係解決]
api_pkg --> api_deps[依存関係解決]
shared_pkg --> shared_deps[依存関係解決]
ui_deps --> shared_lib[共通ライブラリ]
api_deps --> shared_lib
shared_deps --> external[外部ライブラリ]
ui_pkg --> ui_test[UIテスト実行]
api_pkg --> api_test[APIテスト実行]
shared_pkg --> shared_test[共通テスト実行]
Jest 設定の最適化
マルチプロジェクト設定
javascript// jest.config.js (ルート)
module.exports = {
projects: [
{
displayName: 'UI Components',
testMatch: [
'<rootDir>/packages/ui/**/*.test.{js,ts,tsx}',
],
testEnvironment: 'jsdom',
setupFilesAfterEnv: [
'<rootDir>/config/jest.setup.js',
'@testing-library/jest-dom',
],
moduleNameMapping: {
'^@myorg/(.*)$': '<rootDir>/packages/$1/src',
'\\.(css|less|scss)$': 'identity-obj-proxy',
},
},
{
displayName: 'API Services',
testMatch: [
'<rootDir>/packages/api/**/*.test.{js,ts}',
],
testEnvironment: 'node',
setupFilesAfterEnv: [
'<rootDir>/config/jest.setup.js',
],
moduleNameMapping: {
'^@myorg/(.*)$': '<rootDir>/packages/$1/src',
},
},
{
displayName: 'Shared Utils',
testMatch: [
'<rootDir>/packages/shared/**/*.test.{js,ts}',
],
testEnvironment: 'node',
setupFilesAfterEnv: [
'<rootDir>/config/jest.setup.js',
],
moduleNameMapping: {
'^@myorg/(.*)$': '<rootDir>/packages/$1/src',
},
},
],
// 全体のカバレッジ設定
collectCoverage: true,
coverageDirectory: '<rootDir>/coverage',
coverageReporters: ['text', 'lcov', 'html'],
collectCoverageFrom: [
'<rootDir>/packages/*/src/**/*.{js,ts,tsx}',
'!**/*.d.ts',
'!**/*.stories.{js,ts,tsx}',
'!**/node_modules/**',
],
};
パフォーマンス最適化設定
javascript// config/jest.performance.js
module.exports = {
// 並列実行の最適化
maxWorkers: '50%',
// キャッシュの活用
cacheDirectory: '<rootDir>/node_modules/.cache/jest',
// 不要なファイルの除外
testPathIgnorePatterns: [
'/node_modules/',
'/dist/',
'/build/',
'/coverage/',
],
// モジュール解決の高速化
modulePathIgnorePatterns: [
'<rootDir>/packages/.*/dist/',
'<rootDir>/packages/.*/build/',
],
// ウォッチモードの最適化
watchPathIgnorePatterns: [
'/node_modules/',
'/dist/',
'/build/',
],
};
CI/CD での実行方法
GitHub Actions 設定例
yaml# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16.x, 18.x]
steps:
- uses: actions/checkout@v3
with:
# 変更されたファイルの履歴を取得
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run tests
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
# PRの場合は変更されたファイルのみテスト
yarn test --changedSince=origin/${{ github.base_ref }}
else
# mainブランチの場合は全テスト実行
yarn test --coverage
fi
- name: Upload coverage reports
uses: codecov/codecov-action@v3
if: matrix.node-version == '18.x'
with:
directory: ./coverage
効率的なテスト実行スクリプト
javascript// scripts/run-tests.js
const { execSync } = require('child_process');
const { existsSync } = require('fs');
function runTests() {
const isCI = process.env.CI === 'true';
const isPR =
process.env.GITHUB_EVENT_NAME === 'pull_request';
let command = 'yarn test';
if (isCI) {
if (isPR) {
// PRの場合は変更されたファイルのみ
command +=
' --changedSince=origin/main --passWithNoTests';
} else {
// メインブランチの場合は全テスト + カバレッジ
command += ' --coverage --watchAll=false';
}
} else {
// ローカル開発環境
command += ' --watch';
}
console.log(`実行コマンド: ${command}`);
try {
execSync(command, { stdio: 'inherit' });
} catch (error) {
console.error('テスト実行中にエラーが発生しました');
process.exit(1);
}
}
runTests();
並列実行での最適化
bash# 複数のワークスペースを並列でテスト
yarn workspaces foreach --parallel run test
# 特定のワークスペースのみをテスト
yarn workspace @myorg/ui test
# 依存関係を考慮した順次実行
yarn workspaces foreach --topological run test
この設定により、Yarn Workspaces の機能を最大限活用した効率的な Jest テスト環境が構築できます。特に大規模な Monorepo プロジェクトにおいて、その効果を実感できるでしょう。
まとめ
本記事では、Monorepo と Jest を組み合わせた効果的なテスト環境の構築について、基礎知識から実践的な実装方法まで詳しく解説いたしました。
Monorepo と Jest の組み合わせは、複数のパッケージを効率的に管理しながら、統一されたテスト戦略を実現する強力な手法です。特に以下の点で大きなメリットがあります。
重要なポイント
# | ポイント | 効果 |
---|---|---|
1 | 統一されたテスト環境 | 全パッケージで一貫したテスト品質を維持 |
2 | 効率的な依存関係管理 | パッケージ間の依存関係を適切に解決 |
3 | 柔軟な設定管理 | 共通設定と個別設定の適切なバランス |
4 | 最適化された CI/CD | 変更されたパッケージのみの効率的なテスト実行 |
実装においては、Lerna と Yarn Workspaces それぞれに特徴があります。プロジェクトの規模やチームの体制に応じて適切なツールを選択することが重要です。
設定ファイルの管理では、共通設定を基底として各パッケージで継承する方式が効果的でした。この方式により、保守性を保ちながら各パッケージの特性に応じた最適化が可能になります。
CI/CD 環境での運用では、変更されたファイルのみをテスト対象とする機能や、並列実行による高速化など、実用的な最適化手法をご紹介いたしました。
Monorepo と Jest の組み合わせは、モダンな Web アプリケーション開発において欠かせない技術スタックとなっています。本記事でご紹介した手法を参考に、ぜひ皆さまのプロジェクトでも効率的なテスト環境を構築してみてください。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来