1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CIについて調べたら思ったよりシンプルだったので、FastAPIで構築してみた

Posted at

はじめに

「CI(継続的インテグレーション)って、なんか難しそう……」

と、ずっと思っていました。
実際、これまで CI の設定は生成 AI にお願いして、なんとなく動いてれば OK くらいの感覚で使っていました。

でもふと、
「これ、ちゃんと理解しないままでいいのかな?」
と気になって、自分なりに調べてみたところ、
思っていたよりずっとシンプルで、わかりやすい仕組みでした。

概要

この記事では、FastAPI プロジェクトを題材にしながら、
実際に CI の仕組みを学んでいった流れをゆるやかにご紹介します。

「なんとなく使ってるけど、正直よく分かってない」
そんな方に読んでほしい内容です。

CI って結局何をしているの?

CI(継続的インテグレーション)の本質は、
「コードの品質を自動でチェックする仕組み」を作ることです。

難しそうに聞こえるかもしれませんが、
実は、以下の 3 つの要素を押さえておけば十分です:

1. Lint(リント) 📝

コードの見た目や書き方の問題を、自動でチェックします。

  • 静的解析によって、実行前にコードを検査
  • 不要な import 文や、コーディングスタイルの違反を指摘
  • 「コードを実行する前に分かる問題」を見つける

2. Test(テスト) 🧪

コードが期待通りに動くかどうかを、自動で検証します。

  • 動的解析でロジックの正常性を確認
  • 関数が期待通りの値を返すか、API が正しく動作するかなどをチェック
  • 「実際に動かしてみないと分からない問題」を見つける

3. Coverage(カバレッジ) 📊

テストがどれだけのコードをカバーしているかを数値で見える化します。
たとえば、関数は動作していても 分岐や例外パスが未テスト だと見落としがちです。

  • テストの網羅性を測定
  • どの部分のコードがテストされているかの割合を測定
  • 「テストが十分かどうか」を数値で判断する

この 3 つを組み合わせることで、
バグを事前に防ぎ、安心してコードを変更できる環境が整います。
CI は、そのための自動チェック係です。

今回作るもの

FastAPI の簡単な API に対して、段階的に CI 機能を追加していきます:

  1. 基本のテスト自動実行 → まずは成功体験
  2. Lint チェック追加 → わざと失敗させてみて、修正体験をしてみる
  3. カバレッジ測定 → テストの「量と範囲」を見える化
  4. ブランチ保護設定 → 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 を使用
  • ステップ内容:
    1. コードチェックアウト
    2. Python の実行環境をセットアップ
    3. 依存関係インストール
    4. 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 のリポジトリページで以下の手順で設定します:

  1. Settings タブをクリック
  2. 左サイドバーの Branches をクリック
  3. Add branch ruleset ボタンをクリック
  4. 以下の設定を行う:

Ruleset Name

例:Protect main、main ブランチ保護 など
自分が後で見てわかりやすい名前を付けましょう。

Target branches

保護対象のブランチを指定します。

  1. 「Add target」ボタンをクリック
  2. 「Include by pattern」を選択
  3. パターン入力欄に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 を作成すると:

  1. test: ✅ 成功
  2. lint: ❌ 失敗(不要な import のため)
  3. 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って難しそう」と思っていたころの自分に、この記事を渡してあげたい気分です。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?