お知らせ フロントエンド バックエンド インフラ 品質保証 セキュリティ 製品 興味・関心 その他

2025.03.31

品質保証

単体テストカバレッジ取得の自動化

GitHub Actions, Slack APIを使用して単体テストのカバレッジ取得および公開を自動化しました

経緯

マーケライズの開発ではスクラム開発のスプリント終了時にフロントエンド、バックエンドの単体テストを実行しそのカバレッジを出力しています。今までは手動実行していましたが、時間がかかるため自動化を行うことにします。自動化にあたり、カバレージ取得頻度をスプリント終了からソースコードがmainブランチにマージされる度に変更します。

方法

  • ソースコードをGitHub管理していますので、カバレッジ取得はGitHub Actionsにて行います
  • 取得したカバレッジはSlackに投稿します

Slack APPの作成

  • Slackにアプリを追加します
  • 手順は省略します
  • アプリの権限は chat:write, files:writeを付与します
  • Bot Tokenを控えておきます

SlackチャンネルにAppを追加

  • Slackのチャンネルから「チャンネルの詳細を開く」をクリック
  • 「インテグレーション」タブから「アプリを追加する」をクリック
  • Slack APPの作成で作成したSlack APPを追加する

GitHubにSlack Bot TokenとSlackチャンネルIDを追加

Bot TokenをGithubActionsに追加します

  • GitHubの対象リポジトリのSettingsを開く
  • Secrets and variables > actions をクリック
  • Sectersタブの New Repository secretをクリックし以下を入力
NameSLACK_BOT_TOKEN
ValueSlackAPPの作成で控えておいたSlack bot tokenを入力
  • Add secretボタンをクリック
  • Variablesタブの New Repository variableをクリックし以下を入力

GitHubに投稿を行うSlackのチャンネルを伝えます。

  • Slackのチャンネルから「チャンネルの詳細を開く」をクリック
  • チャンネル情報末尾のチャンネルIDをコピー
  • GitHubの対象リポジトリのSettingsを開く
  • Secrets and variables > actions をクリック
  • Add variableボタンをクリックし以下を入力
NameSLACK_CHANNEL_ID
ValueコピーしたSlackのチャンネルID

GitHub ワークフロー

.github/workflowsにGitHubワークフローファイルを作成します。ファイル名は任意、拡張子は.ymlにします。以下ではフロントエンド向けのカバレッジ取得時のワークフローを記載します。

name: mainブランチにFrontEnd関連のリソースがpushされたときの処理

on:
  push:
    branches:
      - main
    paths:
      - "**.json"
      - "**.js"
      - "**.ts"
      - "**.jsx"
      - "**.tsx"
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
 # 関係のないjobは省略
  Coverage:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: frontend/working/directory

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

      - name: Setup Node.js environment
        uses: actions/setup-node@v4
        with:
          node-version: latest

      - name: Cache Dependency
        uses: actions/cache@v4
        id: node-cache
        env:
          cache-name: node-cache
        with:
          path: "**/node_modules"
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}

      - name: Install Dependency
        if: ${{ steps.node-cache.outputs.cache-hit != 'true' }}
        run: npm ci --no-audit --progress=false --silent

      - name: Unit Coverage
        run: npm run coverage

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage_results
          path: coverage/path

  PostCoverage:
    needs: Coverage
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: .github/actions
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js environment
        uses: actions/setup-node@v4
        with:
          node-version: latest

      - name: Cache Dependency
        uses: actions/cache@v4
        id: node-coverage-cache
        env:
          cache-name: node-coverage-cache
        with:
          path: "**/node_modules"
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}

      - name: Install Dependency
        if: ${{ steps.node-coverage-cache.outputs.cache-hit != 'true' }}
        run: npm ci --no-audit --progress=false

      - name: download coverage
        uses: actions/download-artifact@v4
        with:
          name: coverage_results
          path: coverage

      - name: Post coverage
        uses: ./.github/actions/post_coverage_report
        with:
          kind: FE
          token: ${{ secrets.SLACK_BOT_TOKEN }}
          channel_id: ${{ vars.SLACK_CHANNEL_ID }}

注意点として、GitHub Actionsで使用するファイルはメインとなるプログラムとは別のプロジェクトとして管理する必要があります。そのため上記のようにジョブを分け、取得したカバレッジをアップロード、ダウンロードするようにしています。

カバレッジ投稿スクリプトの作成

ワークフローからの指示によりSlackにカバレッジ情報を投稿するGitHub Actionsを作成します。カバレッジ情報のうち、summaryはメッセージとして、詳細部分は添付ファイルとしてSlackに投稿します。 .github/actions/post_coverage_report ディレクトリにファイルを作成していきます

.github/actions/post_coverage_report/action.yml

name: カバレッジレポートの投稿
description: 作成されたフロントエンド向けカバレッジレポートをslackに投稿する
inputs:
  token:
    description: Slack Bot Token
    required: true
  channel_id:
    description: Slack Channel ID
    required: true
  kind:
    description: Coverage kind
    required: true
runs:
  using: "node20"
  main: "index.mjs"

.github/actions/post_coverage_report/index.mjs

import { getInput, notice, setFailed } from "@actions/core";
import { WebClient } from "@slack/web-api";
import { createReadStream, existsSync, readFileSync } from "fs";
import { basename, join } from "node:path";
import { cwd } from "process";

const [token, channel_id, kind] = ["token", "channel_id", "kind"].map(
  (key) => getInput(key) ?? setFailed(`no ${key}.`),
);

try {
  const { summary_path, coverage_path } = await getParameters(kind);

  if (summary_path && !existsSync(summary_path)) {
    throw Error(`no path exists: ${summary_path}`);
  }

  if (!existsSync(coverage_path)) {
    throw Error(`no path exists: ${coverage_path}`);
  }

  const comment = [
    `# ${kind}カバレージ取得結果`,
    summary_path && existsSync(summary_path) ?
      readFileSync(summary_path).toString()
    : undefined,
  ]
    .flatMap((text) => text ?? [])
    .join("\n");

  notice(comment);

  const { files } = new WebClient(token);
  await files.uploadV2({
    filename: basename(coverage_path),
    channel_id,
    initial_comment: comment,
    file: createReadStream(coverage_path),
  });

  notice("uploaded.");

} catch (e) {
  setFailed(e);
  process.exit();
}

function getParameters(kind) {
  switch (kind.toUpperCase()) {
    case "FE":
      return getFrontendParameters();
    case "BE":
      return getBackendParameters();
  }
  throw Error(`unknown kind: ${kind}`);
}

async function getFrontendParameters() {
  const config = await import("path/to/jsconfig/jest.config.js");
  const reporters = new Map(config.default.coverageReporters);
  const dir = config.default.coverageDirectory.replace("<rootDir>", cwd());
  return {
    summary_path: join(dir, reporters.get("text-summary")?.file),
    coverage_path: join(dir, reporters.get("text")?.file),
  };
}

function getBackendParameters() {
  return {
    summary_path: undefined,
    coverage_path: join(cwd(), "coverage", "coverage.txt"),
  };
}

これでGitHubのmainにマージされる度にSlackにカバレージ情報が投稿されるようになりました。

参考:

中村

中村

記事一覧

中村です。
株式会社マーケライズでマーケティングオートメーション(MA)のMRCを開発しています。
現在はMRCのフロントエンド刷新のため、Reactを使用した開発に切磋琢磨しています。
フルリモートです。
枯れたと思っていた植木を半年くらい放っておいたら、梅雨の雨で復活しました。