0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIのコード品質検証のためにやったこと

0
Posted at

はじめに

AI コーディング時代。Claude Code、Cursor、Copilot など、コードを直接タイピングする時間よりも、AI が生成したコードをレビューする時間の方が長くなりました。

しかし、AI が作ったコードの安定性を確保するのは簡単ではありません。

AI は「動作するコード」を作るのは得意ですが、プロジェクトのアーキテクチャルール、テスト品質、コードコンベンションまで気を配るのは別の問題です。

CLAUDE.md にルールを書いておいても、結局ガイドラインに過ぎません。守らなくてもビルドは通ります。

そしてまた、AI によって生産性が向上した現時点において、人が一つ一つ追いかけながらチェックするのも無理があります。

私はこの問題を解決するために、さまざまなツールとプロセスを導入しました。

この記事は、その過程をまとめたものです。

現在やっていること

私たちのプロジェクトでは、コードの安定性のためにすでに運用中のツールが 2 つあります。

1. JaCoCo + CodeCov - カバレッジの可視化

JaCoCo でテストカバレッジを測定し、CodeCov で PR ごとに可視化しています。

PR を開くと GitHub Action がテストを実行し、Codecov がカバレッジレポートを PR コメントとして残します。

# open-pr.yml
- name: Test with Gradle
  run: ./gradlew test
- name: Upload coverage reports to Codecov
  uses: codecov/codecov-action@v5
  with:
    token: ${{ secrets.CODECOV_TOKEN }}

image.png

PR で「この変更によってカバレッジが何 % 変わったか」をすぐに確認できます。

image.png

# codecov.yml
codecov:
  require_ci_to_pass: true
github_checks:
  annotations: true

単なる可視化だけでなく、Codecov の PR チェックがカバレッジ基準を満たさなければ PR が承認されないように 設定しています。

カバレッジが下がるコードは、マージ自体がブロックされます。

2. NShiftKey

NShiftKey は、PR が開かれたときに自動でセキュリティ検査を行う GitHub check ツールです。

PR ごとに 6 つの項目を検査します。

image.png

  • インフラエラー
  • コードレベルのセキュリティ脆弱性
  • ライブラリの脆弱性
  • 機密情報の露出
  • 静的解析ベースのバグ検出
  • コードスメルの検出

PR を開くと NShiftKey が自動で実行され、セキュリティ上の問題が見つかると PR チェックが失敗します。

Codecov と同様に、通過しなければマージがブロックされます。

AI が生成したコードに誤ってシークレットが含まれていたり、脆弱なライブラリを import していたり、Dockerfile の設定を間違えていても、人がレビューする前に検出できます。

ただし、NShiftKey のコードスメル検出は セキュリティ観点の簡単なパターンマッチング レベルです。

例えば、ハードコーディングされたパスワードや危険な API の使用は検出できますが、重複コードや複雑度の過多といった問題は当然検出できません。


限界

これらのツールである程度は対応できましたが、根本的な限界がありました。

カバレッジの罠

現在、私たちのプロジェクトの JaCoCo カバレッジは約 90% 程度です。

数字だけ見ると悪くありません。

しかし、カバレッジが高いということは、テストがそのコードを実行したという意味であり、その実行結果が正しいかどうかまでは保証しません。

@Test
void マナースコア計算(){
    mannerScoreService.calculate(userId);
    // assert がない。実行しただけで結果を検証していない
}

calculate() の中で計算方式をいくら変えても(例:+- に変えても)、このテストは通過します。コードが壊れていても、テストはそれに気づきません。

アーキテクチャルール違反を検出できない

AI がコードを生成する際に、プロジェクトのレイヤー構造に違反する場合があります。

Controller から Repository を直接呼び出したり、Entity に @Autowired を付けたりするケースです。

これは CLAUDE.md にルールをまとめても、AI が 100% 正確に従うわけではありません。

コードスメルを体系的に検出できない

NShiftKey がセキュリティ関連の設定に限定して検査してくれますが、複雑度や不要な DB 呼び出しなど コードレベルの問題 は NShiftKey の検出範囲外であり、人の目でレビューする際にも見落としやすいです。

AI が生成するコードの量が増えるほど、さらに見落としやすくなります。


そんな中、MUSINSA という企業の技術ブログを読みました。

「AI にお願いするのではなく、試験を受けさせろ」

AI にお願いするよりも、強制的なルールを与えて失敗させるという内容でした。

この記事では ArchUnit でテストを失敗させていました。

私もここからアイデアを得て、統制するツールを導入し、AI コードの安定性と品質を改善しようと考えました。

導入するもの

  1. アーキテクチャルール違反 → コード生成時点で検出し、AI が自ら修正(ArchUnit + Claude Code Hooks)
  2. テスト品質 → 「実行しただけのテスト」と「検証するテスト」を区別(Mutation Testing)
  3. コードスメル → 静的解析で体系的に検出(SonarCloud)

ArchUnit 導入記

ArchUnit とは

ArchUnit は、Java のアーキテクチャルールを ユニットテストとして記述する ライブラリです。

