- Authors

- Name
- Youngju Kim
- @fjvbn20031
はじめに:なぜテストするのか
ソフトウェアバグのコストは、発見が遅れるほど指数関数的に増大する。開発段階で発見すれば修正コストが1だとすると、QA段階では10、プロダクションでは100以上になる。IBM Systems Sciences Instituteの研究によると、プロダクションで発見された欠陥の修正コストは設計段階の最大100倍に達する。
テストは単にバグを見つける行為ではない。コードの設計を改善し、リファクタリングのセーフティネットを提供し、チーム間のコミュニケーションツールとなる。適切に書かれたテストは生きたドキュメントでありシステムの仕様書である。
テストのROI
テスト作成に費やす時間をコストと見なすチームがある。しかし実際はその逆だ。
デバッグ時間の短縮: テストが充実したコードベースでは、問題発生時の原因特定が速い。どのテストが失敗したかを見ればよい。
リファクタリングへの自信: テストなしにレガシーコードを修正するのは、目隠しをして手術するようなものだ。テストがあれば大胆に構造を変更できる。
デプロイ速度の向上: 自動化されたテストスイートは手動QA時間を大幅に削減する。CI/CDパイプラインで自動的に品質を検証する。
ドキュメント効果: テストコードは使用例であり振る舞いの仕様書だ。新しいチームメンバーがシステムを理解する最速の方法である。
1. テストピラミッド
テストピラミッドはMike Cohnが提唱した概念で、テストの種類別の比率を視覚的に表現する。
ピラミッド構造
/ E2E \ ~10% 遅いがユーザー視点の検証
/ 統合 \ ~20% コンポーネント間の相互作用を検証
/ ユニット \ ~70% 高速で分離された単位の検証
ユニットテスト(70%): ピラミッドの最下層に位置し、個々の関数、メソッド、クラスを独立して検証する。実行が速く、作成が容易だ。
統合テスト(20%): 中間層として複数のコンポーネントが連携して動作することを検証する。APIエンドポイント、データベースクエリ、外部サービス連携などをテストする。
E2Eテスト(10%): 最上層として実際のユーザーシナリオを全体的に検証する。ブラウザ自動化を通じてワークフロー全体をテストする。
逆ピラミッド・アンチパターン
多くのチームがE2Eテストに過度に依存する逆ピラミッド(アイスクリームコーン)パターンに陥る。
\ E2E / ~70% 遅くて不安定
\ 統合 / ~20%
\ ユニット / ~10% ほぼ不在
このパターンの問題は明白だ。
- E2Eテストは遅く、フィードバックループが長くなる
- フレイキー(不安定な)テストが頻発する
- 失敗原因の特定が困難になる
- メンテナンスコストが指数関数的に増大する
正しいアプローチはピラミッドの下層からしっかり積み上げることだ。ユニットテストでコアロジックを保護し、統合テストで接続部分を検証し、E2Eテストで重要なユーザージャーニーのみ確認する。
2. ユニットテスト
ユニットテストは最小単位のコードを分離して検証するテストだ。
良いユニットテストの条件:FIRST原則
Fast(高速): ミリ秒単位で実行されるべきだ。遅いユニットテストは開発者が頻繁に実行しなくなる。
Isolated(分離): 他のテストに依存せず、外部システム(DB、ネットワーク、ファイルシステム)にも依存しない。
Repeatable(再現可能): どの環境でも、何回実行しても同じ結果を返す。
Self-validating(自己検証): テスト結果が自動的にpass/failで判定される。手動確認は不要だ。
Timely(適時): プロダクションコードの作成前または直後に書く。
モッキング戦略
外部依存を分離するためにモッキングを使用する。ただし、過度なモッキングはテストを実装の詳細に結合させてしまう。
# Python - pytest + unittest.mock
from unittest.mock import Mock, patch
import pytest
class PaymentService:
def __init__(self, gateway):
self.gateway = gateway
def charge(self, amount, card_token):
if amount <= 0:
raise ValueError("Amount must be positive")
return self.gateway.process_payment(amount, card_token)
class TestPaymentService:
def test_charge_positive_amount(self):
# Arrange
mock_gateway = Mock()
mock_gateway.process_payment.return_value = "txn_123"
service = PaymentService(mock_gateway)
# Act
result = service.charge(100, "card_abc")
# Assert
assert result == "txn_123"
mock_gateway.process_payment.assert_called_once_with(100, "card_abc")
def test_charge_negative_amount_raises(self):
mock_gateway = Mock()
service = PaymentService(mock_gateway)
with pytest.raises(ValueError, match="Amount must be positive"):
service.charge(-50, "card_abc")
mock_gateway.process_payment.assert_not_called()
// JavaScript - Jest
describe('PaymentService', () => {
it('should charge positive amount', async () => {
const mockGateway = {
processPayment: jest.fn().mockResolvedValue('txn_123'),
};
const service = new PaymentService(mockGateway);
const result = await service.charge(100, 'card_abc');
expect(result).toBe('txn_123');
expect(mockGateway.processPayment).toHaveBeenCalledWith(100, 'card_abc');
});
it('should reject negative amount', async () => {
const mockGateway = { processPayment: jest.fn() };
const service = new PaymentService(mockGateway);
await expect(service.charge(-50, 'card_abc'))
.rejects.toThrow('Amount must be positive');
expect(mockGateway.processPayment).not.toHaveBeenCalled();
});
});
カバレッジの罠
コードカバレッジ100%を目標にするのは誤ったアプローチだ。重要なのはカバレッジの数値ではなくテストの品質である。
意味のあるカバレッジ: ビジネスロジック、境界条件、エラー処理を検証する。
無意味なカバレッジ: getter/setter、単純な委譲メソッド、フレームワークコードのテストは時間の無駄だ。
実用的な目標はコアビジネスロジック80%以上、全体コード60~80%程度だ。100%カバレッジよりも意味のあるアサーションを持つテストの方がはるかに重要だ。
3. 統合テスト
統合テストは複数のコンポーネントが連携して動作することを検証する。実際のデータベース、メッセージキュー、外部APIとの相互作用をテストする。
APIテスト
REST APIテストは統合テストの代表的な例だ。
# Python - FastAPI + pytest + httpx
import pytest
from httpx import AsyncClient, ASGITransport
from app.main import app
@pytest.mark.asyncio
async def test_create_user():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.post("/users", json={
"name": "Alice",
"email": "alice@example.com"
})
assert response.status_code == 201
data = response.json()
assert data["name"] == "Alice"
assert "id" in data
@pytest.mark.asyncio
async def test_create_user_duplicate_email():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as client:
await client.post("/users", json={
"name": "Alice",
"email": "alice@example.com"
})
response = await client.post("/users", json={
"name": "Bob",
"email": "alice@example.com"
})
assert response.status_code == 409
テストコンテナ(Testcontainers)
Testcontainersは、Dockerコンテナを活用して実際のデータベース、Redis、Kafkaなどをテストに使用するライブラリだ。モックの代わりに実際のインフラを使用するため、より信頼性の高いテストが可能だ。
// Java - Testcontainers + JUnit 5
@Testcontainers
@SpringBootTest
class UserRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private UserRepository userRepository;
@Test
void shouldSaveAndFindUser() {
User user = new User("Alice", "alice@example.com");
userRepository.save(user);
Optional<User> found = userRepository.findByEmail("alice@example.com");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Alice");
}
@Test
void shouldReturnEmptyForNonExistentUser() {
Optional<User> found = userRepository.findByEmail("nobody@example.com");
assertThat(found).isEmpty();
}
}
# Python - testcontainers
from testcontainers.postgres import PostgresContainer
import psycopg2
def test_user_crud():
with PostgresContainer("postgres:16") as postgres:
conn = psycopg2.connect(
host=postgres.get_container_host_ip(),
port=postgres.get_exposed_port(5432),
user=postgres.username,
password=postgres.password,
dbname=postgres.dbname,
)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100) UNIQUE
)
""")
cursor.execute(
"INSERT INTO users (name, email) VALUES (%s, %s) RETURNING id",
("Alice", "alice@example.com")
)
user_id = cursor.fetchone()[0]
cursor.execute("SELECT name FROM users WHERE id = %s", (user_id,))
assert cursor.fetchone()[0] == "Alice"
conn.commit()
conn.close()
DBテスト戦略
データベーステストで注意すべきポイントは以下の通りだ。
テストの分離: 各テストは独立していなければならない。トランザクションロールバックやテスト前のデータ初期化を使用する。
マイグレーション検証: スキーマ変更が既存データと互換性があるか検証する。
シードデータ管理: テストに必要な基礎データを体系的に管理する。FactoryパターンやFixtureを活用する。
4. E2Eテスト
E2E(エンドツーエンド)テストは実際のユーザー視点でシステム全体を検証する。
Playwright
Playwrightは、Microsoftが開発したブラウザ自動化フレームワークで、Chromium、Firefox、WebKitすべてをサポートする。
// Playwright テスト例
import { test, expect } from '@playwright/test';
test.describe('ユーザーログインフロー', () => {
test('有効な認証情報でログイン', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('メールアドレス').fill('user@example.com');
await page.getByLabel('パスワード').fill('SecurePass123');
await page.getByRole('button', { name: 'ログイン' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('ようこそ')).toBeVisible();
});
test('不正なパスワードでログイン失敗', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('メールアドレス').fill('user@example.com');
await page.getByLabel('パスワード').fill('wrong');
await page.getByRole('button', { name: 'ログイン' }).click();
await expect(page.getByText('メールアドレスまたはパスワードが正しくありません')).toBeVisible();
await expect(page).toHaveURL('/login');
});
});
Cypress
CypressはJavaScript E2Eテストフレームワークで、優れた開発者体験を提供する。
// Cypress テスト例
describe('ショッピングカート機能', () => {
beforeEach(() => {
cy.visit('/products');
});
it('商品をカートに追加', () => {
cy.get('[data-testid="product-card"]').first().within(() => {
cy.get('[data-testid="add-to-cart"]').click();
});
cy.get('[data-testid="cart-badge"]').should('have.text', '1');
cy.get('[data-testid="cart-icon"]').click();
cy.get('[data-testid="cart-item"]').should('have.length', 1);
});
it('カート内で数量変更', () => {
cy.get('[data-testid="product-card"]').first()
.find('[data-testid="add-to-cart"]').click();
cy.get('[data-testid="cart-icon"]').click();
cy.get('[data-testid="quantity-increase"]').click();
cy.get('[data-testid="quantity-input"]').should('have.value', '2');
});
});
フレイキー(Flaky)テストへの対処
フレイキーテストとは、同じコードなのに成功したり失敗したりするテストのことだ。E2Eテストで特に頻繁に発生する。
原因と対策:
- タイミング問題: 明示的な待機(explicit wait)を使用する。
sleepではなく特定の条件を待つ - テスト間の依存: 各テストを独立させる。共有状態を排除する
- ネットワーク不安定: APIレスポンスをモックするか、リトライロジックを追加する
- 動的データ: テストデータを固定するか、パターンマッチングを使用する
// Playwright - フレイキーテスト防止パターン
test('データロード後にリストを表示', async ({ page }) => {
await page.goto('/users');
// 悪い例:固定時間の待機
// await page.waitForTimeout(3000);
// 良い例:特定の要素が表示されるまで待機
await page.waitForSelector('[data-testid="user-list"]');
const users = page.getByTestId('user-item');
await expect(users).toHaveCount(10);
});
5. TDD(テスト駆動開発)
TDDはKent Beckが体系化した開発方法論で、テストを先に書き、そのテストをパスさせるコードを書く方式だ。
Red-Green-Refactorサイクル
Red(失敗): まだ存在しない機能に対するテストを書く。当然失敗する。
Green(成功): テストをパスさせる最小限のコードを書く。コードの優雅さは考慮しない。
Refactor(改善): テストがパスする状態を維持しながらコードを改善する。重複排除、ネーミング改善、構造変更などを行う。
実践例:パスワードバリデータ
# Step 1: Red - 失敗するテストを書く
def test_password_minimum_length():
validator = PasswordValidator()
assert validator.validate("short") == False
def test_password_requires_uppercase():
validator = PasswordValidator()
assert validator.validate("longpassword1") == False
def test_password_requires_number():
validator = PasswordValidator()
assert validator.validate("LongPassword") == False
def test_valid_password():
validator = PasswordValidator()
assert validator.validate("SecurePass123") == True
# Step 2: Green - テストをパスさせるコード
class PasswordValidator:
def validate(self, password: str) -> bool:
if len(password) < 8:
return False
if not any(c.isupper() for c in password):
return False
if not any(c.isdigit() for c in password):
return False
return True
# Step 3: Refactor - コード改善
from dataclasses import dataclass
from typing import List, Callable
@dataclass
class ValidationRule:
check: Callable[[str], bool]
message: str
class PasswordValidator:
def __init__(self):
self.rules: List[ValidationRule] = [
ValidationRule(
check=lambda p: len(p) >= 8,
message="Password must be at least 8 characters"
),
ValidationRule(
check=lambda p: any(c.isupper() for c in p),
message="Password must contain uppercase letter"
),
ValidationRule(
check=lambda p: any(c.isdigit() for c in p),
message="Password must contain a digit"
),
]
def validate(self, password: str) -> bool:
return all(rule.check(password) for rule in self.rules)
def get_errors(self, password: str) -> List[str]:
return [
rule.message
for rule in self.rules
if not rule.check(password)
]
TDDの核心原則
- 一度に一つのテストだけ失敗させる: 複数のテストを一度に書かない
- 最も単純な実装から始める: ハードコーディングから始めても構わない
- テストがコードを導く: テストなしにプロダクションコードを書かない
- 小さなステップで前進する: 大きな飛躍より小さな反復が安全だ
6. BDD(振る舞い駆動開発)
BDDはDan Northが提唱した方法論で、ビジネス要件を自然言語に近い形で記述し、実行可能なテストに変換する。
Given-When-Thenパターン
BDDではシナリオを3つの部分に分けて記述する。
Given(前提条件): テストの前提条件を記述する。
When(アクション): テストする行動を記述する。
Then(結果): 期待する結果を記述する。
Cucumberの例
# features/login.feature
Feature: ユーザーログイン
ユーザーが正しい認証情報でシステムにログインできる
Scenario: 成功するログイン
Given メールアドレス "user@example.com" で登録されたユーザーがいる
And パスワードは "SecurePass123" である
When ログインページで正しい認証情報でログインする
Then ダッシュボードページに遷移する
And 「ようこそ」メッセージが表示される
Scenario: 不正なパスワードでのログイン失敗
Given メールアドレス "user@example.com" で登録されたユーザーがいる
When 不正なパスワード "wrongpass" でログインする
Then ログインページに留まる
And エラーメッセージが表示される
Scenario Outline: パスワードポリシーの検証
When パスワード "<password>" で会員登録を試みる
Then 結果は "<result>" である
Examples:
| password | result |
| short | 失敗 |
| NoDigitHere | 失敗 |
| secure123Pass | 成功 |
# step_definitions/login_steps.py
from behave import given, when, then
@given('メールアドレス "{email}" で登録されたユーザーがいる')
def step_user_exists(context, email):
context.user = create_test_user(email=email)
@when('ログインページで正しい認証情報でログインする')
def step_login_with_valid_credentials(context):
context.response = context.client.post('/login', json={
'email': context.user.email,
'password': context.user.password,
})
@then('ダッシュボードページに遷移する')
def step_redirected_to_dashboard(context):
assert context.response.headers['Location'] == '/dashboard'
BDDの価値
BDDは技術チームとビジネスチーム間のコミュニケーションツールだ。Gherkin形式のシナリオは開発者でない人も読んで理解できる。これにより要件の誤解を減らし、受入条件を明確に定義できる。
7. パフォーマンステスト
パフォーマンステストはシステムが負荷状況でどのように動作するか検証する。
パフォーマンステストの種類
負荷テスト(Load Test): 予想トラフィックレベルでシステムが正常に動作するか検証する。
ストレステスト(Stress Test): システムの限界点を見つけるために段階的に負荷を増加させる。
スパイクテスト(Spike Test): 突発的なトラフィック急増にシステムがどう反応するか検証する。
耐久テスト(Soak Test): 長時間にわたり持続的な負荷をかけ、メモリリークなどの問題を検出する。
k6を活用した負荷テスト
k6はGrafana Labsが開発した現代的な負荷テストツールだ。JavaScriptでテストを書き、CLIから実行する。
// load-test.js - k6 負荷テスト
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('errors');
const responseTime = new Trend('response_time');
export const options = {
stages: [
{ duration: '2m', target: 100 }, // 2分で100ユーザーまで増加
{ duration: '5m', target: 100 }, // 5分間100ユーザーを維持
{ duration: '2m', target: 200 }, // 2分で200ユーザーまで増加
{ duration: '5m', target: 200 }, // 5分間200ユーザーを維持
{ duration: '2m', target: 0 }, // 2分で0ユーザーまで減少
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95%のリクエストが500ms未満
errors: ['rate<0.01'], // エラー率1%未満
},
};
export default function () {
const loginRes = http.post('https://api.example.com/login', JSON.stringify({
username: 'testuser',
password: 'testpass',
}), {
headers: { 'Content-Type': 'application/json' },
});
const success = check(loginRes, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'has auth token': (r) => r.json('token') !== undefined,
});
errorRate.add(!success);
responseTime.add(loginRes.timings.duration);
if (loginRes.status === 200) {
const token = loginRes.json('token');
const profileRes = http.get('https://api.example.com/profile', {
headers: { Authorization: `Bearer ${token}` },
});
check(profileRes, {
'profile status is 200': (r) => r.status === 200,
});
}
sleep(1);
}
JMeter
Apache JMeterは長い歴史を持つパフォーマンステストツールだ。GUIベースでテスト計画を作成できるため、非開発者でも使用できる。ただしk6と比較するとリソース消費が大きく、スクリプト作成が煩雑な面がある。
k6 vs JMeter比較:
| 項目 | k6 | JMeter |
|---|---|---|
| スクリプト言語 | JavaScript | XML(GUI) |
| リソース効率 | Goベース、軽量 | Javaベース、重い |
| CI/CD統合 | CLIベースで容易 | 可能だが設定が複雑 |
| プロトコルサポート | HTTP、WebSocket、gRPC | 非常に多様 |
| 分散実行 | k6 Cloud | 別途設定が必要 |
| 学習コスト | 低い | 中程度 |
8. セキュリティテスト
セキュリティテストはアプリケーションの脆弱性を事前に発見し対処するプロセスだ。
OWASP ZAP
OWASP ZAP(Zed Attack Proxy)はオープンソースのWebアプリケーションセキュリティスキャナだ。CI/CDパイプラインに統合して自動的にセキュリティスキャンを実行できる。
# GitHub ActionsでZAPスキャンを実行
name: Security Scan
on:
pull_request:
branches: [main]
jobs:
zap-scan:
runs-on: ubuntu-latest
steps:
- name: Start Application
run: docker compose up -d
- name: ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: 'http://localhost:3000'
rules_file_name: '.zap-rules.tsv'
cmd_options: '-a -j -l WARN'
- name: Upload Report
uses: actions/upload-artifact@v4
if: always()
with:
name: zap-report
path: report_html.html
ファジング(Fuzzing)
ファジングはランダムまたは半ランダムな入力をプログラムに注入し、予期しない動作やクラッシュを誘発するテスト手法だ。
# Python - Hypothesisを活用したプロパティベーステスト
from hypothesis import given, strategies as st
import json
@given(st.text())
def test_json_roundtrip(s):
"""どんな文字列でもJSONエンコード/デコード後に元の値と一致すべき"""
encoded = json.dumps(s)
decoded = json.loads(encoded)
assert decoded == s
@given(st.integers(min_value=0, max_value=1000))
def test_discount_never_exceeds_price(price):
"""割引後の価格がマイナスになってはならない"""
discount = calculate_discount(price, percentage=50)
assert discount >= 0
assert discount <= price
依存関係の脆弱性スキャン
サードパーティライブラリの既知の脆弱性を検出することもセキュリティテストの重要な部分だ。
# GitHub Actions - 依存関係脆弱性スキャン
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: npm audit
run: npm audit --audit-level=high
- name: Python safety check
run: pip install safety && safety check
9. カオスエンジニアリング
カオスエンジニアリングはプロダクション環境に意図的に障害を注入し、システムの復元力を検証する実験手法だ。Netflixが先駆者的な役割を果たし、現在は多くの大規模サービスで導入されている。
カオスエンジニアリングの原則
- 定常状態に関する仮説を立てる: システムが正常に動作する状態を定義する
- 現実世界のイベントをシミュレートする: サーバー障害、ネットワーク遅延、ディスク不足などを注入する
- 本番環境で実験する: ステージングではなく実際の環境でテストしてこそ真の復元力を検証できる
- 爆発半径を最小化する: 実験がユーザーに与える影響を最小限に抑える
- 自動化する: 継続的に実験を実施する
Chaos Monkey
Netflixが開発したChaos Monkeyは、ランダムにプロダクションインスタンスを終了させる。これによりインスタンス一つがダウンしてもサービスが正常に動作するか検証する。
Simian Armyというより広範なツールセットもある。Latency Monkey(ネットワーク遅延注入)、Conformity Monkey(非標準インスタンスの終了)、Chaos Gorilla(アベイラビリティゾーン全体の障害シミュレーション)などが含まれる。
LitmusChaos
LitmusChaosはKubernetes環境に特化したカオスエンジニアリングプラットフォームだ。CNCFプロジェクトとしてクラウドネイティブ環境で広く使用されている。
# LitmusChaos - Pod削除実験
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
name: pod-delete-chaos
namespace: production
spec:
appinfo:
appns: 'production'
applabel: 'app=payment-service'
appkind: 'deployment'
engineState: 'active'
chaosServiceAccount: litmus-admin
experiments:
- name: pod-delete
spec:
components:
env:
- name: TOTAL_CHAOS_DURATION
value: '30'
- name: CHAOS_INTERVAL
value: '10'
- name: FORCE
value: 'false'
probe:
- name: payment-health-check
type: httpProbe
httpProbe/inputs:
url: 'http://payment-service:8080/health'
method:
get:
criteria: '=='
responseCode: '200'
mode: Continuous
runProperties:
probeTimeout: 5
interval: 2
retry: 3
# LitmusChaos - ネットワーク遅延実験
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
name: network-delay-chaos
spec:
appinfo:
appns: 'production'
applabel: 'app=order-service'
appkind: 'deployment'
engineState: 'active'
chaosServiceAccount: litmus-admin
experiments:
- name: pod-network-latency
spec:
components:
env:
- name: NETWORK_LATENCY
value: '2000'
- name: TOTAL_CHAOS_DURATION
value: '60'
- name: NETWORK_INTERFACE
value: 'eth0'
ゲームデイ(Game Day)
ゲームデイはチーム全体が参加して障害シナリオを実行し、対応を練習する演習だ。
ゲームデイの進行手順:
- 計画: テストする障害シナリオを選定する(例:プライマリデータベース障害)
- 仮説設定: 障害発生時に予想されるシステム動作を定義する
- 実行: 障害を注入し、システムの反応を観察する
- 観察: モニタリングダッシュボード、アラート、ログをリアルタイムで確認する
- 分析: 仮説と実際の結果を比較し、改善点を導き出す
- 改善: 発見された弱点を修正し、次のゲームデイを計画する
ゲームデイのシナリオ例:
- プライマリデータベースのフェイルオーバーが30秒以内に完了するか
- あるサービスがダウンした際、依存サービスが適切にフォールバックするか
- CDNが障害を起こしたらオリジンサーバーがトラフィックを処理できるか
- デプロイ中にロールバックが必要な状況で自動ロールバックが動作するか
10. テスト自動化とCI/CD統合
すべてのテスト戦略はCI/CDパイプラインに統合されてこそ価値を発揮する。
テストパイプライン設計
# GitHub Actions - 全体テストパイプライン
name: Test Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Unit Tests
run: |
npm ci
npm run test:unit -- --coverage
- name: Upload Coverage
uses: codecov/codecov-action@v4
integration-tests:
runs-on: ubuntu-latest
needs: unit-tests
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Run Integration Tests
run: |
npm ci
npm run test:integration
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
e2e-tests:
runs-on: ubuntu-latest
needs: integration-tests
steps:
- uses: actions/checkout@v4
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run E2E Tests
run: npm run test:e2e
- name: Upload Test Report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
security-scan:
runs-on: ubuntu-latest
needs: unit-tests
steps:
- uses: actions/checkout@v4
- name: Run Security Scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
severity: 'CRITICAL,HIGH'
performance-test:
runs-on: ubuntu-latest
needs: integration-tests
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Run k6 Load Test
uses: grafana/k6-action@v0.3.1
with:
filename: tests/performance/load-test.js
テスト戦略マトリクス
| テスト種類 | 実行タイミング | 実行頻度 | 実行時間 | ブロッキング |
|---|---|---|---|---|
| ユニットテスト | すべてのPR/コミット | 非常に頻繁 | 秒〜分 | 必ずブロック |
| 統合テスト | すべてのPR | 頻繁 | 分 | 必ずブロック |
| E2Eテスト | PR、マージ | 毎日 | 分〜時間 | 主要フローをブロック |
| パフォーマンステスト | マージ後 | 週次 | 時間 | 閾値超過時のみ |
| セキュリティテスト | PR、夜間 | 毎日 | 分 | CRITICALをブロック |
| カオステスト | ゲームデイ | 月次 | 時間 | 非ブロック(観察) |
おわりに:テスト文化を作ることが核心
ツールやテクニックは重要だが、最も重要なのはテストを当然のこととする文化を作ることだ。
テストを技術的負債と見なさない。 テスト作成の時間は投資であり、コストではない。
テストをコードレビューの必須項目にする。 新機能にテストがなければレビューを通過させない。
失敗するテストを放置しない。 壊れたテストをそのままにするとテストへの信頼が崩壊する。
テストインフラに投資する。 CI/CDパイプライン、テスト環境、テストデータ管理に時間とリソースを投入する。
テストはソフトウェア品質の最終防衛線ではなく、最初の攻撃ラインだ。優れたテスト戦略は開発速度を上げ、バグを減らし、チームの自信を育む。ユニットテストから始めて、段階的にテスト範囲を広げていこう。