T-CREATOR

Jest を可観測化する:JUnit/SARIF/OpenTelemetry で CI ダッシュボードを構築

Jest を可観測化する:JUnit/SARIF/OpenTelemetry で CI ダッシュボードを構築

テストフレームワークの結果を眺めるだけでは、チーム全体のテスト品質やパフォーマンスを俯瞰することはできません。Jest のテスト結果を JUnit、SARIF、OpenTelemetry といった標準フォーマットで出力し、CI ダッシュボードで可視化すれば、テストの成功率や実行時間、セキュリティの脆弱性まで一元管理できるようになります。

本記事では、Jest を可観測化するための具体的な手法を解説していきます。JUnit XML でテスト結果を収集し、SARIF でセキュリティレポートを統合し、OpenTelemetry でパフォーマンスをトレースする方法を、段階的に説明していきましょう。

背景

テスト結果の可観測性とは

可観測性(Observability)とは、システムの内部状態を外部から観測できる度合いを指します。テストにおける可観測性とは、テストの実行結果、パフォーマンス、品質指標を継続的に収集・可視化し、問題の早期発見や改善を可能にすることです。

Jest は標準でコンソールにテスト結果を出力しますが、以下のような情報を構造化された形式で保存・分析することで、より深い洞察を得られます。

mermaidflowchart TB
  jest["Jest テスト実行"] -->|"標準出力"| console["コンソール<br/>ログ"]
  jest -->|"JUnit XML"| junit["JUnit レポート<br/>(成功/失敗)"]
  jest -->|"SARIF"| sarif["SARIF レポート<br/>(脆弱性)"]
  jest -->|"OpenTelemetry"| otel["トレース<br/>(パフォーマンス)"]

  junit --> dashboard["CI<br/>ダッシュボード"]
  sarif --> dashboard
  otel --> dashboard

  dashboard --> insights["洞察<br/>・傾向分析<br/>・品質向上"]

上記の図が示すように、Jest の実行結果を複数のフォーマットで出力することで、テストの成功率、脆弱性、パフォーマンスを一元的に管理できます。

標準フォーマットの重要性

JUnit XML、SARIF、OpenTelemetry は、いずれも業界標準のフォーマットです。これらを採用することで、以下のメリットがあります。

#フォーマット用途主な利点
1JUnit XMLテスト結果の記録CI ツール(Jenkins、GitHub Actions など)との統合が容易
2SARIF静的解析・脆弱性レポートセキュリティスキャン結果の標準化、GitHub Security タブとの連携
3OpenTelemetry分散トレース・メトリクスパフォーマンス分析、ボトルネック特定、APM ツールとの統合

これらのフォーマットを活用すれば、異なるツールやプラットフォーム間でデータを共有し、一貫性のある可視化が実現できます。

課題

テスト結果の散在

Jest の標準出力は人間には読みやすいのですが、機械的に解析するには不向きです。CI 環境で複数のテストスイートを実行すると、結果が散在し、以下のような課題が生じます。

mermaidflowchart LR
  ci["CI パイプライン"] --> test1["テストスイート A"]
  ci --> test2["テストスイート B"]
  ci --> test3["テストスイート C"]

  test1 --> log1["ログ A"]
  test2 --> log2["ログ B"]
  test3 --> log3["ログ C"]

  log1 -.->|"散在"| issue["・結果の統合が困難<br/>・傾向分析ができない<br/>・履歴管理が煩雑"]
  log2 -.-> issue
  log3 -.-> issue

テスト結果が各ジョブのログに埋もれてしまい、過去の結果との比較や、失敗率の傾向を把握することが難しくなります。

セキュリティレポートの欠如

Jest でテストを実行しても、コード内の脆弱性や静的解析結果を統合的に管理する仕組みがありません。セキュリティスキャンツール(例:ESLint の security プラグイン、npm audit など)の結果を Jest のテストレポートと一緒に管理したい場合、SARIF フォーマットが有効です。

パフォーマンスの可視化不足

テストの実行時間はコンソールに表示されますが、個々のテストケースやセットアップ処理のボトルネックを特定するには不十分です。分散トレーシングを導入すれば、テスト実行フローを詳細に可視化できます。

解決策

JUnit XML レポートでテスト結果を収集

