メインコンテンツまでスキップ

開発ツールチェーン

この章では、TypeScriptプロジェクトで使用する開発ツールについて学びます。

この章で学ぶこと

  • ESLintのセットアップと設定
  • Prettierとの統合(Biome という統合代替も紹介)
  • TypeScriptの実行方法(tsx / Node.js の type stripping)
  • VitestとJestでのテスト
  • よく使うマッチャーと非同期テスト
備考

開発ツールを適切に設定することで、コードの品質を維持し、バグを早期に発見できます。チーム開発では特に重要です。

ESLintとは

ESLintはJavaScript/TypeScriptのための静的解析ツールです。コードの問題を検出し、一貫したコーディングスタイルを維持するのに役立ちます。

ESLintを使う理由:

  • 潜在的なバグを早期発見
  • コーディング規約の自動チェック
  • チーム全体で一貫したコードスタイル
  • 自動修正機能でリファクタリングを効率化

ESLintのセットアップ

# ESLintとTypeScript関連パッケージをインストール
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

# ESLintの設定ファイルを作成(対話形式)
npx eslint --init

eslint.config.js(Flat Config形式):

// eslint.config.js
// ESLintの推奨設定をインポート
import eslint from '@eslint/js';
// TypeScript用ESLint設定をインポート
import tseslint from 'typescript-eslint';

// 設定をエクスポート
export default tseslint.config(
// ESLintの推奨ルールを適用
eslint.configs.recommended,
// TypeScriptの推奨ルールを適用
...tseslint.configs.recommended,
{
// 対象ファイル(.tsと.tsxファイル)
files: ['**/*.ts', '**/*.tsx'],
languageOptions: {
// TypeScriptパーサーを使用
parser: tseslint.parser,
parserOptions: {
// tsconfig.jsonを参照(型情報を使うルール用)
project: './tsconfig.json',
},
},
rules: {
// 未使用の変数を警告
'@typescript-eslint/no-unused-vars': 'warn',

// anyの使用を警告
'@typescript-eslint/no-explicit-any': 'warn',

// 明示的な戻り値の型を要求(offで無効化)
'@typescript-eslint/explicit-function-return-type': 'off',

// セミコロンを必須に(errorレベル)
'semi': ['error', 'always'],

// シングルクォートを使用
'quotes': ['error', 'single'],
},
},
{
// 除外するファイル/フォルダ
ignores: ['dist/', 'node_modules/', '*.js'],
}
);

ルールの設定値:

意味
'error'ルール違反はエラー(ビルド失敗)
'warn'ルール違反は警告(ビルドは通る)
'off'ルールを無効化

よく使うESLintルール

// 推奨設定の例
rules: {
// ===== TypeScript関連 =====

// 未使用の変数をエラーに
// argsIgnorePattern: _で始まる引数は無視
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_', // _で始まる引数は無視
varsIgnorePattern: '^_' // _で始まる変数は無視
}],

// any型の使用を禁止
'@typescript-eslint/no-explicit-any': 'error',

// ?? (nullish coalescing)を推奨
'@typescript-eslint/prefer-nullish-coalescing': 'error',

// ?. (optional chaining)を推奨
'@typescript-eslint/prefer-optional-chain': 'error',

// 未処理のPromiseを禁止
'@typescript-eslint/no-floating-promises': 'error',

// ===== 一般的なルール =====

// console.logは警告(本番コードでは削除すべき)
'no-console': 'warn',

// === を必須に(== は禁止)
'eqeqeq': 'error',

// varの使用禁止(let/constを使う)
'no-var': 'error',

// 再代入しない変数はconstに
'prefer-const': 'error',
}

Prettierとは

Prettierはコードフォーマッターです。コードを自動的に整形して、一貫したスタイルを維持します。

ESLintとPrettierの違い:

ツール役割
ESLintコードの品質チェック未使用変数の検出、潜在的バグの発見
Prettierコードの整形インデント、改行、クォートの統一

