はじめに#
TDD(テスト駆動開発)で書いたテストは、ローカル環境で実行するだけでは十分ではありません。チーム開発において真の価値を発揮するには、CI/CD(継続的インテグレーション/継続的デリバリー)パイプラインと連携させ、すべてのコード変更に対して自動的にテストを実行する仕組みが不可欠です。
本記事では、TDDとCI/CDを連携させることで得られるメリットから、GitHub Actionsを使った具体的な実装方法まで、実践的なガイドを提供します。JavaScript/TypeScript(Jest/Vitest)とJava(JUnit 5/Maven)の両方の例を示しながら、テストの自動実行、カバレッジレポートの生成、プルリクエストでのテスト必須化、テスト失敗時の通知設定まで、網羅的に解説します。
TDDとCI/CDを連携させるメリット#
TDDとCI/CDの組み合わせは、単なる自動化を超えた相乗効果を生み出します。
相乗効果の全体像#
graph TB
subgraph TDD["TDD(テスト駆動開発)"]
A[Red: 失敗するテスト] --> B[Green: テスト通過]
B --> C[Refactor: リファクタリング]
C --> A
end
subgraph CICD["CI/CD パイプライン"]
D[コードプッシュ] --> E[自動ビルド]
E --> F[テスト実行]
F --> G[カバレッジ計測]
G --> H{品質基準}
H -->|Pass| I[マージ可能]
H -->|Fail| J[マージ不可]
end
C --> D
TDD -.->|品質の担保| CICD
CICD -.->|即時フィードバック| TDDTDD単体とCI/CD連携の比較#
| 観点 |
TDD単体 |
TDD + CI/CD連携 |
| テスト実行 |
開発者のローカルのみ |
全コミットで自動実行 |
| 品質保証 |
開発者の意識に依存 |
仕組みで強制 |
| レグレッション検知 |
マージ後に発覚 |
マージ前に検知 |
| カバレッジ管理 |
手動確認 |
自動レポート・閾値設定 |
| チーム全体の品質 |
ばらつきあり |
一定水準を維持 |
具体的なメリット#
1. レグレッションの早期発見
プルリクエスト時点で既存機能への影響を検知できるため、本番環境へのバグ混入を防止できます。
2. コードレビューの効率化
テストが通過していることを前提にレビューできるため、レビュアーはビジネスロジックや設計に集中できます。
3. リファクタリングへの自信
テストスイートがセーフティネットとして機能するため、大規模なリファクタリングにも安心して取り組めます。
4. ドキュメントとしてのテスト
CIで実行されるテストは、常に最新の仕様書として機能します。
GitHub Actionsの基礎#
GitHub Actionsは、GitHubが提供するCI/CDプラットフォームです。リポジトリ内の.github/workflowsディレクトリにYAMLファイルを配置することで、ワークフローを定義できます。
ワークフローの基本構造#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
name: ワークフロー名
on:
# トリガーイベント
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
job-name:
runs-on: ubuntu-latest
steps:
- name: ステップ名
uses: アクション名
with:
パラメータ: 値
|
主要な構成要素#
| 要素 |
説明 |
例 |
name |
ワークフローの名前 |
CI Test |
on |
トリガーイベント |
push, pull_request |
jobs |
実行するジョブの定義 |
test, build |
runs-on |
実行環境 |
ubuntu-latest |
steps |
ジョブ内のステップ |
チェックアウト、テスト実行 |
uses |
再利用可能なアクション |
actions/checkout@v4 |
run |
シェルコマンド実行 |
npm test |
JavaScript/TypeScriptプロジェクトのCI設定#
Jest を使用したNode.jsプロジェクト#
.github/workflows/test.ymlを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
name: Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['18.x', '20.x']
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Run tests with coverage
run: npm run test:coverage
|
カバレッジレポートの生成と保存#
Jestのカバレッジ機能を活用し、結果をアーティファクトとして保存します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
name: Test with Coverage
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
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 tests with coverage
run: npm run test:coverage
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 30
|
package.jsonのスクリプト設定#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage --coverageReporters=text --coverageReporters=lcov"
},
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,ts}",
"!src/**/*.d.ts",
"!src/**/index.{js,ts}"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
|
Vitest を使用したプロジェクト#
Viteベースのプロジェクトでは、Vitestを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
name: Vitest Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
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 Vitest
run: npm run test:ci
- name: Run Vitest with coverage
run: npm run test:coverage
|
vitest.config.tsの設定例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
exclude: [
'node_modules/',
'test/',
'**/*.d.ts',
'**/*.config.*'
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80
}
}
}
})
|
JavaプロジェクトのCI設定#
Maven + JUnit 5 の基本設定#
.github/workflows/maven-test.ymlを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
name: Java CI with Maven
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Run tests with Maven
run: mvn --batch-mode --update-snapshots verify
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: target/surefire-reports/
retention-days: 30
|
マトリックスビルドで複数JDKバージョンをテスト#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
name: Java CI Matrix
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
java-version: ['17', '21']
include:
- java-version: '17'
maven-args: '-P java17'
- java-version: '21'
maven-args: '-P java21'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java-version }}
distribution: 'temurin'
cache: maven
- name: Run tests
run: mvn --batch-mode verify ${{ matrix.maven-args }}
|
JaCoCoによるカバレッジ計測#
pom.xmlにJaCoCoプラグインを追加します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
|
ワークフローでカバレッジレポートをアップロードします。
1
2
3
4
5
6
|
- name: Upload JaCoCo coverage report
uses: actions/upload-artifact@v4
with:
name: jacoco-report
path: target/site/jacoco/
retention-days: 30
|
カバレッジレポートの可視化#
Codecovとの連携#
Codecovを使用すると、カバレッジの推移を可視化し、プルリクエストにコメントを自動追加できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
name: Test with Codecov
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
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 tests with coverage
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
fail_ci_if_error: true
|
プルリクエストへのカバレッジコメント#
カバレッジ結果をプルリクエストに自動コメントする設定です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
- name: Code Coverage Summary Report
uses: irongut/CodeCoverageSummary@v1.3.0
with:
filename: coverage/cobertura-coverage.xml
badge: true
format: markdown
output: both
thresholds: '60 80'
- name: Add Coverage PR Comment
uses: marocchino/sticky-pull-request-comment@v2
if: github.event_name == 'pull_request'
with:
recreate: true
path: code-coverage-results.md
|
プルリクエストでのテスト必須化#
ブランチ保護ルールの設定#
GitHub Actionsのテストをマージの必須条件として設定します。
flowchart LR
A[PR作成] --> B[CI実行]
B --> C{テスト結果}
C -->|Pass| D[ステータスチェック通過]
C -->|Fail| E[ステータスチェック失敗]
D --> F[マージ可能]
E --> G[マージ不可]
style D fill:#28a745
style E fill:#dc3545
style F fill:#28a745
style G fill:#dc3545設定手順
- リポジトリの「Settings」を開く
- 「Branches」を選択
- 「Add branch protection rule」をクリック
- 以下を設定する
| 設定項目 |
推奨値 |
| Branch name pattern |
main |
| Require a pull request before merging |
有効 |
| Require status checks to pass before merging |
有効 |
| Require branches to be up to date before merging |
有効 |
| Status checks that are required |
test(ワークフローのjob名) |
ステータスチェックの設定例#
ワークフローのジョブ名が、ブランチ保護で指定する名前になります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
# このジョブ名「test」をブランチ保護で指定
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- run: npm ci
- run: npm test
# Lintチェックも必須にする場合
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- run: npm ci
- run: npm run lint
|
テスト失敗時の通知設定#
Slack通知#
テスト失敗時にSlackへ通知を送信します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
name: Test with Slack Notification
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
test:
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 tests
id: test
run: npm test
- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"text": "Test Failed",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Test Failed* :x:\n*Repository:* ${{ github.repository }}\n*Branch:* ${{ github.ref_name }}\n*Commit:* ${{ github.sha }}\n*Author:* ${{ github.actor }}"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View Workflow"
},
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
]
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
|
GitHub Issues への自動起票#
テスト失敗時にIssueを自動作成する設定です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
- name: Create Issue on Test Failure
if: failure() && github.ref == 'refs/heads/main'
uses: actions/github-script@v7
with:
script: |
const title = `Test failure in ${context.sha.substring(0, 7)}`;
const body = `
## Test Failure Report
- **Commit:** ${context.sha}
- **Branch:** ${context.ref}
- **Author:** ${context.actor}
- **Workflow Run:** ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}
Please investigate and fix the failing tests.
`;
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
labels: ['bug', 'test-failure']
});
|
実践的なワークフロー構成例#
フルスタックプロジェクトの完全なCI設定#
フロントエンド(React + Vitest)とバックエンド(Java + Maven)を含むプロジェクトの例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
|
name: Full Stack CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
env:
NODE_VERSION: '20.x'
JAVA_VERSION: '21'
jobs:
# フロントエンドのテスト
frontend-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./frontend
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run type check
run: npm run type-check
- name: Run unit tests
run: npm run test:ci
- name: Run tests with coverage
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./frontend/coverage/lcov.info
flags: frontend
fail_ci_if_error: true
# バックエンドのテスト
backend-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./backend
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: Run tests with Maven
run: mvn --batch-mode verify
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./backend/target/site/jacoco/jacoco.xml
flags: backend
fail_ci_if_error: true
# E2Eテスト(両方のテストが通過後に実行)
e2e-test:
needs: [frontend-test, backend-test]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: Start backend
run: |
cd backend
mvn spring-boot:run &
sleep 30
- name: Run E2E tests
run: |
cd frontend
npm ci
npm run test:e2e
# 全テスト通過後のサマリー
test-summary:
needs: [frontend-test, backend-test, e2e-test]
runs-on: ubuntu-latest
if: always()
steps:
- name: Check test results
run: |
if [ "${{ needs.frontend-test.result }}" == "failure" ] || \
[ "${{ needs.backend-test.result }}" == "failure" ] || \
[ "${{ needs.e2e-test.result }}" == "failure" ]; then
echo "Some tests failed"
exit 1
fi
echo "All tests passed successfully"
|
TDDサイクルとCIの統合ワークフロー#
TDDをチーム開発で実践する際の理想的なワークフローを示します。
sequenceDiagram
participant Dev as 開発者
participant Local as ローカル環境
participant Git as Git/GitHub
participant CI as GitHub Actions
participant Review as レビュアー
Dev->>Local: 1. Redテスト作成
Local->>Local: テスト失敗確認
Dev->>Local: 2. Green実装
Local->>Local: テスト通過確認
Dev->>Local: 3. Refactorリファクタリング
Local->>Local: テスト通過確認
Dev->>Git: 4. コミット・プッシュ
Git->>CI: 5. ワークフロー起動
CI->>CI: 6. テスト実行
CI->>CI: 7. カバレッジ計測
CI->>Git: 8. ステータス報告
alt テスト成功
Git->>Review: 9. レビュー依頼可能
Review->>Git: 10. レビュー承認
Git->>Git: 11. マージ
else テスト失敗
Git->>Dev: 9. 失敗通知
Dev->>Local: 修正・再プッシュ
endローカルでのプレコミットフック#
CIに送る前にローカルでテストを実行するため、huskyを使用したGit Hooksを設定します。
1
2
3
4
5
|
{
"scripts": {
"prepare": "husky"
}
}
|
.husky/pre-commitファイルを作成します。
1
2
|
npm run lint
npm test
|
.husky/pre-pushファイルを作成します。
これにより、CIでの失敗を事前に防ぎ、フィードバックサイクルを短縮できます。
Flaky Test(不安定なテスト)への対処#
CI環境でのみ失敗するテストや、不定期に失敗するテストへの対処法を解説します。
リトライ機構の実装#
1
2
3
4
5
6
|
- name: Run tests with retry
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm test
|
Flaky Testの検出と管理#
1
2
3
4
5
6
7
|
- name: Run tests with Flaky detection
run: |
for i in {1..5}; do
npm test && break
echo "Attempt $i failed, retrying..."
sleep 5
done
|
Flaky Testを防ぐためのベストプラクティス#
| 原因 |
対策 |
| 時間依存 |
テスト用の固定時刻を注入 |
| 外部API依存 |
モック・スタブを使用 |
| 実行順序依存 |
テストの独立性を確保 |
| 非同期処理 |
適切なawait・タイムアウト設定 |
| リソース競合 |
テストごとにリソースを分離 |
パフォーマンス最適化#
キャッシュの活用#
依存関係のキャッシュを活用してビルド時間を短縮します。
1
2
3
4
5
6
7
8
9
10
11
12
|
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
|
並列実行#
テストを並列実行してCI時間を短縮します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'
- run: npm ci
- run: npm test -- --shard=${{ matrix.shard }}/4
|
変更されたファイルのみテスト#
プルリクエストで変更されたファイルに関連するテストのみを実行します。
1
2
3
4
5
6
7
8
9
10
11
|
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
with:
files: |
src/**/*.ts
src/**/*.tsx
- name: Run related tests
if: steps.changed-files.outputs.any_changed == 'true'
run: npm test -- --findRelatedTests ${{ steps.changed-files.outputs.all_changed_files }}
|
まとめ#
TDDとCI/CDパイプラインの連携は、個人の開発習慣をチーム全体の品質保証の仕組みへと昇華させます。本記事で解説した内容を実践することで、以下の効果が期待できます。
| 導入前 |
導入後 |
| テスト実行は開発者任せ |
すべてのコミットで自動テスト |
| バグは本番で発覚 |
プルリクエスト時点で検知 |
| カバレッジは不明 |
常に可視化・閾値で品質担保 |
| レビューでテスト確認 |
テスト通過が前提 |
| リファクタリングに不安 |
自信を持って改善 |
GitHub Actionsを活用したCI/CDパイプラインは、TDDの効果を最大化し、チーム全体で継続的に高品質なソフトウェアを提供するための基盤となります。まずは基本的なテスト自動化から始め、段階的にカバレッジ計測、通知設定、ブランチ保護を導入していくことをおすすめします。
参考リンク#