0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hybrid License System Day 22: テスト戦略

Last updated at Posted at 2025-12-21

🎄 科学と神々株式会社 アドベントカレンダー 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.

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?