Prettierのセットアップ

# PrettierとESLint連携パッケージをインストール
npm install --save-dev prettier eslint-config-prettier

.prettierrc(Prettierの設定ファイル):

{
"semi": true, // 文末にセミコロンを付ける
"singleQuote": true, // シングルクォートを使用
"tabWidth": 2, // タブ幅を2スペースに
"useTabs": false, // タブではなくスペースを使用
"trailingComma": "es5", // 末尾のカンマを付ける(ES5互換)
"printWidth": 100, // 1行の最大文字数
"bracketSpacing": true, // オブジェクトの{}内にスペース
"arrowParens": "avoid" // 引数が1つのアロー関数で括弧を省略
}

.prettierignore(整形対象から除外):

dist
node_modules
coverage
*.min.js

ESLintとPrettierの統合

ESLintとPrettierを一緒に使う場合、設定の競合を避ける必要があります。

// eslint.config.js に追加
// eslint-config-prettierをインポート(Prettierと競合するルールを無効化)
import eslintConfigPrettier from 'eslint-config-prettier';

export default [
// ... 他の設定
eslintConfigPrettier, // 必ず最後に追加(Prettierと競合するルールを無効化)
];

package.jsonのスクリプト:

{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,json}\"",
"check": "prettier --check \"src/**/*.{ts,tsx,json}\""
}
}
備考

Flat Config では --ext オプションは不要

eslint.config.jsfiles: ['**/*.ts', '**/*.tsx'] で対象ファイルを指定するため、コマンド側では --ext を付けずに eslint . とするのが 2026 年のベストプラクティスです。なお --ext は flat config 移行時にいったん使えなくなった後、ESLint 9.21 で『追加の拡張子を指定する』オプションとして再導入されています。ただし files で指定していれば通常は不要です。

# リントチェック実行(問題を検出)
npm run lint

# 自動修正(修正可能な問題を自動で直す)
npm run lint:fix

# フォーマット実行(コードを整形)
npm run format

Biome(ESLint + Prettier の統合代替)

Biome は Rust で書かれた高速な Linter + Formatter です。2025 年に v2 が GA され、ESLint + Prettier + (Import sorter) を 1 つのツールで置き換えられる選択肢として広く採用されています。

Biome の特徴:

  • 1 ツールで完結: Linter と Formatter が統合されているため設定ファイルが 1 つで済む
  • 高速: Rust 製で、ESLint + Prettier 構成と比べて体感で 10 倍以上の速度
  • ゼロコンフィグ: biome init ですぐに使える。TypeScript / JSX / JSON / CSS に対応
  • 公式 LSP / VSCode 拡張: エディタ統合がシンプル

インストール

# devDependencies として追加
npm install -D --save-exact @biomejs/biome

# 初期設定ファイル(biome.json)を生成
npx biome init

biome.json の基本設定