Jest は jest-junit というカスタムレポーターを使って、JUnit XML 形式でテスト結果を出力できます。このレポーターを導入すれば、CI ツールが結果を自動的にパースし、ダッシュボードに表示できるようになります。

導入手順

まず、必要なパッケージをインストールします。

bashyarn add --dev jest-junit

次に、Jest の設定ファイル(jest.config.js または package.json)に jest-junit を追加します。

javascript// jest.config.js
module.exports = {
  // 既存の設定...
  reporters: [
    'default', // 標準のコンソール出力
    'jest-junit', // JUnit XML レポーター
  ],
};

jest-junit の出力先や形式をカスタマイズする場合は、以下のように設定します。

javascript// jest.config.js
module.exports = {
  reporters: [
    'default',
    [
      'jest-junit',
      {
        outputDirectory: './reports', // 出力ディレクトリ
        outputName: 'junit.xml', // ファイル名
        classNameTemplate: '{classname}', // クラス名のテンプレート
        titleTemplate: '{title}', // テストタイトルのテンプレート
        ancestorSeparator: ' › ', // 階層の区切り文字
        usePathForSuiteName: 'true', // ファイルパスをスイート名に使用
      },
    ],
  ],
};

これで Jest を実行すると、.​/​reports​/​junit.xml にテスト結果が出力されます。

bashyarn jest

生成された junit.xml は以下のような構造を持ちます。

xml<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="jest tests" tests="10" failures="1" errors="0" time="2.345">
  <testsuite name="UserService" tests="5" failures="0" errors="0" time="1.234">
    <testcase classname="UserService › login" name="should authenticate user" time="0.234"/>
    <testcase classname="UserService › logout" name="should clear session" time="0.123"/>
  </testsuite>
  <testsuite name="OrderService" tests="5" failures="1" errors="0" time="1.111">
    <testcase classname="OrderService › createOrder" name="should create new order" time="0.456"/>
    <testcase classname="OrderService › cancelOrder" name="should cancel order" time="0.234">
      <failure message="Expected order status to be CANCELLED but got PENDING"/>
    </testcase>
  </testsuite>
</testsuites>

CI ツール(GitHub Actions、Jenkins、GitLab CI など)は、この XML ファイルを読み込んでテスト結果を可視化します。

GitHub Actions での活用例

GitHub Actions では、dorny​/​test-reporter などのアクションを使って、JUnit XML を自動的にパースできます。

yaml# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - run: yarn install
      - run: yarn jest

      # テストレポートを表示
      - name: Test Report
        uses: dorny/test-reporter@v1
        if: always() # テストが失敗しても実行
        with:
          name: Jest Tests
          path: reports/junit.xml
          reporter: jest-junit

これにより、プルリクエストのチェック欄にテスト結果が自動的に表示され、失敗したテストケースも一目で確認できます。

SARIF でセキュリティレポートを統合

SARIF(Static Analysis Results Interchange Format)は、静的解析ツールの結果を統一的に記録するための JSON フォーマットです。Jest 自体は SARIF を直接サポートしていませんが、ESLint や npm audit の結果を SARIF に変換し、Jest のテスト結果と一緒に管理できます。

ESLint の結果を SARIF に変換

ESLint は @microsoft​/​eslint-formatter-sarif を使って SARIF 形式で結果を出力できます。

bashyarn add --dev @microsoft/eslint-formatter-sarif

ESLint を実行し、結果を SARIF ファイルに保存します。

bashyarn eslint . --format @microsoft/eslint-formatter-sarif --output-file reports/eslint.sarif.json

生成された SARIF ファイルは以下のような構造を持ちます。

json{
  "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
  "version": "2.1.0",
  "runs": [
    {
      "tool": {
        "driver": {
          "name": "ESLint",
          "version": "8.50.0"
        }
      },
      "results": [
        {
          "ruleId": "no-unused-vars",
          "level": "warning",
          "message": {
            "text": "'unusedVar' is defined but never used."
          },
          "locations": [
            {
              "physicalLocation": {
                "artifactLocation": {
                  "uri": "src/userService.ts"
                },
                "region": {
                  "startLine": 42,
                  "startColumn": 7
                }
              }
            }
          ]
        }
      ]
    }
  ]
}

