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

Test

Overview

ソフトウェアテストの概念や手法について記載しているセクション。

Parameterized Testing(パラメータライズドテスト)

パラメータライズドテスト(Parameterized Testing)は、同じテストロジックを異なる入力値セットに対して繰り返し実行する手法。
これによりコードを繰り返し書くことなく、多くのケースを網羅的にテストすることができる。
Jest の test.each で Parameterized test を実行する

  • テストの重複を減らす
  • 複数のパターンを簡潔にテストできる
  • 失敗時にどのパターンで失敗したかが明確になる
// 例:Jest
describe.each([
['valid case', 1, true],
['invalid case', 0, false],
])('%s', (caseName, input, expected) => {
test('returns expected result', () => {
expect(validate(input)).toBe(expected);
});
});
ヒント

この方法を使うことで、テストのメンテナンス性と網羅性を大きく向上させることができる。

Unit Testing(単体テスト)

単体テストは、最小単位(関数やクラス)を対象に動作確認を行うテスト手法。
外部依存をできるだけ排除して、個々の処理が正しく動くかを検証する。

  • 小さな単位に集中
  • 失敗時に原因が特定しやすい
  • テスト速度が非常に速い
// 例:Jest
describe('Calculator', () => {
describe('add', () => {
it('should return sum of two numbers', () => {
expect(add(2, 3)).toBe(5);
});
});
});
ヒント

Unitテストの目的は、「コードの振る舞い(ロジック)」の確認
そのためロックされているかなどのDBレベルは統合テスト(Integration Test)や負荷テストの領域

Integration Testing(結合テスト)

複数のモジュールやコンポーネントを組み合わせて、システム間の連携が正しく動作するかを確認する。

  • 依存関係の問題を検出できる
  • バグの早期発見に役立つ
  • テスト設計がやや複雑になる場合もある
// 例:Jest + Supertest
const request = require('supertest');
const app = require('../app');

describe('POST /login', () => {
it('should login successfully', async () => {
const response = await request(app)
.post('/login')
.send({ email: 'user@example.com', password: 'password' });
expect(response.statusCode).toBe(200);
});
});

System Testing(システムテスト・E2E)

エンドユーザーが操作する流れをブラウザなどを使ってシミュレートし、システム全体の動作を検証する。

  • 実際のユーザー体験に近い
  • UIやバックエンド間の問題を検出できる
  • 実行時間が長くなりがち
// 例:Jest + Puppeteer
const puppeteer = require('puppeteer');

describe('User Registration', () => {
it('should allow user to sign up', async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000/signup');

await page.type('#email', 'test@example.com');
await page.type('#password', 'password');
await page.click('button[type="submit"]');

await page.waitForSelector('.welcome-message');
const text = await page.$eval('.welcome-message', el => el.textContent);
expect(text).toContain('Welcome');

await browser.close();
});
});

Mock / Stub(モック・スタブ)

外部サービスやクラスへの依存をテスト中に置き換えることで、対象コードのみを検証するテクニック。

  • 外部依存を排除できる
  • テスト速度を向上できる
  • 過度なモックはテストの意味を損なう場合がある
// 例:Jest
jest.mock('../services/paymentService', () => ({
charge: jest.fn(() => true),
}));

const { charge } = require('../services/paymentService');

test('should call payment service', () => {
expect(charge()).toBe(true);
});

テストピラミッド

テストピラミッドは、ソフトウェアテストの理想的な構成比率を表す概念。

  • 単体テストが最も多く、素早く回る
  • 結合テストは中間的な量とコスト
  • **システムテスト(E2Eテスト)**は少量で高コスト・高信頼性
  [ E2E Tests ]

[ Integration Tests ]

[ Unit Tests (大量) ]

テストピラミッドを意識することで、効率的かつ堅牢なテスト戦略を構築できる。

テストダブルの種類

テストダブルをなんとなく理解する

テストダブル(Test Doubles)は、本物のコンポーネントや依存関係の代わりに使うオブジェクト全般を指す。

テストダブル (Test Double) とは、ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品のこと。ダブルは代役、影武者を意味する。

代表的な種類は以下の通り。

名前説明
Dummy使われないがパラメータとして必要なだけのオブジェクト未使用の引数にnullや空オブジェクトを渡す
Fake本番用とは異なる軽量実装を持つオブジェクトインメモリDB、簡易キャッシュ
Stubメソッド呼び出しに対して固定の結果を返すオブジェクトAPIレスポンスを固定化する
Mock振る舞い(呼び出し回数や引数)を検証できるオブジェクト関数呼び出しをモックしてアサートする
Spy実際の処理を通しつつ呼び出しを記録するオブジェクト通常関数をラップして呼び出し履歴を確認
// 例:Jest
const fn = jest.fn();
fn.mockReturnValue('stubbed');