{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"files": {
"includes": ["**/*.{ts,tsx,js,jsx,json}"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
}
}

package.json のスクリプト例

{
"scripts": {
"lint": "biome lint .",
"format": "biome format --write .",
"check": "biome check --write ."
}
}

biome check は Lint + Format を同時に実行します。

ESLint + Prettier と Biome の使い分け

観点ESLint + PrettierBiome
速度遅め(Node.js 実行)速い(Rust 実行)
ルール数豊富(多数のプラグイン)組み込みルールのみ(ただし主要ルールはカバー)
プラグインエコシステム成熟(React / Vue / Vitest 等)v2 から GritQL ベースの Linter プラグインが利用可能だがまだ限定的
設定の複雑さ高い(eslint + prettier + 関連設定)低い(biome.json 1 ファイル)
向いているケース細かいルールのカスタマイズが必要、既存プロジェクト新規プロジェクト、CI を高速化したい、設定を簡素にしたい
備考

2026年時点では、新規プロジェクトは Biome、既存の大規模プロジェクトは ESLint + Prettier という使い分けが一般的です。React 周辺のようにエコシステムが豊富な領域では ESLint のプラグイン(eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-react-compiler 等)に依存する場合が多くあります。そのため、ESLint も引き続き有力な選択肢です。

TypeScript の実行方法(ビルドツール)

TypeScript コードを実行する方法は 2026 年時点で多様化しています。用途別に使い分けるのが一般的です。

手段特徴向いているケース
tsc公式コンパイラ。JS ファイルを出力本番ビルド、型チェック専用
tsxesbuild ベースの高速 TS ランナー。tsx watch で HMR 相当開発時の直接実行、ts-node の後継
Node.js 22.18+ / 24(フラグ不要)型を剥がして直接実行(type stripping)。外部依存なし軽量 CLI、学習、スクリプト
Bun全部入りランタイム(TS 直接実行・バンドル・テスト・パッケージ管理)新規プロジェクトの高速化
Denoセキュア・TS ネイティブDeno エコシステムのプロジェクト
esbuild / swc高速ビルドツール。ライブラリの内部に組み込まれることが多い大規模プロジェクトのバンドル

Node.js の型剥がし(Node.js 22+)

Node.js 22 からは、公式に .ts ファイルを型を剥がして実行できる実験的機能が搭載されました。追加ツールなしで TypeScript が動くため、学習環境として最適です。

# 型を剥がして実行(型チェックはしない)
node --experimental-strip-types script.ts

# 型に加えて enum や decorator も変換(より広い互換性)
node --experimental-transform-types script.ts
備考

--experimental-strip-types型注釈だけを消せば JavaScript として動くコードerasableSyntaxOnly 準拠のコード)にのみ対応します。enum や constructor parameter properties など、実行時コードを生成する TS 固有構文は使えません。17 章の erasableSyntaxOnly と組み合わせて制約を揃えると安全です。

Node.js 24 以降ではこの機能が安定化される予定です。2026年4月時点では --experimental- プレフィックスが必要です。

tsx による開発時実行

tsx は esbuild ベースで TypeScript を高速に実行するランナーです。ts-node / ts-node-dev の後継として広く採用されています。

# インストール
npm install -D tsx

# 直接実行
npx tsx src/index.ts

# ファイル変更を監視して自動再実行(ts-node-dev --respawn 相当)
npx tsx watch src/index.ts

ESM・CJS の両方に対応し、設定ファイルが不要な点が特徴です。

テストフレームワークの選択(2026年)

TypeScript プロジェクトで使える主要なテストフレームワークは以下の通りです。

フレームワーク特徴向いているケース
VitestVite の設計を踏襲した高速テストランナー。TS を標準サポート新規プロジェクト、Vite ベースのフロントエンド
Jest老舗で実績豊富。プラグインやガイドが充実既存の Jest プロジェクト、React Native
Node.js 組み込みテスト(node:test追加依存なし、Node.js 22+ で安定軽量な CLI、Node.js ネイティブ構成
Bun TestBun 内蔵の高速テストBun を採用しているプロジェクト
備考

2026年の新規プロジェクトは Vitest が第一候補です。設定が簡潔で、ESM / TS / JSX をそのまま扱える点で Jest より優位です。ただし Jest にしかないプラグインや React Native のサポートが必要な場合は Jest を選びます。

Vitest(推奨)

Vitest は Vite と同じチームが開発しているテストフレームワークで、Vite の構成をそのまま共有できます。TypeScript / JSX / ESM を設定なしで扱え、Jest 互換の API(describe / it / expect)を持つため、学習コストが低いのが特徴です。

Vitest のインストール

npm install -D vitest @vitest/coverage-v8

package.json のスクリプト

{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}

設定ファイル(任意)

vitest.config.ts を作成して細かい挙動を調整できます。設定がなくても動きます。

import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
environment: 'node', // 'jsdom' / 'happy-dom' も指定可能
globals: false, // describe / it をグローバルにしたい場合は true
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
},
},
})

