テスト仕様書を設計書からGeminiで自動生成！〜生成AIを活用した仕組みづくりでドキュメント陳腐化を防ぐ！〜

最近、どうしたら開発のドキュメントの陳腐化を防げるかを考えるの必死になっているエンジニアです。
今回、なぜドキュメントが陳腐化してしまうのか考えながらそれに対する部分的な解として生成AIを用いた私なりの解消法を紹介したいと思います！

## なぜドキュメントは陳腐化するのか（更新されないのか）

* ドキュメントがなくても本番環境でシステムが問題なく動いている
* ドキュメントを書くことのメリットがチーム全体で認識されていない
* 緊急対応時にソースコードを修正し、ドキュメントの更新が漏れてしまう

## どうしたら解決できるのか

### ドキュメントとコードの距離を近づけてバージョン管理をする

普段、GitHubでソースコードを管理することが多いので、ドキュメントも一緒に管理することで、ソースコードを更新する際に設計ドキュメントなども一緒に更新した上でPull Requestを出してもらうようにすれば、更新漏れが少なくなるのではないかと考えました。

余談ですが、既存のドキュメント管理系のサービスにGithubのプルリクエストのようなドキュメントを更新する前にレビューできる機能があれば、ドキュメント更新に対しての心理的安全性も高まるのではないかなと思ってます。

### ドキュメントを更新することのメリットを大きくする

今回の記事では、こちらをメインに考えフローを検討しました。
ドキュメントが更新されないのは、ドキュメントによるメリットが享受されておらず、重要性を認識できていないため、タスクの優先度が下がり、いつしか現行のシステムの仕様からかけ離れたドキュメントとなってしまうのだと思います。

それであれば、ドキュメントを書くことのメリットを増やせば良いと考え、簡単に導入できそうな仕組みを考えてみました。

## テスト仕様書自動生成フロー

![結合試験仕様書自動生成.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/5419831f-10c4-4304-1268-4d39069919de.png)

フローは下記の通りです。

1. 開発者が設計ドキュメントを記載してプルリクエストを作成
1. Github ActionsがPull Requestをトリガーに変更があった設計ファイルをもとにVertex AIでGeminiにテスト仕様書を書き起こすように依頼
1. Vertex AIからのレスポンス内容をCSV形式に整形し、プルリクエスト内でコミット（モデルは`gemini-2.0-flash-exp`を使用）
1. レビュワーは設計内容と一緒にテスト仕様書をレビュー

### メリット 
- テスト仕様書の作成が不要になる（多少の手直しはあっても0→1はなくなる）
- 設計書を更新すればテスト仕様書も一緒に更新されるので設計書を更新しようとする動機づけが生まれる
- 設計に対してのテストという状態が保てる。実装に依存しない

### 実際の出力結果
markdownで書かれたものが簡易的な設計書で、その下の画像が実際にGeminiで生成されたテスト仕様書になります。
#### 設計書
```markdown
# ログイン

## 概要

- TODOアプリを利用するための認証機能。

## 画面イメージ

## 項目設計
| 項目名 | I/O |データ型 | 制約 |
|---|---|---|---|
| メールアドレス | I | String | 必須、最大255文字、メールアドレス形式 |
| パスワード | I | String | 必須、8文字〜12文字、半角英数字記号 |

## 動作一覧

| 動作名 | 説明| トリガー | 処理内容 | 成功時の挙動 | 失敗時の挙動 |
|---|---|---|---|---|---|
|入力内容の検証 | ユーザーが「メールアドレス」および「パスワード」を入力した際にリアルタイムで検証を実施する。| ・入力欄への文字入力<br>・フォーカス移動 | メールアドレス欄: 必須チェック、形式チェック<br>パスワード欄: 必須チェック、最小文字数の確認 | 入力欄の下にエラー表示なし。| エラーメッセージを入力欄下に表示し、該当入力欄を赤枠で強調。 |
| ログインボタンの状態切り替え | 入力欄に有効な値が入力された場合に「ログインボタン」を有効化となる。| 入力欄の値が変更されたとき| メールアドレスとパスワードが両方有効な場合: ボタン有効化<br>どちらかが無効な場合: ボタン無効化| ボタンがクリック可能となる。| ボタンが非活性状態となる。|
| ログイン | ユーザーが「ログインボタン」をクリックした際にログイン処理を実施。| ログインボタンクリック | メールアドレスとパスワードをサーバーに送信し、認証処理を実施する。| ログイン成功: トップページへ遷移<br>ログイン失敗: エラーメッセージを表示| エラーメッセージを表示|
```

#### 設計書から生成されたテスト仕様書
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/06a2d104-2916-c595-f5cc-799152843424.png)

:::note warn
今回の仕組みは設計書をMarkdownファイルで記載されている前提となります。
:::


## 実践
上記の仕組みを作っていく手順をご紹介します。

### 1. Workload Identityの設定

Github ActionsでGoogle Cloudのサービスにアクセスするための設定を行っていきます。
サービスアカウントキーなど用意することなく、Google CloudへアクセスするためにOIDCを使用した方法で設定していきます。

