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!