テストコードの例

// src/calculator.test.ts
import { describe, it, expect } from 'vitest'
import { Calculator } from './calculator'

describe('Calculator', () => {
it('add should sum two numbers', () => {
const calc = new Calculator()
expect(calc.add(2, 3)).toBe(5)
})
})

実行:

npm test # watch モードで起動
npm run test:run # 1 回だけ実行(CI 向け)
npm run test:coverage # カバレッジレポートを生成

Jestとは

JestはFacebook(現Meta)が開発したJavaScript/TypeScript用のテストフレームワークです。従来から実績があり、既存のドキュメントやプラグインが豊富です。

備考

新規プロジェクトでは Vitest を推奨しますが、React Native や Jest 依存のツールチェーンがある場合は引き続き Jest を使います。以下の解説は Jest を学ぶ場合の参考としてお読みください。

Jestの特徴:

  • 設定不要でほぼすぐに使える(Zero config)
  • 高速なテスト実行(並列実行)
  • スナップショットテスト
  • カバレッジレポート内蔵
  • モック機能が充実

Jestのセットアップ

# JestとTypeScript関連パッケージをインストール
npm install --save-dev jest ts-jest @types/jest

# Jest設定ファイルを生成
npx ts-jest config:init

jest.config.js:

const { createDefaultPreset } = require('ts-jest');

/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
// TypeScript 変換は ts-jest の transform を使用
// (preset: 'ts-jest' の文字列形式は非推奨)
transform: {
...createDefaultPreset().transform,
},

// テスト環境(Node.js)
testEnvironment: 'node',

// テストファイルのパターン
testMatch: [
'**/__tests__/**/*.ts', // __tests__フォルダ内の.tsファイル
'**/*.test.ts', // *.test.ts
'**/*.spec.ts' // *.spec.ts
],

// カバレッジ収集対象
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts', // 型定義ファイルは除外
'!src/index.ts' // エントリーポイントは除外
],

// モジュール解決のためのパスマッピング
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
};

テストの基本

まず、テスト対象のコードを作成します。

// src/calculator.ts
export class Calculator {
// 足し算
add(a: number, b: number): number {
return a + b;
}

// 引き算
subtract(a: number, b: number): number {
return a - b;
}

// 掛け算
multiply(a: number, b: number): number {
return a * b;
}

// 割り算(0での除算はエラー)
divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
}

テストファイルを作成します。

// src/calculator.test.ts
import { Calculator } from './calculator';

// describe: テストをグループ化(ネスト可能)
describe('Calculator', () => {
// テスト対象のインスタンス
let calculator: Calculator;

// beforeEach: 各テスト前に実行されるセットアップ
beforeEach(() => {
// 各テストで新しいインスタンスを使用
calculator = new Calculator();
});

// describe: addメソッドのテストグループ
describe('add', () => {
// it: 個別のテストケース
it('should add two positive numbers', () => {
// expect: アサーション(期待値の検証)
// toBe: 厳密等価(===)
expect(calculator.add(2, 3)).toBe(5);
});

it('should add negative numbers', () => {
expect(calculator.add(-1, -2)).toBe(-3);
});

it('should add zero', () => {
expect(calculator.add(5, 0)).toBe(5);
});
});

describe('subtract', () => {
it('should subtract two numbers', () => {
expect(calculator.subtract(5, 3)).toBe(2);
});

it('should handle negative result', () => {
expect(calculator.subtract(3, 5)).toBe(-2);
});
});

describe('multiply', () => {
it('should multiply two numbers', () => {
expect(calculator.multiply(3, 4)).toBe(12);
});

it('should return zero when multiplied by zero', () => {
expect(calculator.multiply(5, 0)).toBe(0);
});
});

describe('divide', () => {
it('should divide two numbers', () => {
expect(calculator.divide(10, 2)).toBe(5);
});

// 例外をテストする場合
it('should throw error when dividing by zero', () => {
// toThrow: 例外がスローされることを検証
expect(() => calculator.divide(10, 0)).toThrow('Division by zero');
});

it('should handle decimal results', () => {
expect(calculator.divide(7, 2)).toBe(3.5);
});
});
});