「Controller は Repository を直接参照してはならない」といったルールをコードで定義し、違反するとテストが失敗します。

// build.gradle
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'

プロジェクト構成

住民同士がコミュニケーションするコミュニティアプリ
├── auth/           # 認証(JWT + OAuth2)
├── message/        # グループチャットメッセージング(コアモジュール)
├── room/           # チャットルーム管理
├── story/          # ストーリー + フリマ取引
├── notification/   # プッシュ通知
├── user/           # ユーザープロフィール
└── common/         # 共通インフラ
各モジュール内部:
├── presentation/   # Controller + DTO
├── domain/         # Service + Repository + Entity
└── infrastructure/ # 外部連携、設定

適用したルール 10 個

ArchitectureTest に 8 つのカテゴリルールを定義しました。主なルールは以下の通りです。

image.png

1. ネーミングコンベンション

  • @RestController クラスは *Controller で終わる必要があり、presentation.controller パッケージに配置しなければなりません。@Service も同様です

2. エンティティの純粋性

  • Entity は Spring アノテーション禁止、Service / Infrastructure への依存禁止

3. レイヤー依存性の方向

  • Controller → Repository の直接参照禁止、Controller での @Transaction@EventListener@Async の使用禁止

4. 循環依存性

  • フィーチャーモジュール間のサービスレイヤーにおける循環参照を検出
  • データレイヤー(Entity、Repository、Event など)のクロスモジュール参照は、モノリス構造上やむを得ないため除外

5. コード品質

  • System.out 禁止、ジェネリック例外(RuntimeException)の throw 禁止、フィールド @Autowired 禁止(コンストラクタインジェクションを強制)
  • プロダクションコードでのテストライブラリ依存禁止

6. エンティティ純粋性の拡張

  • Entity に Lombok @Setter 禁止、@Async@Scheduled 禁止

7. 依存性逆転の原則

  • domain.resolver インターフェースの実装クラスが domain パッケージに配置されてはなりません
  • infrastructure に配置してこそ DIP が守られます
  • このプロジェクトでは、モジュール間の依存性を切るために domain にインターフェース(RoomMessageResolverAuthUserProvider など)を置き、infrastructure に実装クラス(RoomMessageResolverImplAuthUserProviderImpl など)を配置する DIP パターンを使用しています
  • AI が便宜上、実装クラスを domain に直接作ってしまうのを、このルールが検出します

8. API URL 形式の検証

  • Controller の @RequestMapping は必ず /api/v1 で始まらなければなりません。AI が /users のような非標準 API を作るのを防止します。

Claude Code Hooks:自動採点機

ArchUnit は試験用紙です。Claude Code Hooks は 自動採点機 です。

Claude Code Hooks とは

Claude Code のライフサイクルの特定の時点で 自動的に実行されるシェルコマンド です。

  • CLAUDE.md のルールはガイドラインに過ぎません
  • Hooks のルールは強制されます

実際の活用

Stop イベントを活用しました。Claude が応答を終えるたびに、ArchUnit テストが自動実行されます。

.claude/settings.json の Hook 設定は以下の通りです。

image.png

.claude/hooks/archunit-check.sh の設定は以下の通りです。

#!/bin/bash
# Claude Code Stop Hook: ArchUnit アーキテクチャ検証
# Claude が応答を終えた時に実行。違反時は exit 2 で Claude が引き続き修正するようにする。
# 無限ループ防止:Stop hook の中で再度 Stop がトリガーされた場合はスキップ
INPUT=$(cat)
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null)
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
    exit 0
fi
PROJECT_DIR=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null)
if [ -z "$PROJECT_DIR" ]; then
    PROJECT_DIR="/Users/gimdonghyeon/Documents/Jimotoku"
fi
cd "$PROJECT_DIR" || exit 0
# 変更された Java ソースファイルがあるか確認(テストファイルは除外)
MODIFIED=$(git diff --name-only 2>/dev/null | grep '\.java$' | grep 'src/main/' || true)
if [ -z "$MODIFIED" ]; then
    exit 0  # Java ソースの変更がなければスキップ