GitHub Security タブとの連携

GitHub Actions で SARIF ファイルをアップロードすると、リポジトリの Security タブに結果が表示されます。

yaml# .github/workflows/security.yml
name: Security Scan

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18
      - run: yarn install
      - run: yarn eslint . --format @microsoft/eslint-formatter-sarif --output-file reports/eslint.sarif.json

      # SARIF ファイルをアップロード
      - name: Upload SARIF
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: reports/eslint.sarif.json

これにより、プルリクエストのチェック欄や Security タブで脆弱性を確認でき、コードレビュー時にセキュリティ観点の指摘も可能になります。

npm audit の結果を SARIF に変換

npm audit の結果を SARIF に変換するには、npm-audit-sarif などのツールを使います。

bashyarn add --dev npm-audit-sarif

以下のスクリプトで npm audit の結果を SARIF 形式で出力します。

bashnpm audit --json | npx npm-audit-sarif > reports/npm-audit.sarif.json

これにより、依存パッケージの脆弱性も SARIF フォーマットで管理でき、GitHub Security タブに統合されます。

OpenTelemetry でパフォーマンスをトレース

OpenTelemetry は、分散トレーシングとメトリクス収集のための標準フレームワークです。Jest のテスト実行をトレースすれば、個々のテストケースの実行時間や、セットアップ処理のボトルネックを可視化できます。

OpenTelemetry の導入

まず、必要なパッケージをインストールします。

bashyarn add --dev @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http

次に、OpenTelemetry の初期化スクリプトを作成します。

javascript// tracing.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const {
  OTLPTraceExporter,
} = require('@opentelemetry/exporter-trace-otlp-http');
const {
  getNodeAutoInstrumentations,
} = require('@opentelemetry/auto-instrumentations-node');

// トレースエクスポーターの設定(Jaeger、Zipkin、OpenTelemetry Collector などに送信)
const traceExporter = new OTLPTraceExporter({
  url: 'http://localhost:4318/v1/traces', // OpenTelemetry Collector のエンドポイント
});

// SDK の初期化
const sdk = new NodeSDK({
  traceExporter,
  instrumentations: [getNodeAutoInstrumentations()], // 自動計装
  serviceName: 'jest-tests', // サービス名
});

// SDK を起動
sdk.start();

// プロセス終了時に SDK をシャットダウン
process.on('SIGTERM', () => {
  sdk
    .shutdown()
    .then(() => console.log('Tracing terminated'))
    .catch((error) =>
      console.log('Error terminating tracing', error)
    )
    .finally(() => process.exit(0));
});

Jest を実行する前に、このスクリプトをロードします。

bashnode -r ./tracing.js ./node_modules/.bin/jest

または、package.json のスクリプトに追加します。

json{
  "scripts": {
    "test:trace": "node -r ./tracing.js ./node_modules/.bin/jest"
  }
}

これで Jest のテスト実行がトレースされ、OpenTelemetry Collector に送信されます。

カスタムスパンの追加

個々のテストケースやセットアップ処理を詳細にトレースするには、カスタムスパンを追加します。

javascript// userService.test.ts
import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('jest-tests');

describe('UserService', () => {
  beforeAll(async () => {
    // セットアップ処理をトレース
    const span = tracer.startSpan(
      'beforeAll: setup database'
    );
    await setupDatabase();
    span.end();
  });

  test('should authenticate user', async () => {
    // テストケースをトレース
    const span = tracer.startSpan(
      'test: authenticate user'
    );
    const result = await userService.login(
      'user@example.com',
      'password'
    );
    expect(result.success).toBe(true);
    span.end();
  });
});

これにより、各テストケースの実行時間や依存関係が可視化され、ボトルネックを特定できます。

トレース結果の可視化

OpenTelemetry Collector から送信されたトレースデータは、Jaeger や Zipkin などの可視化ツールで確認できます。

以下は、Docker Compose で Jaeger を起動する例です。

yaml# docker-compose.yml
version: '3'
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - '16686:16686' # Jaeger UI
      - '4318:4318' # OTLP HTTP
    environment:
      - COLLECTOR_OTLP_ENABLED=true

Jaeger を起動します。

bashdocker-compose up -d

Jest を実行し、トレースデータを送信します。