テストの構造:

  • describe: テストをグループ化(ネスト可能)
  • it / test: 個別のテストケースを定義
  • expect: アサーション(期待値の検証)
  • beforeEach: 各テスト前に実行される処理

よく使うマッチャー(Matcher)

describe('Jest Matchers', () => {
// ===== 等価性 =====
it('equality matchers', () => {
expect(2 + 2).toBe(4); // 厳密等価(===)
expect({ a: 1 }).toEqual({ a: 1 }); // 深い等価性(オブジェクト比較)
expect(null).toBeNull(); // nullチェック
expect(undefined).toBeUndefined(); // undefinedチェック
expect('hello').toBeDefined(); // 定義済みチェック
});

// ===== 真偽値 =====
it('truthiness matchers', () => {
expect(true).toBeTruthy(); // truthyな値
expect(false).toBeFalsy(); // falsyな値
expect(0).toBeFalsy(); // 0はfalsy
expect('').toBeFalsy(); // 空文字はfalsy
expect(1).toBeTruthy(); // 1はtruthy
});

// ===== 数値 =====
it('number matchers', () => {
expect(10).toBeGreaterThan(5); // より大きい
expect(5).toBeGreaterThanOrEqual(5); // 以上
expect(3).toBeLessThan(5); // より小さい
expect(0.1 + 0.2).toBeCloseTo(0.3); // 浮動小数点の近似比較
});

// ===== 文字列 =====
it('string matchers', () => {
expect('Hello World').toContain('World'); // 部分文字列を含む
expect('TypeScript').toMatch(/Script$/); // 正規表現マッチ
expect('hello').toHaveLength(5); // 長さ
});

// ===== 配列 =====
it('array matchers', () => {
const fruits = ['apple', 'banana', 'orange'];
expect(fruits).toContain('banana'); // 要素を含む
expect(fruits).toHaveLength(3); // 配列の長さ
// 部分配列を含む
expect([1, 2, 3]).toEqual(expect.arrayContaining([1, 2]));
});

// ===== オブジェクト =====
it('object matchers', () => {
const user = { name: 'Alice', age: 25, email: 'alice@example.com' };
expect(user).toHaveProperty('name'); // プロパティを持つ
expect(user).toHaveProperty('age', 25); // プロパティと値
expect(user).toMatchObject({ name: 'Alice', age: 25 }); // 部分一致
});

// ===== 例外 =====
it('exception matchers', () => {
const throwError = () => {
throw new Error('Something went wrong');
};
expect(throwError).toThrow(); // 例外をスロー
expect(throwError).toThrow('Something went wrong'); // 特定のメッセージ
expect(throwError).toThrow(Error); // 特定の型
});
});

非同期テスト

非同期関数をテストする方法を学びます。

// src/api.ts
// ユーザー情報を取得する非同期関数(模擬的なAPI)
export async function fetchUser(id: number): Promise<{ id: number; name: string }> {
// 実際にはAPIを呼び出す
return new Promise(resolve => {
// 100ミリ秒後に結果を返す
setTimeout(() => {
resolve({ id, name: `User ${id}` });
}, 100);
});
}

// コールバック形式のAPI(レガシーコード用)
export function fetchUserCallback(
id: number,
callback: (user: { id: number; name: string }) => void
): void {
setTimeout(() => {
callback({ id, name: `User ${id}` });
}, 100);
}
// src/api.test.ts
import { fetchUser, fetchUserCallback } from './api';

