はじめに
「このプロジェクトのテスト密度は業界平均と比べてどうですか?」
大規模なフレームワーク刷新プロジェクトのレビューで、こういう質問が飛んでくることがあります。「感覚的には十分テストしています」では通らない場面です。
今回のプロジェクトは Java8 EOL対応のフレームワーク刷新——機能追加ゼロ、既存コードを新フレームワーク上に載せ替えることが主目的の、約1,000 KLスケールの案件です。こういった「何も増やさない置き換え」では、品質の良し悪しを測る軸が通常の新規開発とは異なります。
参照先として使ったのが、IPA(独立行政法人情報処理推進機構)の**「ソフトウェア開発分析データ集2022」**です。5,546プロジェクトのデータを集計した264ページのPDFに、テストケース数・検出バグ数のベンチマーク値(中央値、P25、P75)が開発種別ごとに記載されています。
このPDFからAIエージェント(Claude Code)でほしいデータを取り出そうとしたら、想定外の壁に何度もぶつかりました。その試行錯誤ごとに学びがあったので、失敗ログも含めて記録します。
縦軸は1つ。
「感覚値の品質評価」を「根拠のある評価」に変える。
Part 1:Java8 EOL特有のKL算出
KLとは何か
KL(Kilo Lines)はソフトウェア規模の単位で、有効行数(空白行・コメント行を除いた実行可能な行数)を1,000行単位で表したものです。テスト密度もバグ密度も「件/KL」で比較するため、分母となるKLを正確に出すことが前提になります。
フレームワーク刷新特有のKL計算問題
通常の新規開発では「プロジェクト全体のソースコードの有効行数」をKLにすれば済みます。しかしフレームワーク刷新のような案件には問題があります。
このプロジェクトで本当に「作業した」コードはどこからどこまでか?
既存コードベース全体をKLにすると、今回の作業と無関係な過去の積み上げまで含まれます。かといって「差分行数だけ」を使うと、実際に動作確認した対象コードを過小評価します。
採用した方針は、**「今回の作業で新規作成または変更したファイルの有効行数を合算する」**です。
import subprocess
def count_effective_lines(filepath: str) -> int:
"""有効行数(空白行・Javaコメント行を除外)をカウント"""
count = 0
try:
with open(filepath, encoding='utf-8', errors='ignore') as f:
for line in f:
s = line.strip()
if s and not s.startswith('//') \
and not s.startswith('*') \
and not s.startswith('/*'):
count += 1
except Exception:
pass
return count
def get_changed_files(diff_filter: str, extension: str = '.java') -> list[str]:
"""Gitで変更ファイル一覧を取得。diff_filter: 'A'=追加, 'M'=変更"""
result = subprocess.run(
['git', 'diff', '--name-only', f'--diff-filter={diff_filter}',
'origin/main..HEAD'],
capture_output=True, text=True, check=True
)
return [f for f in result.stdout.strip().split('\n')
if f.endswith(extension) and f]
added_files = get_changed_files('A')
modified_files = get_changed_files('M')
kl_added = sum(count_effective_lines(f) for f in added_files) / 1000
kl_modified = sum(count_effective_lines(f) for f in modified_files) / 1000
kl_total = kl_added + kl_modified
print(f"新規 : {len(added_files):,} ファイル / {kl_added*1000:,.0f} 行 ({kl_added:.3f} KL)")
print(f"変更 : {len(modified_files):,} ファイル / {kl_modified*1000:,.0f} 行 ({kl_modified:.3f} KL)")
print(f"合計 : {kl_total:.3f} KL")
今回の実行結果:
新規 : 2,599 ファイル / 397,502 行 (397.502 KL)
変更 : 4,134 ファイル / 590,489 行 (590.489 KL)
合計 : 987.991 KL
約988 KL。これを品質指標の分母として使います。
Part 2:IPAベンチマーク値をAIエージェントで抽出する(失敗ログ付き)
IPAの「ソフトウェア開発分析データ集2022」
IPAが公開しているこのPDFは、日本のソフトウェア開発プロジェクトのデータを5,546件集計した統計集です。第5章「テスト検出バグ」には、開発種別(新規開発・改良開発・再開発)ごとに以下のデータが記載されています。
- SLOC規模あたりのテストケース数 [件/KSLOC]
- SLOC規模あたりの検出バグ数 [件/KSLOC]
- N、最小、P25、中央値、P75、最大、平均、標準偏差
フレームワーク刷新(Java8 EOL)は「再開発」または「改良開発」に近い開発種別です。この章のデータが今回の比較基準になります。
AIエージェント(Claude Code)による試行錯誤
PDF URLはわかっているので、AIエージェントに指示すれば取り出せると思っていました。しかし4段階の壁がありました。
失敗1:deep-researchワークフロー
まずdeep-researchスキルを呼び出しました。
agent({schema}): subagent completed without calling StructuredOutput
(after 2 in-conversation nudges)
ワークフロー内のサブエージェントが指定スキーマどおりに出力しないまま終了し、ワークフロー全体が落ちました。
失敗2:WebFetchでPDFを直接取得
次にWebFetchでIPA PDFのURLに直接アクセス。
The web page content appears to be a PDF file encoded in a compressed/binary
format. I cannot extract the numeric data tables from the provided content.
PDFはバイナリエンコードされており、WebFetchでは生テキストとして読めませんでした。
失敗3:Readツール → pdftoppm not found
キャッシュされたPDFファイルをReadツールで読もうとしました。
pdftoppm failed: Command 'pdftoppm' not found or is in an unsafe location
PDF→画像変換に使うpdftoppm(poppler)が実行環境に未インストールでした。
失敗4:PyMuPDF(fitz)→ モジュール未インストール
PyMuPDFライブラリで変換を試みました。
import fitz
# ModuleNotFoundError: No module named 'fitz'
成功:pdfplumber
最後にpdfplumberで試みたところ成功しました。
import pdfplumber, urllib.request, os
PDF_URL = "https://www.ipa.go.jp/digital/software-survey/metrics/" \
"hjuojm000000c6it-att/000102171.pdf"
PDF_PATH = "ipa_dev_data_2022.pdf"
# ダウンロード(初回のみ)
if not os.path.exists(PDF_PATH):
urllib.request.urlretrieve(PDF_URL, PDF_PATH)
# 第5章「テスト検出バグ」: ページ57〜76(0-indexedで56〜75)
with pdfplumber.open(PDF_PATH) as pdf:
print(f"総ページ数: {len(pdf.pages)}") # → 264
for page_idx in range(56, 77):
page = pdf.pages[page_idx]
text = page.extract_text()
tables = page.extract_tables()
print(f"\n=== ページ {page_idx + 1} ===")
if text:
print(text[:400])
if tables:
print("[テーブル]")
for table in tables:
for row in table:
# 空行をスキップして数値行だけ出力
if any(c for c in row if c and c.replace(',','').replace('.','').isdigit()):
print(row)
出力(表5-1-1 の抜粋):
総ページ数: 264
=== ページ 57 ===
5. テスト検出バグ
5.1.1 SLOC規模あたりのテストケース数、検出バグ数:全開発種別
表5-1-1 SLOC規模あたりのテストケース数、検出バグ数(全開発種別) [件/KSLOC]
[テーブル]
['結合テスト(テストケース)', '665', '0.0', '22.2', '51.7', '120.1', ...]
['総合テスト(テストケース)', '620', '0.0', '6.4', '16.0', '40.7', ...]
['結合テスト検出バグ数(現象)', '507', '0.000', '0.468', '1.111', '2.402', ...]
['総合テスト検出バグ数(現象)', '475', '0.000', '0.051', '0.197', '0.639', ...]
pdfplumberはテキストと表を独立して抽出できるため、数値テーブルをそのままDataFrameに取り込めます。
4回の失敗から見えた教訓
| 手段 | 結果 | 失敗の理由 |
|---|---|---|
| deep-researchワークフロー | ❌ | サブエージェントがStructuredOutputを未呼び出しで終了 |
| WebFetch(直接URL) | ❌ | バイナリ圧縮PDFはテキスト変換不可 |
| Readツール(pdftoppm) | ❌ | 変換コマンドが実行環境に未インストール |
| Python fitz(PyMuPDF) | ❌ | ライブラリ未インストール |
| Python pdfplumber | ✅ | テキスト+テーブルの両抽出に対応 |
「PDFからテーブルデータを取りたい」ならまずpdfplumberを試す、というのが今回の最短経路です。ただし「AIエージェントが一発で成功した」という話ではなく、ツールを変えながら失敗を重ねて成功にたどり着いた、というのが実態です。この試行錯誤のプロセス自体が実践知になります。
Part 3:実績値とIPAベンチマークの比較・評価
今回のプロジェクト実績値
プロジェクト規模 988 KL に対して:
| 指標 | 実績値 |
|---|---|
| テストケース数 | 約86,000件 |
| バグ件数(検出総数) | 1,152件 |
| テスト密度 | 87.7件/KL |
| バグ密度 | 1.17件/KL |
IPAベンチマーク値(pdfplumberで抽出)
表5-1-1〜5-1-4より。IPA統計の単位は「件/KSLOC」。本プロジェクトの「件/KL(有効行数ベース)」とは計算定義がわずかに異なりますが、オーダーレベルの比較には十分な精度です。
テストケース数 [件/KSLOC]
| 開発種別 | 中央値(結合テスト) | P75(結合テスト) | 中央値(総合テスト) | P75(総合テスト) |
|---|---|---|---|---|
| 全開発種別 | 51.7 | 120.1 | 16.0 | 40.7 |
| 新規開発 | 37.63 | 79.84 | 9.41 | 21.57 |
| 改良開発 | 67.91 | 159.51 | 24.84 | 56.75 |
| 再開発 | 39.30 | 71.22 | 13.45 | 27.29 |
検出バグ数(現象)[件/KSLOC]
| 開発種別 | 中央値(結合テスト) | P75(結合テスト) | 中央値(総合テスト) | P75(総合テスト) |
|---|---|---|---|---|
| 全開発種別 | 1.111 | 2.402 | 0.197 | 0.639 |
| 新規開発 | 1.565 | 2.613 | 0.282 | 0.829 |
| 改良開発 | 0.976 | 2.133 | 0.164 | 0.624 |
| 再開発 | 1.091 | 2.180 | 0.228 | 0.368 |
比較結果
Java8 EOL対応のフレームワーク刷新は「既存コードを新しい基盤に移し替える」という性質から再開発に最も近い開発種別です。ただし保守作業の側面もあるため、改良開発も参照値として並べて評価しました。
IPA統計は結合テストと総合テストを別々に示していますが、今回の実績値は両フェーズを合算した値です。そのため比較は「IPA中央値(結合+総合)の合算」を目安にしています。
テスト密度の評価
結合+総合テスト合算 [件/KSLOC]
─────────────────────────────────────
IPA 再開発 中央値: 52.75 P75: 98.51
IPA 改良開発 中央値: 92.75 P75: 216.26
─────────────────────────────────────────────────
今回の実績 ────────────── 87.7 ──────────────
実績87.7件/KLは、IPAの再開発中央値(52.75)を大きく上回り、P75(98.51)に迫る水準です。改良開発の中央値(92.75)とも同等です。
→ テスト量は業界水準と比較して十分に確保されていると判断できます。
バグ密度の評価
結合+総合テスト合算 [件/KSLOC]
─────────────────────────────────────
IPA 再開発 中央値: 1.319 P75: 2.548
IPA 改良開発 中央値: 1.140 P75: 2.757
─────────────────────────────────────────────────
今回の実績 ───────────── 1.17 ───────────────
実績1.17件/KLは、IPAのどの開発種別の中央値とも同等か低い水準です。再開発のP75(2.548)を大きく下回っています。
→ バグ密度は業界水準の中央値以下であり、品質は良好と評価できます。
「品質良好」の根拠をまとめる
| 指標 | 実績値 | IPA再開発(中央値) | IPA再開発(P75) | 評価 |
|---|---|---|---|---|
| テスト密度 [件/KL] | 87.7 | 52.75 | 98.51 | 中央値を大きく上回る ✅ |
| バグ密度 [件/KL] | 1.17 | 1.319 | 2.548 | 中央値以下 ✅ |
この2点が揃うと、
「テストを十分に実施した結果、バグを適切に検出・修正できており、品質は良好」
という評価を、感覚値でなく統計的根拠をもって主張できます。「感覚的には問題ないと思います」ではなく、「IPAの5,546プロジェクトデータと比較してテスト密度は再開発P75水準、バグ密度は中央値以下です」と答えられるようになります。
おわりに
今回やったことをまとめます。
| フェーズ | 手段 | 得られたもの |
|---|---|---|
| KL算出 | git diff + 有効行数カウント | 988 KLという比較の分母 |
| ベンチマーク取得 | pdfplumber(4回の失敗の後) | IPA開発種別別のテスト密度・バグ密度 |
| 品質評価 | 実績値とIPA統計の比較 | 「感覚良好」から「数字で良好」へ |
AIエージェントが一発でPDFデータを取れたわけではありません。deep-research → WebFetch → pdftoppm → fitz と4つのアプローチが失敗してから、pdfplumberでようやく成功しました。この失敗の連鎖自体が、「PDFからテーブルデータを取るならpdfplumber」という実践知として残ります。
そして、IPAのような公開統計データは「感覚値の品質評価」を「根拠のある評価」に変える最短ルートです。開発種別の選択(再開発か改良開発か)や、IPA統計のSLOC単位と実績値のKL単位の差異など、厳密には議論の余地がある部分はあります。しかしオーダーレベルの比較をする上では十分な精度があり、「業界水準と比べてどうか」という質問に、次回から数字で答えられるようになります。
参考
- IPA「ソフトウェア開発分析データ集2022」: https://www.ipa.go.jp/digital/software-survey/metrics/hjuojm000000c6it-att/000102171.pdf
- pdfplumber(PyPI): https://pypi.org/project/pdfplumber/