bashyarn test:trace

ブラウザで http:​/​​/​localhost:16686 にアクセスすると、Jaeger UI でトレース結果を確認できます。各テストケースの実行時間やスパンの依存関係が視覚的に表示され、パフォーマンスのボトルネックを特定できます。

mermaidflowchart LR
  jest["Jest 実行"] -->|"トレース"| otel["OpenTelemetry<br/>SDK"]
  otel -->|"OTLP HTTP"| collector["OpenTelemetry<br/>Collector"]
  collector --> jaeger["Jaeger"]
  collector --> zipkin["Zipkin"]
  collector --> datadog["Datadog"]

  jaeger --> ui["UI で<br/>可視化"]
  zipkin --> ui
  datadog --> ui

この図が示すように、OpenTelemetry を使えば、複数のバックエンド(Jaeger、Zipkin、Datadog など)にトレースデータを送信でき、チームの好みに応じた可視化ツールを選択できます。

具体例

CI パイプラインでの統合例

ここでは、GitHub Actions を使って、JUnit XML、SARIF、OpenTelemetry を統合した CI パイプラインを構築します。

ワークフロー全体像

mermaidflowchart TB
  start["GitHub Actions<br/>開始"] --> checkout["コードを<br/>チェックアウト"]
  checkout --> install["依存関係<br/>インストール"]
  install --> parallel1["並列処理"]

  parallel1 --> lint["ESLint 実行<br/>(SARIF 出力)"]
  parallel1 --> audit["npm audit<br/>(SARIF 出力)"]
  parallel1 --> test["Jest 実行<br/>(JUnit XML 出力)"]
  parallel1 --> trace["Jest トレース<br/>(OpenTelemetry)"]

  lint --> upload_sarif["SARIF を<br/>GitHub にアップロード"]
  audit --> upload_sarif
  test --> upload_junit["JUnit XML を<br/>レポート"]
  trace --> upload_trace["トレースを<br/>Jaeger に送信"]

  upload_sarif --> dashboard["CI ダッシュボード<br/>で可視化"]
  upload_junit --> dashboard
  upload_trace --> dashboard

  dashboard --> done["完了"]

上記のフローに従って、GitHub Actions のワークフローを作成します。

ワークフローファイル

yaml# .github/workflows/ci.yml
name: CI Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test-and-analyze:
    runs-on: ubuntu-latest

    services:
      # Jaeger をサービスとして起動
      jaeger:
        image: jaegertracing/all-in-one:latest
        ports:
          - 16686:16686
          - 4318:4318
        options: >-
          --health-cmd "wget --no-verbose --tries=1 --spider http://localhost:16686 || exit 1"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'yarn'

      - name: Install dependencies
        run: yarn install --frozen-lockfile

次に、ESLint、npm audit、Jest を並列実行します。

yaml# ESLint を実行し、SARIF 形式で出力
- name: Run ESLint
  run: |
    mkdir -p reports
    yarn eslint . --format @microsoft/eslint-formatter-sarif --output-file reports/eslint.sarif.json
  continue-on-error: true # エラーがあっても続行

# npm audit を実行し、SARIF 形式で出力
- name: Run npm audit
  run: |
    npm audit --json | npx npm-audit-sarif > reports/npm-audit.sarif.json
  continue-on-error: true

# Jest を実行し、JUnit XML 形式で出力
- name: Run Jest
  run: yarn jest --ci --coverage
  env:
    JEST_JUNIT_OUTPUT_DIR: ./reports
    JEST_JUNIT_OUTPUT_NAME: junit.xml

# Jest をトレースモードで実行
- name: Run Jest with Tracing
  run: yarn test:trace
  env:
    OTEL_EXPORTER_OTLP_ENDPOINT: http://localhost:4318
    OTEL_SERVICE_NAME: jest-tests

最後に、各レポートをアップロードします。

yaml# SARIF ファイルを GitHub Security タブにアップロード
- name: Upload ESLint SARIF
  uses: github/codeql-action/upload-sarif@v2
  if: always()
  with:
    sarif_file: reports/eslint.sarif.json
    category: eslint

- name: Upload npm audit SARIF
  uses: github/codeql-action/upload-sarif@v2
  if: always()
  with:
    sarif_file: reports/npm-audit.sarif.json
    category: npm-audit