describe('Async Tests', () => {
// パターン1: async/await(推奨)
it('should fetch user with async/await', async () => {
// awaitで非同期処理の完了を待つ
const user = await fetchUser(1);
expect(user).toEqual({ id: 1, name: 'User 1' });
});

// パターン2: Promiseを返す
it('should fetch user with Promise', () => {
// returnでPromiseを返す(Jestが完了を待つ)
return fetchUser(2).then(user => {
expect(user).toEqual({ id: 2, name: 'User 2' });
});
});

// パターン3: resolves/rejectsマッチャー
it('should resolve with user data', async () => {
// resolvesでPromiseの解決値を検証
await expect(fetchUser(3)).resolves.toEqual({ id: 3, name: 'User 3' });
});

// パターン4: コールバック(doneを使用)
it('should fetch user with callback', done => {
fetchUserCallback(4, user => {
expect(user).toEqual({ id: 4, name: 'User 4' });
done(); // テスト完了を通知(これがないとタイムアウト)
});
});
});

モック(Mock)

外部依存(API呼び出しなど)をモック化してテストを分離します。

// src/userService.ts
import { fetchUser } from './api';

export class UserService {
// ユーザー名を取得
async getUserName(id: number): Promise<string> {
const user = await fetchUser(id);
return user.name;
}

// 複数ユーザーの名前を取得
async getUsersNames(ids: number[]): Promise<string[]> {
const users = await Promise.all(ids.map(id => fetchUser(id)));
return users.map(u => u.name);
}
}
// src/userService.test.ts
import { UserService } from './userService';
import { fetchUser } from './api';

// apiモジュール全体をモック化
// 実際のAPI呼び出しは行われない
jest.mock('./api');

// モック化されたfetchUserの型定義
const mockedFetchUser = fetchUser as jest.MockedFunction<typeof fetchUser>;

describe('UserService', () => {
let userService: UserService;

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

it('should get user name', async () => {
// モックの戻り値を設定
mockedFetchUser.mockResolvedValue({ id: 1, name: 'Alice' });

const name = await userService.getUserName(1);

expect(name).toBe('Alice');
// モックが正しい引数で呼ばれたか検証
expect(mockedFetchUser).toHaveBeenCalledWith(1);
// モックが1回だけ呼ばれたか検証
expect(mockedFetchUser).toHaveBeenCalledTimes(1);
});

it('should get multiple user names', async () => {
// 複数回呼ばれる場合、それぞれ異なる値を返す
mockedFetchUser
.mockResolvedValueOnce({ id: 1, name: 'Alice' })
.mockResolvedValueOnce({ id: 2, name: 'Bob' })
.mockResolvedValueOnce({ id: 3, name: 'Charlie' });

const names = await userService.getUsersNames([1, 2, 3]);

expect(names).toEqual(['Alice', 'Bob', 'Charlie']);
expect(mockedFetchUser).toHaveBeenCalledTimes(3);
});

it('should handle API error', async () => {
// エラーをシミュレート
mockedFetchUser.mockRejectedValue(new Error('API Error'));

// rejectsでエラーを検証
await expect(userService.getUserName(1)).rejects.toThrow('API Error');
});
});

モックの主要なメソッド:

メソッド説明
jest.mock('./module')モジュール全体をモック化
mockResolvedValue(value)Promiseがresolveする値を設定
mockRejectedValue(error)Promiseがrejectする値を設定
mockResolvedValueOnce(value)1回だけ特定の値を返す
toHaveBeenCalledWith(args)特定の引数で呼ばれたか検証
toHaveBeenCalledTimes(n)呼び出し回数を検証

テストカバレッジ

テストがコードのどれだけをカバーしているか測定できます。

# カバレッジレポートを生成
npm test -- --coverage

package.json:

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

カバレッジのしきい値設定(jest.config.js):

module.exports = {
// ... 他の設定

// カバレッジのしきい値設定
coverageThreshold: {
global: {
branches: 80, // 分岐カバレッジ 80%以上
functions: 80, // 関数カバレッジ 80%以上
lines: 80, // 行カバレッジ 80%以上
statements: 80, // ステートメントカバレッジ 80%以上
},
},

// カバレッジレポートの形式
coverageReporters: ['text', 'lcov', 'html'],
};