もうすでに他のWorkload Identityのプールがある方は、IAMと管理のWorkload Identity連携に遷移し、「プールを作成」を押下します。
初めての方は、「開始」を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/a5f234e5-62b3-1b39-31ed-cb4be1713594.png)

名前とプールIDに任意の文字列を入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/caed390d-6ecb-d684-5a65-1b3759d854b1.png)

プロバイダの選択で`OpenID Connect（OIDC）`を選択し、プロバイダ名に任意の文字列を入力します。
発行元に`https://token.actions.githubusercontent.com` を入力します。
オーディエンスは`デフォルトのオーディエンス`を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/264813ee-9c2e-fe1d-0292-572fe6710ddf.png)
属性マッピングに以下を入力します。
| Google 属性            | OIDC 属性            |
| --------------------- | --------------------- |
| google.subject      | assertion.sub     |
| attribute.repository_owner | assertion.repository_owner |
| attribute.aud       | assertion.aud      |
| attribute.actor     | assertion.actor     |
| attribute.repository | assertion.repository|

属性条件に下記を入力して、保存を押します。
`attribute.repository==assertion.repository`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/efc13188-1435-a37c-8718-397c13072a8d.png)

### 2. サービスアカウントの設定

サービスアカウントの作成をします。
サービスアカウント名とサービスアカウントIDに任意の文字列を入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/2c1c9843-6e02-38c9-5c8c-2a9d183ca307.png)

サービスアカウントに対しての権限を以下2つを付与します。
- Vertex AIユーザー
- サービスアカウント トークン作成者

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/07f767f9-39ae-b29f-13ef-d5886a0de30e.png)

作成されたサービスアカウントの権限タブの「アクセス許可」を押して、新しいプリンシパルに下記に必要な情報を入れます。
```txt
principalSet://iam.googleapis.com/projects/<プロジェクト番号>/locations/global/workloadIdentityPools/<プールID>/attribute.repository/<組織名>/<リポジトリ名>
```

ロールは、`Workload Identity ユーザー`を選択して、保存を押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/518bf16a-4f12-9318-5479-1e5cd34b8ae1.png)

これでGoogle Cloud上での設定は終わりです。

### 3. Github Actionsの設定

続いて、Githubのリポジトリ設定を行っていきます。

まず、後ほどGithub Actions内で参照するシークレット変数を設定します。
リポジトリのメニューバーの「Settings」を押して設定ページに遷移します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/d7e8ad66-3fb9-0fac-976d-d6d9bec805e7.png)

Secrets and variablesのActions設定から下記の2つを保存します。

- workload_identity_provider
```text
projects/<プロジェクト番号>/locations/global/workloadIdentityPools/<プールID>/providers/<プロバイダーID>
```

- service_account
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/094406cd-7644-49cc-6a8a-4b150e2114cb.png)



Github Actions内で実行する、テスト仕様書を生成するためのコードを準備します。

```python:.github/scripts/generate_test_spec.py
import os
import sys
from google import genai
from google.genai import types

# 設定値
PROJECT_ID = "<プロジェクトID>"
LOCATION = "us-central1"
MODEL_NAME = "gemini-2.0-flash-exp"
TEMPERATURE = 0
TOP_P = 0.95
MAX_OUTPUT_TOKENS = 8192
RESPONSE_MODALITIES = ["TEXT"]
TEST_SPEC_HEADER = "ケースID, テスト大項目,テスト中項目,小項目,テスト内容,期待結果,実施日時,ステータス"
SYSTEM_INSTRUCTION = f"""システムの設計書が渡されるので、設計に対してテストを作成してください。
テスト仕様書をカンマ区切りで出力してください。
テスト仕様書以外の情報は不要です。
ヘッダ行は以下のようにしてください。
{TEST_SPEC_HEADER}
実施日、ステータスは空欄で構いません。
"""

SAFETY_SETTINGS = [types.SafetySetting(
    category="HARM_CATEGORY_HATE_SPEECH",
    threshold="OFF"
  ),types.SafetySetting(
    category="HARM_CATEGORY_DANGEROUS_CONTENT",
    threshold="OFF"
  ),types.SafetySetting(
    category="HARM_CATEGORY_SEXUALLY_EXPLICIT",
    threshold="OFF"
  ),types.SafetySetting(
    category="HARM_CATEGORY_HARASSMENT",
    threshold="OFF"
  )
]

def generate_test_spec(document: str) -> str:
    """設計ドキュメントからテスト仕様書を生成する。

    Args:
        document: 設計ドキュメントのテキスト。

    Returns:
        生成されたテスト仕様書のテキスト。
    """
    client = genai.Client(
        vertexai=True,
        project=PROJECT_ID,
        location=LOCATION
    )

    contents = [
        types.Content(
            role="user",
            parts=[types.Part.from_text(document)]
        )
    ]

    generate_content_config = types.GenerateContentConfig(
        temperature=TEMPERATURE,
        top_p=TOP_P,
        max_output_tokens=MAX_OUTPUT_TOKENS,
        response_modalities=RESPONSE_MODALITIES,
        safety_settings=SAFETY_SETTINGS,
        system_instruction=[types.Part.from_text(SYSTEM_INSTRUCTION)],
    )

    response = client.models.generate_content(
        model=MODEL_NAME,
        contents=contents,
        config=generate_content_config,
    )
    return response.text


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("使用法: python script.py <設計書ファイル>")
        sys.exit(1)

    input_file_paths = sys.argv[1:]
    for input_file_path in input_file_paths:
      try:
        with open(input_file_path, "r") as f:
          design_document = f.read()
      except FileNotFoundError:
        print(f"エラー: ファイル '{input_file_path}' が見つかりません。")
        sys.exit(1)
      test_spec_content = generate_test_spec(design_document)
      output_file_path = input_file_path.replace("docs", "tests").replace(".md", ".csv")
      os.makedirs(os.path.dirname(output_file_path), exist_ok=True)
      with open(output_file_path, "w") as f:
        f.write(test_spec_content)
      print(f"テスト仕様書を '{output_file_path}' に保存しました。")
```

