はじめに
以下の記事では、npm のワークスペース機能を使って Express と React を TypeScript で開発するためのモノレポ環境を構築しました。
今回の記事では、このプロジェクトに、テスト環境を導入します。
モノレポでのテスト導入には、いくつかのメリットがあります。
-
単一のコマンドでテスト実行: ルートディレクトリから
npm run testを実行するだけで、すべてのパッケージのテストをまとめて実行できる -
依存関係を考慮したテスト: 共通の
sharedパッケージに変更があった場合、それに依存するbackendやfrontendのテストだけを再実行するといった効率的なテストが可能 - 一貫した設定: ルートでテストランナーの設定を統一することで、各パッケージでの設定漏れやばらつきを防ぐことができる
今回は、以下のテストツールを導入します。
- Vitest
- Vite ベースの高速なユニットテストフレームワーク
- バックエンド(Express)と共通パッケージ(
shared)のユニットテストに使用
- Playwright
- エンドツーエンド(E2E)テストフレームワーク
- フロントエンドとバックエンドを統合したテストに使用
1. 依存関係の追加
まず、テストに必要なパッケージをモノレポのルートにインストールします。これにより、すべてのワークスペースで同じバージョンを使用できます。
cd monorepo-npm
npm install -D vitest @vitest/coverage-v8 @playwright/test
2. Vitestの設定(ユニットテスト)
backend と shared のパッケージで Vitest を使えるように設定します。
ルートの tsconfig.json の更新
Vitest 用の型定義を追加するために、ルートの tsconfig.json を更新します。
{
"compilerOptions": {
// ...既存の設定
},
"exclude": ["node_modules", "dist"],
"ts-node": {
"esm": true,
"experimentalSpecifierResolution": "node"
}
}
"ts-node" の設定について
"ts-node" の設定は、npm run test で TypeScript のテストファイルを直接実行するために必要です。
ts-node は、TypeScript を実行時にコンパイルして Node.js で直接実行するためのツールです。しかし、モノレポ環境では、packages ディレクトリ内の各ワークスペースが独自の tsconfig.json を持っています。npm run test コマンドはルートから実行されるため、ts-node がルートの package.json に設定されている test スクリプトを解釈する際、ルートの tsconfig.json を参照します。
"ts-node" のプロパティの設定値について
ここで設定している各プロパティは、以下の役割を果たしています。
-
"esm": true- ES Modules (ESM) 構文を有効にするための設定
- TypeScript の
import ... from '...'構文は、デフォルトでは CommonJS (require) に変換されるが、この設定により ESM のまま実行できるようになる - 本プロジェクトでは
type: "module"が設定されているため、この設定は不可欠
-
"experimentalSpecifierResolution": "node"- ファイル拡張子の解決に関する設定
- ESM 環境では、
import './types.js'のようにファイル拡張子を明示的に指定する必要がある - この設定により、
import './types'のように拡張子を省略した形式でも、Node.js が自動的に.jsや.tsを補完して解決できるようになる
この設定をルートに置くことで、すべてのワークスペースのテストスクリプトが統一された方法で実行され、import 関連のエラーを防ぐことができます。
shared パッケージのテスト設定
shared/package.json にテストスクリプトを追加します。
{
"name": "@monorepo-npm/shared",
// ...
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"dev": "tsc --watch",
"test": "vitest run"
},
"devDependencies": {
// Vitest をルートでインストールしているので Vitest は不要です
}
}
shared/src/types.test.ts を作成し、型定義のテスト例を記述します。
import { describe, it, expect } from "vitest";
import type { User, ApiResponse } from "./types.js";
describe("Shared Types", () => {
it("User type should be defined correctly", () => {
const user: User = {
id: "123",
name: "Test User",
email: "test@example.com",
createdAt: new Date(),
};
expect(user.name).toBe("Test User");
});
it("ApiResponse type should handle success case", () => {
const response: ApiResponse<string> = {
success: true,
data: "Success!",
};
expect(response.success).toBe(true);
expect(response.data).toBe("Success!");
});
});
npm run test を実行すると、テストが成功します。
> monorepo-npm@1.0.0 test
> npm run test --workspaces --if-present
> @monorepo-npm/shared@1.0.0 test
> vitest run
RUN v3.2.4 C:/Users/ymori/Documents/GitHub/monorepo-npm/shared
✓ src/types.test.ts (2 tests) 6ms
✓ Shared Types > User type should be defined correctly 3ms
✓ Shared Types > ApiResponse type should handle success case 1ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 11:42:59
Duration 1.60s (transform 92ms, setup 0ms, collect 92ms, tests 6ms, environment 0ms, prepare 901ms)
backend パッケージのテスト設定
backend/package.json にテストスクリプトを追加します。
{
"name": "@monorepo-npm/backend",
// ...
"scripts": {
"build": "tsc",
"dev": "tsx watch src/index.ts",
"start": "node dist/index.js",
"clean": "rm -rf dist",
"test": "vitest run"
},
"devDependencies": {
// ...
}
}
backend/src/index.test.ts を作成し、Express API の単体テスト例を記述します。
import { describe, it, expect } from "vitest";
import request from "supertest";
import express from "express";
import type { User, ApiResponse } from "@monorepo-npm/shared";
/** モックのExpressアプリケーション */
const app = express();
app.use(express.json());
const sampleUsers: User[] = [
{
id: "1",
name: "田中太郎",
email: "tanaka@example.com",
createdAt: new Date("2023-01-01"),
},
];
app.get("/api/users", (req, res) => {
res.json({
success: true,
data: sampleUsers,
});
});
describe("Backend API", () => {
it("GET /api/users should return a list of users", async () => {
const res = await request(app).get("/api/users");
const responseBody: ApiResponse<User[]> = res.body;
expect(res.status).toBe(200);
expect(responseBody.success).toBe(true);
if (responseBody.data) {
expect(responseBody.data).toHaveLength(1);
expect(responseBody.data[0].name).toBe("田中太郎");
}
});
});
npm run test を実行すると、テストが成功します。
今回は、backend のテストも実行されます。
> monorepo-npm@1.0.0 test
> npm run test --workspaces --if-present
> @monorepo-npm/backend@1.0.0 test
> vitest run
RUN v3.2.4 C:/Users/ymori/Documents/GitHub/monorepo-npm/packages/backend
✓ src/index.test.ts (1 test) 27ms
✓ Backend API > GET /api/users should return a list of users 25ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 11:53:59
Duration 825ms (transform 82ms, setup 0ms, collect 246ms, tests 27ms, environment 0ms, prepare 204ms)
> @monorepo-npm/shared@1.0.0 test
> vitest run
RUN v3.2.4 C:/Users/ymori/Documents/GitHub/monorepo-npm/shared
✓ src/types.test.ts (2 tests) 6ms
✓ Shared Types > User type should be defined correctly 3ms
✓ Shared Types > ApiResponse type should handle success case 1ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 11:54:01
Duration 666ms (transform 87ms, setup 0ms, collect 76ms, tests 6ms, environment 0ms, prepare 199ms)
3. Playwrightの設定(E2Eテスト)
モノレポ全体をテストするために、Playwright の設定はルートディレクトリに配置します。
Playwright の設定ファイル作成
playwright.config.ts をルートに作成します。
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './packages',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
trace: 'on-first-retry',
},
webServer: {
command: 'npm run dev',
url: 'http://127.0.0.1:3000',
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
testDir: './packages' とすることで、Playwright は packages ディレクトリ以下にあるテストファイルを自動的に見つけて実行します。webServer プロパティの設定により、テスト実行前に npm run dev コマンドが自動的に実行され、フロントエンドとバックエンドのサーバーが起動します。
E2Eテストファイルの作成
frontend パッケージ内に src/e2e.spec.ts を作成します。
import { test, expect } from '@playwright/test';
test('has a user list heading', async ({ page }) => {
await page.goto('http://localhost:3000');
const heading = page.getByRole('heading', { name: 'ユーザー一覧' });
await expect(heading).toBeVisible();
});
test('shows user list from backend', async ({ page }) => {
await page.goto('http://localhost:3000');
const userList = page.getByRole('list');
await expect(userList).toBeVisible();
// バックエンドから取得したユーザーが表示されていることを確認
await expect(page.getByText('田中太郎 - tanaka@example.com')).toBeVisible();
});
このテストでは、http://localhost:3000 にアクセスし、ユーザー一覧のタイトルが表示されること、そしてバックエンドから取得したユーザーデータが表示されることを確認します。
実行と動作確認
すべての設定が完了したら、ルートディレクトリからテストを実行してみましょう。
ルートの package.json にスクリプトを追加
Vitest と Playwright のテストをまとめて実行するスクリプトを追加します。
// monorepo-npm/package.json
{
"scripts": {
// ...既存のスクリプト
"test": "npm run test --workspaces --if-present",
"test:e2e": "playwright test",
"test:all": "npm run test && npm run test:e2e"
}
}
これで、以下のコマンドでテストを実行できます。
-
npm run test: すべてのワークスペース(sharedとbackend)のユニットテストを実行 -
npm run test:e2e: Playwright を使った E2E テストを実行 -
npm run test:all: ユニットテストと E2E テストをまとめて実行
まとめ
この記事では、npm を使ったモノレポ環境に、ユニットテスト(Vitest)と E2E テスト(Playwright)を導入する方法を記載しました。これにより、各パッケージのロジックからアプリケーション全体の動作まで、開発のあらゆるフェーズで品質を確保できます。