試してみよう: ユニットテストを書こう ★★

以下のStringUtilsクラスに対するテストを書いてください。

// src/stringUtils.ts
export class StringUtils {
// 文字列を逆順にする
reverse(str: string): string {
return str.split('').reverse().join('');
}

// 文字列が回文かどうか判定
isPalindrome(str: string): boolean {
const cleaned = str.toLowerCase().replace(/[^a-z0-9]/g, '');
return cleaned === this.reverse(cleaned);
}

// 単語数をカウント
countWords(str: string): number {
if (!str.trim()) return 0;
return str.trim().split(/\s+/).length;
}
}

テストすべきケース:

  1. reverse: 通常の文字列、空文字列、1文字
  2. isPalindrome: 回文、非回文、スペースや記号を含む回文
  3. countWords: 通常の文、空文字列、複数スペース
ヒント
  1. describeでクラス名とメソッド名をグループ化
  2. beforeEachでインスタンスを作成
  3. 各メソッドに対して正常系・境界値をテスト
  4. toBetoBeTruthytoBeFalsyなどのマッチャーを使用
回答と解説
// src/stringUtils.test.ts
import { StringUtils } from './stringUtils';

describe('StringUtils', () => {
let utils: StringUtils;

// 各テスト前に新しいインスタンスを作成
beforeEach(() => {
utils = new StringUtils();
});

// reverseメソッドのテスト
describe('reverse', () => {
it('should reverse a normal string', () => {
expect(utils.reverse('hello')).toBe('olleh');
});

it('should handle empty string', () => {
expect(utils.reverse('')).toBe('');
});

it('should handle single character', () => {
expect(utils.reverse('a')).toBe('a');
});

it('should reverse Japanese characters', () => {
expect(utils.reverse('あいう')).toBe('ういあ');
});
});

// isPalindromeメソッドのテスト
describe('isPalindrome', () => {
it('should return true for palindrome', () => {
expect(utils.isPalindrome('racecar')).toBe(true);
});

it('should return false for non-palindrome', () => {
expect(utils.isPalindrome('hello')).toBe(false);
});

it('should ignore case', () => {
expect(utils.isPalindrome('RaceCar')).toBe(true);
});

it('should ignore spaces and punctuation', () => {
expect(utils.isPalindrome('A man, a plan, a canal: Panama')).toBe(true);
});

it('should handle empty string', () => {
expect(utils.isPalindrome('')).toBe(true);
});

it('should handle single character', () => {
expect(utils.isPalindrome('a')).toBe(true);
});
});

// countWordsメソッドのテスト
describe('countWords', () => {
it('should count words in a normal sentence', () => {
expect(utils.countWords('Hello World')).toBe(2);
});

it('should return 0 for empty string', () => {
expect(utils.countWords('')).toBe(0);
});

it('should return 0 for whitespace only', () => {
expect(utils.countWords(' ')).toBe(0);
});

it('should handle multiple spaces between words', () => {
expect(utils.countWords('hello world')).toBe(2);
});

it('should handle leading and trailing spaces', () => {
expect(utils.countWords(' hello world ')).toBe(2);
});

it('should count single word', () => {
expect(utils.countWords('hello')).toBe(1);
});
});
});

解説:

  • describeでクラス名とメソッド名をネストしてグループ化し、テストの構造を明確にしています
  • 各メソッドに対して、正常系、境界値(空文字、1文字)、エッジケース(スペースや記号)をテストしています
  • beforeEachで毎回新しいインスタンスを作成し、テスト間の影響を防いでいます
  • テスト名は「should + 期待する動作」の形式で記述し、何をテストしているか明確にしています

テスト実行:

# テストを実行
npm test

# カバレッジ付きで実行
npm test -- --coverage

まとめ