fi
# ArchUnit テスト実行
echo "🔍 ArchUnit アーキテクチャ検証実行中..."
OUTPUT=$(./gradlew test --tests '*ArchitectureTest*' -q 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
    echo "ArchUnit アーキテクチャ違反を発見!"
    echo ""
    echo "$OUTPUT" | grep -A 5 "Architecture Violation\|FAILED\|> Task.*FAILED" | head -40
    echo ""
    echo "違反事項を修正してください。"
    exit 2  # Claude が引き続き作業するようにする
fi
echo "ArchUnit アーキテクチャ検証通過"
exit 0

Claude Code を通じて作成しました

なぜテストを直接実行せず、スクリプトファイルを別に作って実行するのか?

  • ArchUnit のみ検査するため、全テストよりも実行時間がはるかに短いです
  • Java ソースの変更がなければスキップします(git diff チェック)
  • Claude が応答を終えるたびに実行されるため、全テストでは重すぎます

Mutation Testing:テストは本当に検証しているのか?

なぜ必要なのか

  • JaCoCo カバレッジ約 90%、テストファイル 93 個、テストコード 2600 個以上。テストの数だけ見ると十分に見えます。
  • しかし、Claude Code がコードを書き換えるブラックボックス(in → blackbox → out)領域の変更を検出できるほど、テストコードが良質に書かれているかどうかは別の問題です。
  • そこで、Mutation Testing を導入することにしました。

Mutation Testing とは?

  • テストがプロダクションコードのバグを実際に検出できるのか?
  • つまり、テストの質を検証するテストです。
  1. プロダクションコードを意図的に変形する(mutant を生成)
  2. 変形されたコードに対して既存のテストを実行する
  3. テストが失敗すれば killed(良い。テストが変形を検出した)
  4. テストが通過すれば survived(悪い。テストが変形を見逃した)

例えば

  public int calculateUnreadBadge(int totalUnread, int readCount) {                                                   
      if (readCount > totalUnread) {                                                                                  
          return 0;                                                                                         
      }                                                                                                               
      return totalUnread - readCount;                                                                                 
  }                                                                                                                   

以下のようなコードがある場合、

Mutant の例 — PITest がコードを以下のように変形します。

 // Mutant 1: 条件境界の変形(ConditionalsBoundary)
  if (readCount >= totalUnread) {  // > → >=

  // Mutant 2: 数学演算の変形(Math)
  return totalUnread + readCount;  // - → +

  // Mutant 3: 戻り値の変形(ReturnValue)
  return 0;                        // 結果を無条件に 0 に

悪いテスト は以下のような結果になります。

@Test
  void バッジ計算() {
      calculateUnreadBadge(10, 3);
      // assert がない。実行しただけで終わり。
      // → Mutant 1, 2, 3 すべて survived(カバレッジは 100%)
  }

反対に 良いテスト は以下のように書かれます。

  @Test
  void 読んだ分だけバッジが減るべき() {
      assertThat(calculateUnreadBadge(10, 3)).isEqualTo(7);
      // → Mutant 2(+ に変えると 13)killed ✓
      // → Mutant 3(0 を返す)killed ✓
  }

  @Test
  void 読んだ数が全体より大きければ_0になる() {
      assertThat(calculateUnreadBadge(5, 5)).isEqualTo(0);
      assertThat(calculateUnreadBadge(5, 6)).isEqualTo(0);
      // → Mutant 1(>= に変えると 5,5 で結果が変わる)killed ✓
  }
  • PITest は分析に時間がかかりすぎるため、CI でのみ動作するようにしました。
# deploy.yml
- name: Run Tests
  run: ./gradlew test --no-daemon

- name: Run PITest (Mutation Testing)
  run: ./gradlew pitest --no-daemon
  continue-on-error: true

- name: Upload PITest Report
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: pitest-report
    path: build/reports/pitest/
    retention-days: 14

SonarCloud 導入

SonarCloud とは?

  • 静的解析ツールです。コードを実行しなくても、バグ、コードスメル、セキュリティ脆弱性、重複コードを検出します。

連携するだけで、PR に分析レポートを投稿してくれます。

image.png

image.png

上記のような分析機能を提供しています。

私たちのプロジェクトでは、上の画像以外にも 5 つほどの他のパフォーマンス上の問題を発見することができました。

JaCoCo 連携

SonarCloud は JaCoCo が生成した XML レポートを読み取ってカバレッジを表示します。特別な設定なしに sonar.coverage.jacoco.xmlReportPaths を指定するだけで済みます。

この他にも

  • Gemini によるコードレビュー(以前は CodeRabbit、Copilot を使っていましたが、これが一番性能が良かったです)
  • Agent Team を構成したコード品質レビューなどの機能を使っています

ここでは今回新たに導入した、コード品質を高めるためのツールを紹介する記事なので、ここまでにします。

まとめると以下の通りです。

全体ツール体系の整理

コード生成時点(Claude Code)

  • CLAUDE.md によるガイドライン
  • ArchUnit によるアーキテクチャの試験用紙
  • Claude Hooks による採点および修正の自動化

PR 時点(GitHub Actions)

  • JaCoCo + CodeCov によるカバレッジの可視化
  • SonarCloud による静的解析(バグ、スメル、セキュリティ)
  • NShiftKey によるセキュリティ問題の検出
  • Gemini によるコードレビュー
  • ユニットテスト → ./gradlew test
  • PITest → Mutation Testing(テスト品質の測定)

強制するツール vs 検知するツール

すべてのツールが同じ強度で動作するわけではありません。

役割に応じて 強制 または 検知 に分けることができます。

強制するツールが セーフティネット であるなら、検知するツールは 診断ツール です。

なぜこのように分けるかというと、開発速度とのトレードオフの関係にあるからです。

例えば、PITest を Claude Code に必ず適用するとなると、テストに 1 時間もかかってしまいます。

これらのツールを組み合わせて、異なる観点からコードの安定性を確保し、AI が書いたコードを信頼できるコードにするために努力しています。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?