🎄 科学と神々株式会社 アドベントカレンダー 2025
Hybrid License System Day 22: テスト戦略
統合・デプロイ編 (2/5)
📖 はじめに
Day 22では、テスト戦略を学びます。統合テスト(Jest)、E2Eテスト、パフォーマンステスト、セキュリティテストを実装しましょう。
🎯 テストピラミッド
テストの種類と配分
/\
/ \ E2E Tests (10%)
/----\ UI/統合テスト
/------\ Integration Tests (20%)
/--------\ API統合テスト
/----------\ Unit Tests (70%)
/------------\ 単体テスト
各テストレベルの役割
1. Unit Tests(単体テスト)
- 個別関数・メソッドのテスト
- 高速・大量実行
- カバレッジ目標: 80%+
2. Integration Tests(統合テスト)
- コンポーネント間連携
- データベース接続
- API呼び出し
3. E2E Tests(エンドツーエンドテスト)
- ユーザーシナリオ
- ブラウザ自動化
- 実環境に近い検証
4. Performance Tests(パフォーマンステスト)
- 負荷テスト
- レスポンスタイム測定
- ボトルネック発見
5. Security Tests(セキュリティテスト)
- 脆弱性スキャン
- 認証・認可テスト
- SQLインジェクション対策検証
🧪 Jest設定
package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:integration": "jest --testMatch='**/*.integration.test.js'",
"test:e2e": "jest --testMatch='**/*.e2e.test.js'"
},
"devDependencies": {
"jest": "^29.7.0",
"supertest": "^6.3.3",
"@types/jest": "^29.5.8"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": [
"/node_modules/",
"/tests/"
],
"collectCoverageFrom": [
"src/**/*.js",
"!src/index.js"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
🔬 ユニットテスト
AuthService単体テスト
// auth-service/tests/unit/authService.test.js
const AuthService = require('../../src/authService');
const DBService = require('../../src/dbService');
const CryptoService = require('../../src/cryptoService');
// モック化
jest.mock('../../src/dbService');
jest.mock('../../src/cryptoService');
describe('AuthService', () => {
let authService;
let mockDB;
let mockCrypto;
beforeEach(() => {
mockDB = new DBService();
mockCrypto = new CryptoService();
authService = new AuthService();
authService.db = mockDB;
authService.crypto = mockCrypto;
});
describe('validateActivationInput', () => {
it('should throw error if email is missing', () => {
expect(() => {
authService.validateActivationInput(null, 'password', 'client');
}).toThrow('Missing required fields');
});
it('should throw error for invalid email format', () => {
expect(() => {
authService.validateActivationInput('invalid-email', 'password', 'client');
}).toThrow('Invalid email format');
});
it('should throw error if password is too short', () => {
expect(() => {
authService.validateActivationInput('test@example.com', 'short', 'client');
}).toThrow('Password must be at least 8 characters');
});
it('should pass validation with valid inputs', () => {
expect(() => {
authService.validateActivationInput(
'test@example.com',
'SecurePassword123',
'valid-client-id-12345'
);
}).not.toThrow();
});
});
describe('authenticateUser', () => {
it('should authenticate user with correct credentials', async () => {
// モックセットアップ
const mockUser = {
user_id: 'user-123',
email: 'test@example.com',
password_hash: 'hashed-password',
plan: 'free'
};
mockDB.getUserByEmail.mockReturnValue(mockUser);
mockCrypto.verifyPassword = jest.fn().mockResolvedValue(true);
// 実行
const result = await authService.authenticateUser(
'test@example.com',
'correct-password'
);
// 検証
expect(result).toEqual(mockUser);
expect(mockDB.getUserByEmail).toHaveBeenCalledWith('test@example.com');
expect(mockCrypto.verifyPassword).toHaveBeenCalledWith(
'correct-password',
'hashed-password'
);
});
it('should throw error with incorrect password', async () => {
const mockUser = {
user_id: 'user-123',
email: 'test@example.com',
password_hash: 'hashed-password'
};
mockDB.getUserByEmail.mockReturnValue(mockUser);
mockCrypto.verifyPassword = jest.fn().mockResolvedValue(false);
await expect(
authService.authenticateUser('test@example.com', 'wrong-password')
).rejects.toThrow('Invalid credentials');
});
it('should throw error for non-existent user', async () => {
mockDB.getUserByEmail.mockReturnValue(null);
await expect(
authService.authenticateUser('nonexistent@example.com', 'password')
).rejects.toThrow('Invalid credentials');
});
});
});
🔗 統合テスト
API Gateway統合テスト
// api-gateway/tests/integration/gateway.integration.test.js
const request = require('supertest');
const app = require('../../src/app');
describe('API Gateway Integration Tests', () => {
describe('POST /api/v1/license/activate', () => {
it('should proxy activation request to Auth Service', async () => {
const response = await request(app)
.post('/api/v1/license/activate')
.send({
email: 'test@example.com',
password: 'TestPassword123',
client_id: 'integration-test-client'
})
.expect('Content-Type', /json/);
expect(response.status).toBeGreaterThanOrEqual(200);
expect(response.body).toHaveProperty('success');
});
it('should handle Auth Service unavailability', async () => {
// Auth Serviceを停止した状態でテスト
const response = await request(app)
.post('/api/v1/license/activate')
.send({
email: 'test@example.com',
password: 'password',
client_id: 'test'
});
expect(response.status).toBe(503);
expect(response.body.error).toContain('Service Unavailable');
});
});
describe('Rate Limiting', () => {
it('should enforce rate limit after 100 requests', async () => {
const promises = [];
// 101回リクエスト送信
for (let i = 0; i < 101; i++) {
promises.push(
request(app).get('/api/v1/health')
);
}
const responses = await Promise.all(promises);
// 最後のリクエストは429 Too Many Requestsのはず
const lastResponse = responses[100];
expect(lastResponse.status).toBe(429);
});
});
describe('Health Check', () => {
it('should return healthy status when all services are up', async () => {
const response = await request(app)
.get('/health')
.expect(200);
expect(response.body).toMatchObject({
status: 'healthy',
services: expect.objectContaining({
auth: 'healthy',
admin: 'healthy'
})
});
});
});
});
🌐 E2Eテスト
Playwright E2Eテスト
// e2e/tests/admin-dashboard.e2e.test.js
const { test, expect } = require('@playwright/test');
test.describe('Admin Dashboard E2E', () => {
test.beforeEach(async ({ page }) => {
// ログイン
await page.goto('http://localhost:3002/login');
await page.fill('input[name="email"]', 'admin@example.com');
await page.fill('input[name="password"]', 'admin123');
await page.click('button[type="submit"]');
// ダッシュボード表示を待つ
await page.waitForSelector('.dashboard');
});
test('should display overview statistics', async ({ page }) => {
// 統計カードが表示されるか
const totalUsers = await page.locator('.stat-card:has-text("Total Users")');
await expect(totalUsers).toBeVisible();
const activeLicenses = await page.locator('.stat-card:has-text("Active Licenses")');
await expect(activeLicenses).toBeVisible();
});
test('should navigate to user list', async ({ page }) => {
await page.click('a:has-text("Users")');
await page.waitForSelector('.user-list');
// ユーザーテーブルが表示されるか
const table = await page.locator('table.user-table');
await expect(table).toBeVisible();
});
test('should search users by email', async ({ page }) => {
await page.goto('http://localhost:3002/users');
// 検索入力
await page.fill('input[name="search"]', 'test@example.com');
await page.waitForTimeout(500); // デバウンス待ち
// 結果確認
const rows = await page.locator('table.user-table tbody tr').count();
expect(rows).toBeGreaterThan(0);
const firstEmail = await page.locator('table.user-table tbody tr:first-child td:nth-child(2)').textContent();
expect(firstEmail).toContain('test@example.com');
});
test('should delete user', async ({ page }) => {
await page.goto('http://localhost:3002/users');
// 削除ボタンクリック
await page.click('table.user-table tbody tr:first-child button:has-text("Delete")');
// 確認ダイアログ
page.on('dialog', dialog => dialog.accept());
// 削除完了を待つ
await page.waitForTimeout(1000);
// リロードして確認
await page.reload();
});
});
⚡ パフォーマンステスト
Apache Benchによる負荷テスト
#!/bin/bash
# performance-test.sh
echo "=== API Gateway Performance Test ==="
# 同時接続数100、合計1000リクエスト
ab -n 1000 -c 100 \
-H "Content-Type: application/json" \
-p payload.json \
http://localhost:3000/api/v1/license/validate
echo ""
echo "=== Auth Service Performance Test ==="
ab -n 1000 -c 100 \
-H "Content-Type: application/json" \
-H "X-Service-Secret: your-secret" \
-p activation.json \
http://localhost:3001/activate
Jestによるパフォーマンステスト
// tests/performance/response-time.test.js
describe('Performance Tests', () => {
it('should activate license within 500ms', async () => {
const startTime = Date.now();
await request(app)
.post('/api/v1/license/activate')
.send({
email: 'perf-test@example.com',
password: 'TestPassword123',
client_id: 'perf-test-client'
});
const duration = Date.now() - startTime;
expect(duration).toBeLessThan(500);
});
it('should handle 100 concurrent activations', async () => {
const promises = [];
for (let i = 0; i < 100; i++) {
promises.push(
request(app)
.post('/api/v1/license/activate')
.send({
email: `user${i}@example.com`,
password: 'password',
client_id: `client-${i}`
})
);
}
const startTime = Date.now();
const results = await Promise.all(promises);
const duration = Date.now() - startTime;
// 100並行処理が5秒以内
expect(duration).toBeLessThan(5000);
// 全て成功
const successCount = results.filter(r => r.status === 200).length;
expect(successCount).toBeGreaterThan(95); // 95%成功率
});
});
🔒 セキュリティテスト
SQLインジェクション対策テスト
// tests/security/sql-injection.test.js
describe('SQL Injection Protection', () => {
it('should reject SQL injection in email field', async () => {
const response = await request(app)
.post('/api/v1/license/activate')
.send({
email: "'; DROP TABLE users; --",
password: 'password',
client_id: 'test'
});
expect(response.status).toBe(400);
expect(response.body.error).toContain('Invalid email');
});
it('should reject SQL injection in client_id', async () => {
const response = await request(app)
.post('/api/v1/license/activate')
.send({
email: 'test@example.com',
password: 'password',
client_id: "' OR '1'='1"
});
expect(response.status).toBe(400);
expect(response.body.error).toContain('suspicious');
});
});
XSS対策テスト
// tests/security/xss.test.js
describe('XSS Protection', () => {
it('should sanitize HTML in user input', async () => {
const response = await request(app)
.post('/api/admin/users')
.set('Authorization', `Bearer ${adminToken}`)
.send({
email: '<script>alert("xss")</script>@example.com',
password: 'password',
plan: 'free'
});
expect(response.status).toBe(400);
});
});
📊 カバレッジレポート
カバレッジ実行
npm run test:coverage
期待される結果
--------------------|---------|----------|---------|---------|
File | % Stmts | % Branch | % Funcs | % Lines |
--------------------|---------|----------|---------|---------|
All files | 85.2 | 82.5 | 88.1 | 84.9 |
authService.js | 92.3 | 87.5 | 95.0 | 91.8 |
dbService.js | 88.7 | 85.0 | 90.5 | 88.2 |
cryptoService.js | 78.5 | 72.3 | 80.0 | 77.9 |
--------------------|---------|----------|---------|---------|
🎯 次のステップ
Day 23では、CI/CDパイプラインを学びます。GitHub Actions設定、自動テスト実行、Dockerイメージビルド、デプロイ自動化について詳しく解説します。
🔗 関連リンク
次回予告: Day 23では、GitHub Actionsワークフローとデプロイ戦略を詳しく解説します!
Copyright © 2025 Gods & Golem, Inc. All rights reserved.