【入門】Jest 初心者が最初に知っておくべきテスト設計の基本原則

JavaScript 開発において、品質の高いアプリケーションを作成するためには、テストが欠かせません。特に Jest は、その使いやすさと豊富な機能で多くの開発者に愛用されているテストフレームワークです。
しかし、いざテストを書こうとすると「何から始めればいいかわからない」「どのようにテストケースを設計すればいいかわからない」といった悩みを抱える方も多いのではないでしょうか。
今回は、Jest 初心者の方が最初に知っておくべきテスト設計の基本原則について、段階的に解説していきます。この記事を読むことで、効果的なテストの書き方と、保守しやすいテストコードの作成方法を身につけることができるでしょう。
背景
Jest とは何か
Jest は、Facebook(現 Meta)によって開発された JavaScript テストフレームワークです。シンプルな設定で動作し、多機能でありながら学習コストが低いことが特徴です。
javascript// 基本的なJestテストの例
function add(a, b) {
return a + b;
}
test('1 + 2 は 3 になる', () => {
expect(add(1, 2)).toBe(3);
});
Jest の主な特徴を表にまとめました。
# | 特徴 | 詳細 |
---|---|---|
1 | ゼロ設定 | 複雑な設定なしですぐに使い始められます |
2 | スナップショットテスト | UI コンポーネントの変更を検知できます |
3 | 並列実行 | テストを並列で実行し、実行時間を短縮できます |
4 | モック機能 | 外部依存をモック化してテストを独立させられます |
5 | カバレッジレポート | どの部分がテストされているかを可視化できます |
なぜテスト設計が重要なのか
テスト設計が重要な理由は、単にバグを見つけるためだけではありません。以下の図でテスト設計の価値を示します。
mermaidflowchart TD
design[テスト設計] --> quality[品質向上]
design --> maintenance[保守性向上]
design --> confidence[開発の自信]
quality --> bug_reduction[バグの早期発見]
quality --> regression[リグレッション防止]
maintenance --> readable[読みやすいコード]
maintenance --> modular[モジュール化促進]
confidence --> refactoring[安全なリファクタリング]
confidence --> feature_add[新機能追加の安心感]
図で理解できる要点:
- テスト設計は品質、保守性、開発の自信という 3 つの価値を生み出します
- これらの価値が相互に作用し、開発全体の効率性を高めます
- 結果として、安定したソフトウェア開発が可能になります
良いテスト設計により、以下のメリットが得られます。
- バグの早期発見:開発段階でバグを見つけ、修正コストを削減
- リファクタリングの安全性:既存機能を壊さずにコード改善が可能
- ドキュメント効果:テストコードが仕様書の役割を果たす
- 開発速度の向上:手動テストの時間を削減し、効率的な開発が可能
JavaScript 開発におけるテストの位置づけ
JavaScript 開発におけるテストは、フロントエンドからバックエンドまで幅広い領域で活用されています。
mermaidgraph LR
subgraph frontend[フロントエンド]
component[コンポーネントテスト]
integration[統合テスト]
e2e[E2Eテスト]
end
subgraph backend[バックエンド]
unit[ユニットテスト]
api[APIテスト]
db[データベーステスト]
end
jest[Jest] --> component
jest --> unit
jest --> api
testing_lib[Testing Library] --> component
testing_lib --> integration
cypress[Cypress] --> e2e
supertest[Supertest] --> api
図で理解できる要点:
- Jest は主にユニットテストとコンポーネントテストで活用されます
- フロントエンドでは Testing Library と組み合わせることが多いです
- 各テストレベルで適切なツールを選択することが重要です
特に Jest は以下の分野で威力を発揮します。
- ユニットテスト:関数やクラスの単体テスト
- コンポーネントテスト:React コンポーネントの動作確認
- 統合テスト:複数のモジュール間の連携テスト
課題
テストを書きたいが何から始めればよいかわからない
Jest 初心者の方が最初に直面する課題の一つが「どこから手をつければよいかわからない」ということです。
mermaidstateDiagram-v2
[*] --> confused: テストを書きたい
confused --> overwhelmed: 情報が多すぎる
overwhelmed --> paralyzed: 何もできない
paralyzed --> frustrated: 時間だけが過ぎる
frustrated --> give_up: テストを諦める
confused --> structured: 段階的アプローチ
structured --> practice: 実践
practice --> confident: 自信がつく
confident --> [*]
この問題は、以下の要因から生じることが多いです。
# | 要因 | 詳細 | 対策 |
---|---|---|---|
1 | 情報過多 | 高度なテクニックの情報が多い | 基本から段階的に学習 |
2 | 完璧主義 | 最初から完璧なテストを書こうとする | 小さく始めて徐々に改善 |
3 | 実践不足 | 理論だけで実際に書く経験が少ない | 簡単な例から実際に手を動かす |
4 | 目標不明確 | 何を目指すべきかがわからない | 明確な学習目標を設定 |
テストケースの作り方がわからない
テストケースの作成において、初心者の方が迷いやすいポイントです。
javascript// 悪い例:何をテストしているかわからない
test('テスト', () => {
const result = doSomething(input);
expect(result).toBeTruthy();
});
javascript// 良い例:明確な意図があるテスト
test('正の数を入力した場合、その数の平方根を返す', () => {
const result = calculateSquareRoot(9);
expect(result).toBe(3);
});
テストケース作成でよくある問題点:
- テストの意図が不明確:何をテストしているかわからない
- テストケースが不十分:正常系のみで異常系を考慮していない
- テストの粒度が適切でない:一つのテストで複数のことをテストしている
どこまでテストを書けばよいかわからない
テストカバレッジの目標設定や、どの部分にテストを書くべきかの判断に迷う方が多いです。
mermaidflowchart TD
start[テスト対象の選定] --> critical[重要度の評価]
critical --> high[高重要度]
critical --> medium[中重要度]
critical --> low[低重要度]
high --> must_test[必須テスト]
medium --> should_test[推奨テスト]
low --> optional_test[任意テスト]
must_test --> business_logic[ビジネスロジック]
must_test --> public_api[公開API]
should_test --> util_func[ユーティリティ関数]
optional_test --> simple_getter[単純なgetter]
図で理解できる要点:
- テスト対象を重要度で分類し、優先順位をつけます
- ビジネスロジックや公開 API は必須でテストを書きます
- リソースに応じて、重要度の高いものから順にテストを充実させます
適切なテスト範囲を決める指針:
- 必須:ビジネスロジック、公開 API、複雑なアルゴリズム
- 推奨:ユーティリティ関数、データ変換処理
- 任意:単純な getter/setter、定数の取得
解決策
テスト設計の 3 つの基本原則
効果的なテストを書くために、以下の 3 つの基本原則を押さえておくことが重要です。
AAA(Arrange, Act, Assert)パターン
AAA パターンは、テストを 3 つの段階に分けて構造化する手法です。
javascripttest('ユーザー作成時に正しいデータが設定される', () => {
// Arrange: テストに必要なデータや環境を準備
const userData = {
name: '田中太郎',
email: 'tanaka@example.com',
age: 30,
};
// Act: テスト対象の処理を実行
const user = new User(userData);
// Assert: 期待される結果かどうかを検証
expect(user.name).toBe('田中太郎');
expect(user.email).toBe('tanaka@example.com');
expect(user.age).toBe(30);
});
AAA パターンの各段階の役割:
# | 段階 | 英語 | 役割 | 実装のポイント |
---|---|---|---|---|
1 | 準備 | Arrange | テストデータの準備 | 必要最小限のデータを用意 |
2 | 実行 | Act | テスト対象の処理実行 | 一つの明確なアクション |
3 | 検証 | Assert | 結果の確認 | 期待値との比較 |
1 つのテストは 1 つのことをテストする
単一責任の原則をテストにも適用し、一つのテストでは一つの機能や動作のみをテストします。
javascript// 悪い例:複数のことをテストしている
test('ユーザーサービスのテスト', () => {
const user = createUser('田中太郎');
expect(user.name).toBe('田中太郎');
const updated = updateUser(user, { age: 25 });
expect(updated.age).toBe(25);
const deleted = deleteUser(user.id);
expect(deleted).toBe(true);
});
javascript// 良い例:それぞれ独立したテスト
test('ユーザー作成時に名前が正しく設定される', () => {
const user = createUser('田中太郎');
expect(user.name).toBe('田中太郎');
});
test('ユーザー更新時に年齢が正しく変更される', () => {
const user = createUser('田中太郎');
const updated = updateUser(user, { age: 25 });
expect(updated.age).toBe(25);
});
test('ユーザー削除時にtrueが返される', () => {
const user = createUser('田中太郎');
const deleted = deleteUser(user.id);
expect(deleted).toBe(true);
});
単一責任テストのメリット:
- 問題の特定が容易:テストが失敗した時、原因が明確
- 保守性の向上:変更時の影響範囲が限定的
- 可読性の向上:テストの意図が明確
テストは独立している必要がある
各テストは他のテストの実行結果に依存せず、独立して実行できる必要があります。
javascript// 悪い例:テスト間で状態を共有
let globalUser;
test('ユーザーを作成する', () => {
globalUser = createUser('田中太郎');
expect(globalUser.name).toBe('田中太郎');
});
test('ユーザーを更新する', () => {
// 前のテストに依存している
const updated = updateUser(globalUser, { age: 25 });
expect(updated.age).toBe(25);
});
javascript// 良い例:各テストが独立している
test('ユーザーを作成する', () => {
const user = createUser('田中太郎');
expect(user.name).toBe('田中太郎');
});
test('ユーザーを更新する', () => {
// テスト内で必要なデータを準備
const user = createUser('田中太郎');
const updated = updateUser(user, { age: 25 });
expect(updated.age).toBe(25);
});
テスト独立性を保つ方法:
javascript// beforeEachでテストごとに初期化
describe('ユーザーサービステスト', () => {
let userService;
beforeEach(() => {
userService = new UserService();
});
test('ユーザー作成テスト', () => {
const user = userService.create('田中太郎');
expect(user.name).toBe('田中太郎');
});
test('ユーザー更新テスト', () => {
const user = userService.create('田中太郎');
const updated = userService.update(user.id, {
age: 25,
});
expect(updated.age).toBe(25);
});
});
具体例
基本的な Jest テストの書き方
describe と it の使い方
Jest では describe
と it
(または test
)を使ってテストを構造化します。
javascript// 基本的な構造
describe('電卓クラス', () => {
it('2つの数値を足し算できる', () => {
const calculator = new Calculator();
const result = calculator.add(2, 3);
expect(result).toBe(5);
});
it('2つの数値を引き算できる', () => {
const calculator = new Calculator();
const result = calculator.subtract(5, 3);
expect(result).toBe(2);
});
});
ネストした構造でより詳細にテストを整理できます:
javascriptdescribe('ユーザー管理システム', () => {
describe('ユーザー作成', () => {
it('有効なデータでユーザーを作成できる', () => {
const userData = {
name: '田中太郎',
email: 'tanaka@example.com',
};
const user = createUser(userData);
expect(user.name).toBe('田中太郎');
});
it('無効なメールアドレスでエラーが発生する', () => {
const userData = {
name: '田中太郎',
email: 'invalid-email',
};
expect(() => createUser(userData)).toThrow(
'無効なメールアドレス'
);
});
});
describe('ユーザー更新', () => {
it('既存ユーザーの情報を更新できる', () => {
const user = createUser({
name: '田中太郎',
email: 'tanaka@example.com',
});
const updated = updateUser(user.id, {
name: '田中花子',
});
expect(updated.name).toBe('田中花子');
});
});
});
expect の基本的な使い方
Jest は豊富なマッチャーを提供しており、様々な検証が可能です。
javascript// 基本的なマッチャー
test('基本的なマッチャーの使い方', () => {
// 同値比較
expect(2 + 2).toBe(4);
expect({ name: '田中' }).toEqual({ name: '田中' });
// 真偽値
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
// 数値比較
expect(2 + 2).toBeGreaterThan(3);
expect(Math.PI).toBeCloseTo(3.14, 2);
// 文字列
expect('Hello World').toMatch(/World/);
expect('testing').toContain('test');
});
配列とオブジェクトのテスト:
javascripttest('配列とオブジェクトのテスト', () => {
const users = ['田中', '佐藤', '鈴木'];
// 配列の検証
expect(users).toContain('田中');
expect(users).toHaveLength(3);
expect(users).toEqual(
expect.arrayContaining(['田中', '佐藤'])
);
const user = {
id: 1,
name: '田中太郎',
profile: {
age: 30,
city: '東京',
},
};
// オブジェクトの検証
expect(user).toHaveProperty('name');
expect(user).toHaveProperty('profile.age', 30);
expect(user).toMatchObject({
name: '田中太郎',
profile: expect.objectContaining({
city: '東京',
}),
});
});
セットアップとクリーンアップ
テストの前後で必要な処理を実行するためのフック関数を活用します。
javascriptdescribe('データベース操作テスト', () => {
let database;
// 全テスト実行前に一度だけ実行
beforeAll(() => {
database = new Database();
database.connect();
});
// 全テスト実行後に一度だけ実行
afterAll(() => {
database.close();
});
// 各テスト実行前に毎回実行
beforeEach(() => {
database.clearData();
database.seedTestData();
});
// 各テスト実行後に毎回実行
afterEach(() => {
database.clearData();
});
test('ユーザーデータを保存できる', () => {
const user = { name: '田中太郎' };
const saved = database.save(user);
expect(saved.id).toBeDefined();
});
});
実践的なテストケース作成
関数のテスト
実際のビジネスロジックを含む関数のテスト例です。
javascript// テスト対象の関数
function calculateTax(price, taxRate = 0.1) {
if (price < 0) {
throw new Error('価格は0以上である必要があります');
}
if (taxRate < 0 || taxRate > 1) {
throw new Error('税率は0から1の間である必要があります');
}
return Math.round(price * taxRate);
}
javascriptdescribe('税金計算関数', () => {
test('正常な価格と税率で税額を計算する', () => {
const tax = calculateTax(1000, 0.1);
expect(tax).toBe(100);
});
test('税率を省略した場合、デフォルト税率10%が適用される', () => {
const tax = calculateTax(1000);
expect(tax).toBe(100);
});
test('小数点以下は四捨五入される', () => {
const tax = calculateTax(333, 0.1);
expect(tax).toBe(33); // 33.3 → 33
});
test('価格が負の値の場合エラーが発生する', () => {
expect(() => calculateTax(-100)).toThrow(
'価格は0以上である必要があります'
);
});
test('税率が範囲外の場合エラーが発生する', () => {
expect(() => calculateTax(1000, 1.5)).toThrow(
'税率は0から1の間である必要があります'
);
expect(() => calculateTax(1000, -0.1)).toThrow(
'税率は0から1の間である必要があります'
);
});
});
非同期処理のテスト
Promise や async/await を使った非同期処理のテスト方法です。
javascript// テスト対象の非同期関数
async function fetchUserData(userId) {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('ユーザーが見つかりません');
}
return response.json();
}
javascriptdescribe('ユーザーデータ取得', () => {
// fetch をモック化
beforeEach(() => {
global.fetch = jest.fn();
});
afterEach(() => {
jest.resetAllMocks();
});
test('正常にユーザーデータを取得する', async () => {
const mockUser = { id: 1, name: '田中太郎' };
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser,
});
const user = await fetchUserData(1);
expect(user).toEqual(mockUser);
expect(fetch).toHaveBeenCalledWith('/api/users/1');
});
test('ユーザーが見つからない場合エラーが発生する', async () => {
fetch.mockResolvedValueOnce({
ok: false,
});
await expect(fetchUserData(999)).rejects.toThrow(
'ユーザーが見つかりません'
);
});
test('ネットワークエラーの場合エラーが発生する', async () => {
fetch.mockRejectedValueOnce(new Error('Network Error'));
await expect(fetchUserData(1)).rejects.toThrow(
'Network Error'
);
});
});
エラーハンドリングのテスト
様々なエラーケースを想定したテストの書き方です。
javascript// テスト対象のクラス
class BankAccount {
constructor(initialBalance = 0) {
if (initialBalance < 0) {
throw new Error(
'初期残高は0以上である必要があります'
);
}
this.balance = initialBalance;
}
withdraw(amount) {
if (amount <= 0) {
throw new Error('出金額は0より大きい必要があります');
}
if (amount > this.balance) {
throw new Error('残高不足です');
}
this.balance -= amount;
return this.balance;
}
}
javascriptdescribe('銀行口座クラス', () => {
describe('初期化', () => {
test('正常な初期残高で口座を作成できる', () => {
const account = new BankAccount(1000);
expect(account.balance).toBe(1000);
});
test('初期残高を省略した場合、残高0で作成される', () => {
const account = new BankAccount();
expect(account.balance).toBe(0);
});
test('負の初期残高でエラーが発生する', () => {
expect(() => new BankAccount(-100)).toThrow(
'初期残高は0以上である必要があります'
);
});
});
describe('出金処理', () => {
let account;
beforeEach(() => {
account = new BankAccount(1000);
});
test('正常に出金できる', () => {
const newBalance = account.withdraw(300);
expect(newBalance).toBe(700);
expect(account.balance).toBe(700);
});
test('0以下の出金額でエラーが発生する', () => {
expect(() => account.withdraw(0)).toThrow(
'出金額は0より大きい必要があります'
);
expect(() => account.withdraw(-100)).toThrow(
'出金額は0より大きい必要があります'
);
});
test('残高を超える出金でエラーが発生する', () => {
expect(() => account.withdraw(1500)).toThrow(
'残高不足です'
);
});
test('残高ちょうどの出金は成功する', () => {
const newBalance = account.withdraw(1000);
expect(newBalance).toBe(0);
});
});
});
まとめ
Jest 初心者の方に向けて、テスト設計の基本原則について解説しました。重要なポイントを整理すると以下の通りです。
テスト設計の 3 つの基本原則:
- AAA パターン:準備、実行、検証の 3 段階で構造化
- 単一責任:1 つのテストで 1 つのことをテストする
- 独立性:テスト間で状態を共有しない
実践のコツ:
- 簡単な関数から始めて、徐々に複雑なケースに挑戦する
- エラーケースも含めて、様々なシナリオをテストする
- 非同期処理やモックを適切に活用する
継続的な改善:
- テストコードも通常のコードと同様にリファクタリングを行う
- テストの実行時間やメンテナンス性を意識する
- チーム内でテスト品質の基準を共有する
良いテストは、コードの品質向上だけでなく、開発者の自信や開発速度の向上にもつながります。最初は小さく始めて、着実にテストスキルを向上させていきましょう。
継続的にテストを書くことで、バグの少ない高品質なアプリケーション開発が可能になります。ぜひ今回学んだ基本原則を実際のプロジェクトで活用してみてください。
関連リンク
- article
【入門】Jest 初心者が最初に知っておくべきテスト設計の基本原則
- article
Jest でテストを書き続けるコツと開発文化への定着方法
- article
テストフレームワーク(Jest・Mocha)と Node.js
- article
Emotion と Jest/Testing Library で UI テストを快適に
- article
Jest のバージョンアップ手順とトラブルシューティング
- article
Jest と Monorepo 構成:複数パッケージでの活用法
- article
Astro と Tailwind CSS で美しいデザインを最速実現
- article
shadcn/ui のコンポーネント一覧と使い方まとめ
- article
Apollo Client の Reactive Variables - GraphQL でグローバル状態管理
- article
Remix でデータフェッチ最適化:Loader のベストプラクティス
- article
ゼロから始める Preact 開発 - セットアップから初回デプロイまで
- article
Zod で配列・オブジェクトを安全に扱うバリデーションテクニック
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- blog
失敗を称賛する文化はどう作る?アジャイルな組織へ生まれ変わるための第一歩
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来