【実務・中級編】GitHub Actionsワークフローを「ユニットテスト」する:Jestを活用したアクションコードの品質向上術 – バージョン管理・CI/CD活用バイブル

GitHub Actionsワークフローを「ユニットテスト」する:Jestを活用したアクションコードの品質向上術

こんにちは。テックリードの皆さん。
日々のCI/CDパイプラインの構築、本当にお疲れ様です。

「YAMLの構文エラーを見つけるためだけに、わざわざリモートリポジトリにプッシュして、CIが落ちるのを待つ」
……そんな不毛なループに、大切な開発時間を溶かしていませんか?

カスタムアクション(JavaScript/TypeScript)の規模が大きくなるにつれ、コードのロジックが複雑化し、デバッグのコストは跳ね上がります。本番環境(GitHubのランナー)でしか動かないコードなど、現代の高速な開発サイクルにおいては「技術的負債」でしかありません。

今回は、JestとGitHub Actions Toolkitを駆使して、カスタムアクションを完全にローカルでユニットテストし、開発スピードを極限まで高めるプロの手法を伝授します。

—

1. なぜカスタムアクションに「ユニットテスト」が必要なのか

多くのチームは、GitHub ActionsのYAMLを書くだけで満足しています。しかし、組織がスケールし、共通化されたコンポジットアクションやJavaScriptアクションが増えてくると、以下の問題が必ず発生します。

1. フィードバックループの崩壊: プッシュ $\rightarrow$ 実行 $\rightarrow$ 失敗のサイクルが遅すぎる。
2. 環境依存のバグ: `github.context` や環境変数 (`process.env.INPUT_`) のモック漏れによる本番爆発。
3. リファクタリングの恐怖: 共通アクションのコードを修正した際、他のどのリポジトリに影響が出るか分からない。

これらを解決する唯一の手段が、Jestによるアクションコードの完全なユニットテスト化です。

—

2. 開発環境の極限最適化(VSCode神プラグイン & ショートカット)

テスト駆動でアクションを開発するため、VSCodeの環境を研ぎ澄まします。

必須プラグイン

  • Jest Runner (`orta.vscode-jest-runner`): エディタ上のテストコードの横に「Play」ボタンを出現させ、単一のテストケースを0.1秒で即座に実行する。
  • GitHub Actions (`GitHub.vscode-actionlint`): アクションのYAMLに対する静的解析をリアルタイムで行う。

爆速開発のためのキーボードショートカット(macOS)

  • `Cmd + Shift + P` $\rightarrow$ `Jest: Toggle Coverage` : カバレッジを視覚化し、テスト漏れを瞬時に検知。
  • `Cmd + Option + T` : 直前に実行したテストケースの再実行(Jest Runner拡張機能のカスタムバインド推奨)。

—

3. 実践:Jest × Actions Toolkitによるテスト実装

ここでは、入力値を受け取り、GitHubのAPIを叩いてラベルを付与するようなJavaScriptアクションを想定します。

プロジェクト構成

.
├── action.yml
├── package.json
├── jest.config.js
├── src/
│ ├── index.js # エントリーポイント
│ └── main.js # ビジネスロジック
└── __tests__/
└── main.test.js # テストコード

① 依存関係のインストール

GitHub Actions Toolkitのコア機能 (`@actions/core`) やGitHub APIクライアント (`@actions/github`) をテストしやすくするためのモック環境を整えます。

npm install @actions/core @actions/github
npm install –save-dev jest @types/jest

② テスト対象のコード (`src/main.js`)

const core = require(‘@actions/core’);
const github = require(‘@actions/github’);

async function run() {
try {
// 入力値の取得
const token = core.getInput(‘github-token’, { required: true });
const tagName = core.getInput(‘tag-name’, { required: true });

if (!tagName.startsWith(‘v’)) {
throw new Error(‘タグ名は “v” で始まる必要があります。’);
}

// コンテキストの検証
const { owner, repo } = github.context.repo;
core.info(`Processing repository: ${owner}/${repo} for tag: ${tagName}`);

// 成功時の出力
core.setOutput(‘result’, ‘success’);
} catch (error) {
core.setFailed(error.message);
}
}

module.exports = { run };

③ ユニットテストの実装 (`__tests__/main.test.js`)

ここが本記事の核心です。`@actions/core` や `@actions/github` は、そのまま実行すると実環境の環境変数やAPIを要求するため、Jestで完全にモック化します。