# JUnit XML レポートを表示
- name: Test Report
  uses: dorny/test-reporter@v1
  if: always()
  with:
    name: Jest Tests
    path: reports/junit.xml
    reporter: jest-junit

# カバレッジレポートをアップロード
- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v3
  with:
    files: ./coverage/coverage-final.json
    flags: unittests
    name: codecov-umbrella

このワークフローを実行すると、以下の結果が得られます。

#項目出力先確認方法
1ESLint の静的解析結果GitHub Security タブリポジトリ → Security → Code scanning alerts
2npm audit の脆弱性レポートGitHub Security タブリポジトリ → Security → Code scanning alerts
3Jest のテスト結果プルリクエストのチェック欄プルリクエスト → Checks → Jest Tests
4Jest のカバレッジCodecovCodecov のダッシュボード
5Jest のトレースJaeger UIhttp://localhost:16686 (ローカル環境)

ローカル環境での確認

CI 環境だけでなく、ローカル環境でも同じレポートを確認できます。以下は、Docker Compose を使って Jaeger を起動し、Jest のトレース結果を可視化する例です。

Docker Compose 設定

yaml# docker-compose.yml
version: '3'
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - '16686:16686' # Jaeger UI
      - '4318:4318' # OTLP HTTP
    environment:
      - COLLECTOR_OTLP_ENABLED=true

Jaeger を起動

bashdocker-compose up -d

Jest をトレースモードで実行

bashexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_SERVICE_NAME=jest-tests
yarn test:trace

Jaeger UI でトレースを確認

ブラウザで http:​/​​/​localhost:16686 にアクセスし、jest-tests サービスを選択すると、テスト実行のトレース結果が表示されます。各テストケースの実行時間や、セットアップ処理のボトルネックを視覚的に確認できます。

ダッシュボードの例

以下は、JUnit XML、SARIF、OpenTelemetry を統合した CI ダッシュボードのイメージです。

GitHub Actions のサマリー画面

yaml ESLint: 12 warnings, 0 errors
 npm audit: 0 vulnerabilities
 Jest Tests: 150 passed, 2 failed
   UserService  should handle invalid email
   OrderService  should cancel order
📊 Coverage: 85.3%
📈 Trace: View in Jaeger

GitHub Security タブ

javaCode scanning alerts
─────────────────────────────────────────
🔍 ESLint
  - no-unused-vars: 'unusedVar' is defined but never used (src/userService.ts:42)
  - @typescript-eslint/no-explicit-any: Unexpected any (src/orderService.ts:78)

🔒 npm audit
  - axios: Regular Expression Denial of Service (CVE-2023-12345)
  - lodash: Prototype Pollution (CVE-2023-67890)

Jaeger UI のトレースビュー

javajest-tests
├─ beforeAll: setup database (125ms)
├─ test: should authenticate user (45ms)
│  ├─ database query: SELECT * FROM users (20ms)
│  └─ password validation (15ms)
├─ test: should handle invalid email (12ms)
└─ afterAll: cleanup (34ms)

このように、JUnit XML、SARIF、OpenTelemetry を組み合わせることで、テスト結果、セキュリティ、パフォーマンスを一元的に管理できます。

まとめ

Jest を可観測化することで、テストの品質とパフォーマンスを継続的に改善できます。JUnit XML でテスト結果を構造化し、SARIF でセキュリティレポートを統合し、OpenTelemetry でパフォーマンスをトレースすることで、CI ダッシュボードでの一元管理が実現します。

本記事で紹介した手法

#フォーマット導入方法活用例
1JUnit XMLjest-junit レポーターを追加GitHub Actions でテスト結果を表示
2SARIFESLint、npm audit の結果を変換GitHub Security タブで脆弱性を確認
3OpenTelemetrySDK を初期化し、カスタムスパンを追加Jaeger でトレースを可視化

これらの手法を組み合わせることで、テストの成功率、セキュリティリスク、パフォーマンスのボトルネックを俯瞰でき、チーム全体の開発効率が向上します。

可観測性の導入は、一度設定すれば継続的に価値を生み出します。まずは JUnit XML から始めて、徐々に SARIF や OpenTelemetry を追加していくと良いでしょう。

関連リンク