:::note warn
`docs/`ディレクトリ以下に設計書をmdファイルで格納し、テスト仕様書は`tests`/ディレクトリに格納するようなコードとなっています。
ディレクトリなどを変更したい場合は`input_file_path.replace`の記述があるところを適宜変更してください。
:::

次に、プルリクエストがされたときに自動的にテスト仕様書が生成されるようにGithub Actionsの設定をしていきます。
```yaml:.github/workflows/generate_spec.yaml
name: Generate test spec
on:
  pull_request:

jobs:
  generate_test_spec:
    permissions:
      contents: write
      pull-requests: write
      id-token: write

    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Diff files /docs/**
        id: diff_files
        run: |
            changed=$(git diff --name-only origin/${{ github.base_ref }} HEAD --relative docs/ | wc -l)
            echo "::set-output name=changed::$changed"

      - uses: google-github-actions/auth@v2
        if: ${{ steps.diff_files.outputs.changed != '0' }}
        with:
          workload_identity_provider: ${{ secrets.workload_identity_provider }}
          service_account: ${{ secrets.service_account }}

      - name: Set up Cloud SDK
        if: ${{ steps.diff_files.outputs.changed != '0' }}
        uses: google-github-actions/setup-gcloud@v2

      - name: Set up Python
        uses: actions/setup-python@v5
        if: ${{ steps.diff_files.outputs.changed != '0' }}
        with:
          python-version: '3.11'

      - name: Install dependencies
        if: ${{ steps.diff_files.outputs.changed != '0' }}
        run: |
          python -m pip install --upgrade pip
          pip install google-genai


      - name: generate_test_spec
        if: ${{ steps.diff_files.outputs.changed != '0' }}
        run: |
          diff_files=$(git diff --name-only origin/${{ github.base_ref }} HEAD --relative docs/ | tr '\n' ' ')
          # 設計書1つにに対してテスト仕様書を1つ生成する
          python .github/scripts/generate_test_spec.py $diff_files

      - name: Commit test spec
        if: ${{ steps.diff_files.outputs.changed != '0' }}
        run: |
          git config --global user.email "github-actions[bot]@users.noreply.github.com"
          git config --global user.name "github-actions[bot]"
          git add tests/

          if git diff --cached --exit-code; then
            echo "No changes to commit. Skipping..."
          else
            git commit -m "Add test spec"
            git push origin HEAD:${{ github.head_ref }}
          fi
```

これでテスト仕様書生成のための設定は完了です。
実際にドキュメントを作成してプルリクエストを作成してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/132fb143-e5a8-29ee-e57b-1f83530df17a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/a6173e33-8615-bfb8-0972-f6359e266424.png)

テスト仕様書が自動で生成されるようになりました！:open_hands:

## 最後に

今回は実施できませんでしたが、今後テストを行う際にCSVファイルでの管理は手間がかかるため、以下のフローを試してみたいと考えています。
![結合試験仕様書自動生成.drawio (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/947600/6dd9f0b3-44d1-8847-37fb-2ecbef9edeb0.png)

1. mainブランチへのマージ後に、Github ActionsでCSVファイルをGoogleスプレッドシートに転記
2. 転記されたスプレッドシートをテストのマスターデータとして扱う
3. テスト実施時はマスターデータを複製し、複製したスプレッドシート上でテストを進め、結果を記録

このフローであれば、テスト時のデータ管理がよりスムーズになるかなと思います。


今回は、全て自動化ではなく半自動化という形で生成AIによって作られたテスト仕様書を人がレビューして直せるようにプルリクエスト内で仕様書を作成するようにしました。
今回の内容をGithub Actions自作しようと思いましたが、間に合わなかったので時間作って、Marketplaceに公開できたらと思っています。

開発者の体験を高めたり、今まで使っていた工数を削減することでよりビジネスとしてコアな部分へ注力できるようになるかと思うので日々効率化というところは追求していこうと思います！

## 参考記事

https://zenn.dev/kou_pg_0131/articles/gh-actions-oidc-gcp