はじめに
「CI(継続的インテグレーション)って、なんか難しそう……」
と、ずっと思っていました。
実際、これまで CI の設定は生成 AI にお願いして、なんとなく動いてれば OK くらいの感覚で使っていました。
でもふと、
「これ、ちゃんと理解しないままでいいのかな?」
と気になって、自分なりに調べてみたところ、
思っていたよりずっとシンプルで、わかりやすい仕組みでした。
概要
この記事では、FastAPI プロジェクトを題材にしながら、
実際に CI の仕組みを学んでいった流れをゆるやかにご紹介します。
「なんとなく使ってるけど、正直よく分かってない」
そんな方に読んでほしい内容です。
CI って結局何をしているの?
CI(継続的インテグレーション)の本質は、
「コードの品質を自動でチェックする仕組み」を作ることです。
難しそうに聞こえるかもしれませんが、
実は、以下の 3 つの要素を押さえておけば十分です:
1. Lint(リント) 📝
コードの見た目や書き方の問題を、自動でチェックします。
- 静的解析によって、実行前にコードを検査
- 不要な import 文や、コーディングスタイルの違反を指摘
- 「コードを実行する前に分かる問題」を見つける
2. Test(テスト) 🧪
コードが期待通りに動くかどうかを、自動で検証します。
- 動的解析でロジックの正常性を確認
- 関数が期待通りの値を返すか、API が正しく動作するかなどをチェック
- 「実際に動かしてみないと分からない問題」を見つける
3. Coverage(カバレッジ) 📊
テストがどれだけのコードをカバーしているかを数値で見える化します。
たとえば、関数は動作していても 分岐や例外パスが未テスト だと見落としがちです。
- テストの網羅性を測定
- どの部分のコードがテストされているかの割合を測定
- 「テストが十分かどうか」を数値で判断する
この 3 つを組み合わせることで、
バグを事前に防ぎ、安心してコードを変更できる環境が整います。
CI は、そのための自動チェック係です。
今回作るもの
FastAPI の簡単な API に対して、段階的に CI 機能を追加していきます:
- 基本のテスト自動実行 → まずは成功体験
- Lint チェック追加 → わざと失敗させてみて、修正体験をしてみる
- カバレッジ測定 → テストの「量と範囲」を見える化
- ブランチ保護設定 → CI が通らないとマージできない仕組みを導入
最終的には「あ、CI ってこんな感じか!」と思ってもらえるような内容を目指しました。
プロジェクト構成
fastapi-ci-sample/
├── main.py # FastAPI アプリケーション
├── client.py # API クライアント
├── tests/ # テストコード
│ ├── __init__.py
│ ├── test_main.py # メインテスト
│ └── test_client.py # クライアントテスト
├── requirements.txt # 依存関係
├── .github/
│ └── workflows/
│ └── ci.yml # GitHub Actions CI 設定
└── README.md
前提条件
この記事は前回の記事で作成した FastAPI アプリケーションをベースに実装しています。
FastAPI の基本的な実装については前回の記事をご参照ください。
- Python 3.8 以上
- FastAPI の基本的な知識
- Git の基本操作
1. テストコードを書いてみる
テスト環境の準備
まずはシンプルに、FastAPI アプリに対してテストコードを追加して動かしてみるところから始めます。
テストを書くことで、「ちゃんと API が動いているか?」を自動で確認できるようになります。
ここでは、次のような 3 つのテストを用意しました:
- ルート(/)への GET リクエストのテスト
- 加算 API(/add)の正常なリクエスト
- 加算 API に 0 を含めたリクエスト(変則的なケース)
今回は実装を省略しましたが、失敗するケースのテストもぜひ一緒に書いておくのがおすすめです。
「正しくエラーになること」も、アプリの品質を支える大切な要素です。
ディレクトリ構成
まずはテスト用のディレクトリとファイルを作成します:
mkdir -p tests
tests/__init__.py
# tests package
tests/test_main.py
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_read_root():
"""ルートエンドポイントのテスト"""
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, World!"}
def test_add_numbers_success():
"""加算エンドポイントの正常ケーステスト"""
test_data = {"a": 10.5, "b": 20.3}
response = client.post("/add", json=test_data)
assert response.status_code == 200
result = response.json()
assert result["a"] == 10.5
assert result["b"] == 20.3
assert result["result"] == 30.8
assert "message" in result
def test_add_numbers_zero():
"""ゼロを含む加算のテスト"""
test_data = {"a": 0, "b": 5.5}
response = client.post("/add", json=test_data)
assert response.status_code == 200
result = response.json()
assert result["result"] == 5.5
依存関係の追加
requirements.txt
にテスト関連のパッケージを追加:
pytest==7.4.3
pytest-cov==4.1.0
flake8==6.1.0
httpx==0.25.2
ローカルでテスト実行
まずは手元の環境でテストを実行して、意図通りに動作するか確認してみましょう:
# 依存関係をインストール
pip install -r requirements.txt
# テストを実行
pytest tests/ -v
実行結果
============ test session starts ============
tests/test_main.py::test_read_root PASSED [33%]
tests/test_main.py::test_add_numbers_success PASSED [66%]
tests/test_main.py::test_add_numbers_zero PASSED [100%]
============ 3 passed in 0.18s ===============
追加したテストが正常に動作することが確認できました。
このように、コードを書いたらテストを通すという流れを作っておくことで、安心して開発を進めることができます。
そして今後は、「どう失敗させて、どう検知するか」という観点でもテストを加えていくと、より CI の信頼性が高まります。
2. GitHub Actions でテスト自動化
次は、GitHub にプッシュしたときに自動でテストが実行されるようにします。
基本の CI 設定
.github/workflows/ci.yml
を作成:
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: |
pytest tests/ -v
この設定のポイント:
-
トリガー:
main
,develop
ブランチへのプッシュとmain
への PR で実行 - 環境: Ubuntu 最新版で Python 3.9 を使用
-
ステップ内容:
- コードチェックアウト
- Python の実行環境をセットアップ
- 依存関係インストール
- Pytest を使ってテストを実行
3. Lint チェックで品質向上
テストが動くようになったので、次はコード品質をチェックする Lint を追加します。
Lint は一見地味ですが、ミスやバグの温床になる「うっかりミス」や「不統一な書き方」を防ぐ強い味方です。
まずは「失敗する体験」をしてみる
いきなり CI に組み込む前に、まずはローカルで Lint エラーを出してみましょう。
tests/test_main.py
に意図的に不要な import を追加します。
import os # 使用しないimport文(意図的にLintエラーを発生させる)
import pytest # 実際は使わないが意図的に残す
from fastapi.testclient import TestClient
# ... 以下既存のコード
ローカルで Lint チェック
flake8 tests/test_main.py --max-line-length=88
結果:
tests/test_main.py:1:1: F401 'os' imported but unused
tests/test_main.py:2:1: F401 'pytest' imported but unused
期待通りエラーが出ました!
こうした実害がなさそうに見える import の消し忘れや書き方の不統一も、Lint はしっかり見逃しません。
「あとで消そうと思ってそのまま」のようなコードが、少しずつ蓄積していくと、
気づかないうちにコード全体の見通しや保守性に影響してくることもあります。
CI に Lint ジョブを追加
既存の ci.yml
に Lint チェックを追加します:
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
# ... (テストジョブの内容は前回と同じ)
lint: # 👈 新しく追加
runs-on: ubuntu-latest
needs: test # テストが成功した後に実行
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run lint check
run: |
flake8 . --max-line-length=88 --exclude=venv,__pycache__,.git
新しく追加された部分の説明:
-
needs: test
: テストジョブが成功してから Lint を実行 -
flake8
: Python コードの品質チェック -
--exclude
: 不要なディレクトリを除外
完全版 CI 設定ファイル
最終的な .github/workflows/ci.yml
の全体像:
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: |
pytest tests/ -v
lint:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run lint check
run: |
flake8 . --max-line-length=88 --exclude=venv,__pycache__,.git
coverage:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests with coverage
run: |
pytest tests/ --cov=. --cov-report=term --cov-fail-under=80
4. カバレッジ測定で網羅性チェック
次に、テストがどの程度コードをカバーしているかを測定してみましょう。
コードの一部しかテストされていないと、
「テストは通っているのに実はバグが潜んでいる」ということが起きがちです。
そこで活躍するのが テストカバレッジです。
「どのコードがテストされていて、どこがされていないのか?」を数値とレポートで見える化してくれます。
カバレッジ測定の設定
まず、CI にカバレッジチェックを追加します。80% 以下の場合は失敗するように設定:
coverage:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests with coverage
run: |
pytest tests/ --cov=. --cov-report=term --cov-fail-under=80
--cov-fail-under=80
により、カバレッジが 80% 未満の場合は CI が失敗します。
現在のカバレッジを確認してみる
現状のカバレッジを測定してみましょう:
pytest tests/ --cov=. --cov-report=term
実行結果:
---------- coverage: platform darwin, python 3.9.6-final-0 -----------
Name Stmts Miss Cover
----------------------------------------
client.py 15 15 0%
main.py 13 0 100%
tests/__init__.py 0 0 100%
tests/test_main.py 24 0 100%
----------------------------------------
TOTAL 52 15 71%
client.py
が全くテストされていないため、全体のカバレッジが 71% に留まっています。
失敗体験:カバレッジ不足で CI が失敗
80% 以下なので、試しにこの状態で CI を流すと…
pytest tests/ --cov=. --cov-fail-under=80
実行結果:
FAIL Required test coverage of 80% not reached. Total coverage: 71.15%
しっかり失敗しました。これがカバレッジ測定の威力です!
「テストしてない場所がある」ことを、ちゃんと CI が教えてくれました。
なぜテスト網羅性が重要なのか?
現在の状況では:
-
main.py
(FastAPI アプリ) → 100% カバー ✅ -
client.py
(API クライアント) → 0% カバー ❌
つまり、client.py
に以下のような問題があっても気づけません:
- ロジックエラー: 条件分岐が間違っている
- 例外処理の不備: エラーハンドリングが機能しない
- リファクタリング時の破損: コード変更で動作が変わる
つまり、**テストがないコードは「壊れてても気づかれないコード」**とも言えます。
カバレッジを向上させる
client.py
のテストを追加して、カバレッジを改善しましょう。
tests/test_client.py
import pytest
import requests
from unittest.mock import patch, Mock
import client
def test_call_api_success():
"""API呼び出し成功のテスト"""
# モックレスポンスを作成
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"message": "Hello, World!"}
with patch('requests.get', return_value=mock_response) as mock_get:
with patch('builtins.print') as mock_print:
client.call_api()
# requests.getが正しいURLで呼ばれたか確認
mock_get.assert_called_once_with("http://localhost:8000/")
# 成功メッセージが出力されたか確認
mock_print.assert_any_call("API 呼び出し成功!")
def test_call_api_failure_status():
"""API呼び出し失敗(ステータスコードエラー)のテスト"""
mock_response = Mock()
mock_response.status_code = 404
with patch('requests.get', return_value=mock_response):
with patch('builtins.print') as mock_print:
client.call_api()
mock_print.assert_any_call("API 呼び出し失敗: ステータスコード 404")
def test_call_api_connection_error():
"""API呼び出し失敗(接続エラー)のテスト"""
with patch('requests.get', side_effect=requests.exceptions.ConnectionError()):
with patch('builtins.print') as mock_print:
client.call_api()
mock_print.assert_any_call(
"サーバーに接続できませんでした。FastAPI サーバーが起動しているか確認してください。"
)
def test_call_api_general_exception():
"""API呼び出し失敗(一般的な例外)のテスト"""
with patch('requests.get', side_effect=Exception("テストエラー")):
with patch('builtins.print') as mock_print:
client.call_api()
mock_print.assert_any_call("エラーが発生しました: テストエラー")
カバレッジ向上の確認
新しいテストを追加してカバレッジを再測定:
pytest tests/ --cov=. --cov-report=term
結果:
---------- coverage: platform darwin, python 3.9.6-final-0 -----------
Name Stmts Miss Cover
------------------------------------------
client.py 15 2 87%
main.py 13 0 100%
tests/__init__.py 0 0 100%
tests/test_client.py 34 0 100%
tests/test_main.py 24 0 100%
------------------------------------------
TOTAL 86 2 98%
無事にカバレッジが 98% まで向上しました 🎉
今度は CI も通るはずです:
pytest tests/ --cov=. --cov-fail-under=80
実行結果:
============ 7 passed in 0.34s =======
Required test coverage of 80% reached. Total coverage: 97.67%
✅ 無事 CI が成功するようになりました!
5. ブランチ保護で品質を担保する
ここまでで CI にテスト・Lint・カバレッジチェックを組み込めました。
しかし、現時点では CI が失敗していても main ブランチにマージできてしまいます。
チーム開発だけの話に見えるかもしれませんが、
**「CI が通ってるものだけを main に入れる」**という仕組みは、個人開発でもとても有効です。
ブランチ保護ルールの設定
GitHub のリポジトリページで以下の手順で設定します:
- Settings タブをクリック
- 左サイドバーの Branches をクリック
- Add branch ruleset ボタンをクリック
- 以下の設定を行う:
Ruleset Name
例:Protect main、main ブランチ保護 など
自分が後で見てわかりやすい名前を付けましょう。
Target branches
保護対象のブランチを指定します。
- 「Add target」ボタンをクリック
- 「Include by pattern」を選択
- パターン入力欄に
main
と入力
Branch rules
-
Restrict deletions
→ main ブランチの誤削除を防ぐ -
Block force pushes
→--force
などでの強制 push をブロック -
Require status checks to pass
→ CI (test / lint / coverage) が通らないとマージ不可に - 「Add checks」ボタンでtest
,lint
,coverage
を追加
Enforcement status
最後に 「Enforcement status」を「Active」に設定 してください。
これを有効にしないと、ルールが適用されません。
- Active: ルールが有効になり、実際に保護される
- Disabled: ルールは作成されるが適用されない
実際のワークフロー
設定後の開発フローは以下のようになります:
# 1. ブランチ作成
git checkout -b feature/fix-validation
# 2. コード変更 → コミット → プッシュ
git add .
git commit -m "Fix input validation"
git push origin feature/fix-validation
# 3. GitHubでPR作成 → CI実行
# 4. CI成功で自分でマージ
CI が失敗するとどうなる?
試しに不要な import が残った状態で PR を作成すると:
- test: ✅ 成功
- lint: ❌ 失敗(不要な import のため)
- coverage: ✅ 成功
この状態では マージボタンが無効化 され、マージできません。
lint エラーを修正してプッシュすると、CI が再実行されてマージ可能になります。
完成した CI 設定ファイル(test / lint / coverage 対応)
最終的な .github/workflows/ci.yml
の全体像:
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: |
pytest tests/ -v
lint:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run lint check
run: |
flake8 . --max-line-length=88 --exclude=venv,__pycache__,.git
coverage:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests with coverage
run: |
pytest tests/ --cov=. --cov-report=term --cov-fail-under=80
まとめ
最初は「なんとなく」で使っていた CI も、
あらためて自分の手で動かしてみると、「あ、こういうことだったのか」と腑に落ちる部分が多くありました。
今回試してみたのは、この4つ:
- Lint:無駄なコードや書き方のブレを防いでくれる「お行儀チェック」
- Test:コードがちゃんと動いてるか確認できる「動作テストの自動化」
- Coverage:テストが足りているか見える化してくれる「安全範囲の可視化」
- ブランチ保護:CIが通ってるコードしか main に入らないようにする「守りの仕組み」
全部をまとめて見ると複雑そうに感じますが、1つひとつに分けてみれば、どれも驚くほどシンプル。
それらを組み合わせることで、安心してコードを変更できる環境がちゃんと整うのだと実感しました。
「CIって難しそう」と思っていたころの自分に、この記事を渡してあげたい気分です。