この章で学んだこと:

  • ESLint: コードの品質チェックと一貫したスタイルの維持
  • Prettier: コードの自動整形
  • ESLintとPrettierの統合: 両ツールの競合を解消
  • Biome: ESLint + Prettier を 1 ツールで置き換える高速な選択肢
  • TypeScript の実行: tsx、Node.js 22.18+/24 の type stripping
  • Vitest: 新規プロジェクトで第一候補のテストフレームワーク
  • Jest: テストフレームワークのセットアップ
  • テストの基本: describe、it、expect、beforeEach
  • マッチャー: toBe、toEqual、toThrow、toContainなど
  • 非同期テスト: async/await、Promise、done
  • モック: 外部依存のモック化とテストの分離

次の章では、React + TypeScriptについて学びます。

初学者がつまずきやすいポイント

警告

よくある間違い

❌ ESLintとPrettierのルールが競合する

// eslint.config.js
// ❌ Prettierと競合するルールを有効にしている
rules: {
'indent': ['error', 2], // Prettierと競合
'quotes': ['error', 'single'], // Prettierと競合
}

// ✅ eslint-config-prettierを最後に追加
import eslintConfigPrettier from 'eslint-config-prettier';

export default [
// ... 他の設定
eslintConfigPrettier, // 競合ルールを無効化
];

原因: ESLintとPrettierは同じフォーマットルールを持ち、競合することがあります。 解決策: eslint-config-prettierを設定の最後に追加して、競合するルールを無効化しましょう。

❌ Jestでasync/awaitのエラーをキャッチしない

// ❌ awaitを忘れるとテストが通ってしまうことがある
it('should throw error', () => {
expect(asyncFunction()).rejects.toThrow(); // awaitがない!
});

// ✅ awaitを付ける
it('should throw error', async () => {
await expect(asyncFunction()).rejects.toThrow();
});

原因: rejects/resolvesマッチャーはPromiseを返すため、awaitが必要です。 解決策: 非同期テストでは必ずasync/awaitを使いましょう。

❌ テスト間で状態が共有される

// ❌ 状態がテスト間で共有される
let counter = 0;

describe('Counter', () => {
it('should increment', () => {
counter++;
expect(counter).toBe(1);
});

it('should be zero', () => {
expect(counter).toBe(0); // Fail! counterは1のまま
});
});

// ✅ beforeEachでリセット
describe('Counter', () => {
let counter: number;

beforeEach(() => {
counter = 0; // 各テスト前にリセット
});

// ...
});

原因: テスト間で変数が共有されると、テストの実行順序に依存してしまいます。 解決策: beforeEachで状態を初期化し、テストを独立させましょう。

❌ モック化したモジュールをリセットしない

// ❌ モックの状態が残る
jest.mock('./api');

it('test 1', () => {
(fetchData as jest.Mock).mockResolvedValue({ id: 1 });
// ...
});

it('test 2', () => {
// test 1のモック設定が残っている可能性がある
});

// ✅ 各テスト後にリセット
afterEach(() => {
jest.clearAllMocks(); // モックの呼び出し履歴をクリア
// または
jest.resetAllMocks(); // モックの実装もリセット
});

原因: モックの状態は明示的にリセットしないと残り続けます。 解決策: afterEachまたはbeforeEachjest.clearAllMocks()を呼びましょう。

❌ toBe()でオブジェクトを比較する

// ❌ toBeは参照比較
const obj1 = { a: 1 };
const obj2 = { a: 1 };
expect(obj1).toBe(obj2); // Fail! 参照が異なる

// ✅ toEqualで値を比較
expect(obj1).toEqual(obj2); // Pass! 値が同じ

原因: toBe===で比較するため、オブジェクトは参照が同じでないとfalseになります。 解決策: オブジェクトの値比較にはtoEqual、プリミティブにはtoBeを使いましょう。


次に読む

実践 へ進みます。React + TypeScript、Express バックエンド、最終プロジェクトで総合演習します。