expect(fn()).toBe('stubbed'); // Stub
expect(fn).toHaveBeenCalled(); // Mock

テストピラミッドの応用(プラクティス)

テストピラミッドをさらに効果的に活用するためのプラクティス例。

  • 単体テストを基盤にする
    → バグ検出の初動を早くし、フィードバックループを速める。

  • 結合テストは重要な組み合わせに絞る
    → フロー全体の接続性を重点的にチェックする。

  • E2Eテストは本当に必要なシナリオだけ
    → たとえば「購入フロー」「ログイン認証」など、ビジネスに直結する重要フローだけに絞る。

  • テストの信頼性を最優先
    → 不安定なテスト(Flaky Test)は技術的負債になるので徹底して排除・安定化する。

これらを意識することで、より少ないコストで最大限の品質保証ができるようになります。

失敗しやすいテスト設計例

テスト設計でよく陥りがちな失敗パターンを紹介します。

テスト対象が広すぎる

  • 1つのテストケースで複数の機能を検証してしまう。
  • → 問題発生時にどこが原因かわかりづらくなる。

外部依存をそのまま使う

  • 実際のDBやAPIを直接叩くテスト。
  • → テストが遅く、不安定になるリスクが高まる。

アサーションが不十分

  • 期待される動作を厳密に検証していない。
  • → 本来バグが起きてもテストが通ってしまう。

テストデータが雑

  • 無関係なフィールドにゴミデータを詰めている。
  • → 本当に必要なデータだけセットする方がシンプルで堅牢。

これらを避けるだけで、テスト品質が大きく向上します。

テストカバレッジ活用法

テストカバレッジとは、テストがコードのどれだけの部分を網羅しているかを示す指標です。
ただし「カバレッジ率だけ」を追うのは危険です。

正しい使い方

  • カバレッジを「アラート」として使う
    • 明らかにカバレッジが低い部分はテスト漏れを疑う。
  • 重要なビジネスロジックを最優先でカバーする
    • 特にドメイン知識に関わるコードはカバレッジ100%を目指す。

よくある誤解

  • カバレッジ100% = バグゼロ ではない
    • カバレッジがあっても、間違った期待値でアサートしていたら意味がない。

Jestでカバレッジレポートを出す例

npx jest --coverage

実行すると、各ファイルごとにStatements / Branches / Functions / Linesのカバレッジがレポートされます。

カバレッジを賢く活用して、テストの「漏れ」と「意味」を両方意識することが重要です。

  • 結合テスト(Integration Test)
    • モジュール同士をつないで、インターフェースやデータのやり取りが正しく動くかを確認する。
    • 開発者自身が PR 作成時やローカル環境で自然にやっていることが多い。
  • 総合テスト(System Test)
    • システム全体が仕様通りに動くかを検証する。環境は QA や Staging。ここで初めて本番に近い構成での動作をチェック。
  • QA(Quality Assurance)
    • 専任の QA エンジニアが入って、網羅的にテスト計画に基づいて検証する。バグ検出や仕様齟齬の発見が目的。
  • QC(Quality Control)
    • 出来上がった成果物が要求品質を満たしているかを検証・判定する活動。リリース直前の最終チェックに近いイメージ。

開発者チェックと QA の関係 • 望ましい流れ

  1. 開発者が PR 作成時に 基本的な結合テスト・自己チェック を実施する (=「最低限、自分で壊していないか」を担保する段階)
  2. そのうえで QA エンジニアに依頼し、体系的かつ網羅的な QA テスト に回す

こうすると QA エンジニアは「明らかなバグ潰し」ではなく「網羅的検証・ユーザー視点のテスト」に集中できる。 結果的にフィードバックの質が上がり、開発と QA 双方の時間を無駄にしない。

マイルストーンに「結合」を組み込むべきか?

  • 結合テストをマイルストーンに置くメリット
  • 「開発者自身の結合チェック」が明文化され、チーム全体での最低基準が揃う
  • QA に依頼する前の責任分担がクリアになる
  • ただし注意点
  • 実際には PR 発行時に自然に行われていることが多いため、「改めて結合フェーズをマイルストーン化」すると冗長になる可能性がある
  • その場合は「結合テスト」というフェーズを新設するより、PR 作成時の定義済みチェックリストに落とし込む方が現実的

提案

  • マイルストーンに「結合」を独立して設けるのではなく、「開発完了」の定義に『結合テスト(自己確認)が完了していること』を含める のが自然。
  • 例えば Definition of Done (DoD) に以下を追加:
  • PR にユニットテストが含まれていること
  • ローカル/QA環境で主要な結合動作を確認していること
  • チェックリストに沿って自己確認済みであること
ヒント

結論としては、「結合テストをマイルストーン化」するよりも、「開発者が QA に依頼する前に必ず結合テストを済ませる」というルールを DoD やチェックリストで明文化する方が現実的で効果的