/

  • @file main.test.js
  • @description GitHub Actionsの入力・出力・例外処理を検証するユニットテスト

/

const core = require(‘@actions/core’);
const github = require(‘@actions/github’);
const { run } = require(‘../src/main’);

// @actions/core と @actions/github のモック化
jest.mock(‘@actions/core’);
jest.mock(‘@actions/github’);

describe(‘Custom Action Unit Tests’, () => {
beforeEach(() => {
// テストごとにモックをリセット
jest.clearAllMocks();
});

test(‘正常系: 正しいプレフィックスのタグが渡された場合、処理が成功する’, async () => {
// 1. 入力値のモック設定
core.getInput.mockImplementation((name) => {
if (name === ‘github-token’) return ‘fake-token’;
if (name === ‘tag-name’) return ‘v1.0.0’;
});

// 2. GitHub Contextのモック設定
github.context = {
repo: { owner: ‘test-owner’, repo: ‘test-repo’ },
};

// 3. 実行
await run();

// 4. アサーション
expect(core.setFailed).not.toHaveBeenCalled();
expect(core.setOutput).toHaveBeenCalledWith(‘result’, ‘success’);
expect(core.info).toHaveBeenCalledWith(
expect.stringContaining(‘test-owner/test-repo’)
);
});

test(‘異常系: プレフィックス “v” がない場合、アクションが失敗する’, async () => {
// 1. 不正な入力値を設定
core.getInput.mockImplementation((name) => {
if (name === ‘github-token’) return ‘fake-token’;
if (name === ‘tag-name’); return ‘1.0.0’; // “v”がない
});

// 2. 実行
await run();

// 3. アサーション
expect(core.setFailed).toHaveBeenCalledWith(
‘タグ名は “v” で始まる必要があります。’
);
});
});

—

4. CI/CDパイプラインへの組み込み(品質保証プロセス)

ローカルでテストが書けたら、当然これをCI(GitHub Actions自身)に組み込み、プルリクエスト時に自動実行させます。ここでコンポジットアクションや再利用可能ワークフローの信頼性が担保されます。

品質保証ワークフロー (`.github/workflows/test.yml`)

name: “Action Quality Control”

on:
pull_request:
branches: [main]
paths:

  • ‘src/’
  • ‘__tests__/’
  • ‘action.yml’
  • ‘package.json’

jobs:
test:
name: Unit Test & Lint
runs-on: ubuntu-latest

steps:

  • name: Checkout Repository

uses: actions/checkout@v4

  • name: Setup Node.js

uses: actions/setup-node@v4
with:
node-version: ’20.x’
cache: ‘npm’

  • name: Install Dependencies

run: npm ci

  • name: Run Jest Unit Tests

run: npm test — –coverage

  • name: Verify Action Build (if using ncc)

# @vercel/ncc等でJSをバンドルしている場合のビルド検証
run: |
npm run build
git diff –exit-code dist/
# 💡 ハック: dist/ がビルド済みコードと一致しているかチェックし、
# コミット忘れをCIで阻止するプロのテクニック

—

5. チーム開発におけるベストプラクティスとルール

1. `dist/` のバージョン管理ルール:
JavaScriptアクションの場合、`dist/index.js`(nccなどでトランスパイルされた単一ファイル)をGit管理下置くことが推奨されます。上記のCI設定にある `git diff –exit-code dist/` は、「ソースを修正したのにビルド成果物をコミットし忘れた」というレビュー時の不毛なやり取りを完全に根絶する最強のガードレールです。
2. テストカバレッジの閾値設定:
`jest.config.js`にて `coverageThreshold` を設定し、カバレッジが80%を下回ったらマージできないようにチーム規約化しましょう。

// jest.config.js
module.exports = {
testEnvironment: ‘node’,
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: -10,
},
},
};

—

おわりに

「GitHub Actionsのコードはテストしにくい」というのは、もはや過去の神話です。

JestとActions Toolkitを組み合わせることで、通常のWebアプリケーション開発と同等レベルの堅牢性とスピード感を手に入れることができます。
リモートのランナーに祈りを捧げるデバッグ手法は今日で終わりにし、ローカルでミリ秒単位のフィードバックを得られる快適な開発体験を手に入れましょう。

あなたのパイプラインが、もっと美しく、もっと強靭になることを願っています。Happy Automation!

タイトルとURLをコピーしました