C テストフレームワーク (testfw) と同様に、.NET テストプロジェクトでも個別のテストケースごとに詳細な結果ログを生成する機能を設計する。
C テストフレームワークは以下の仕組みで results を生成している:
get_test_code_c_cpp.awk)
insert_summary.awk)
[状態] - テストの前提条件[手順] - 実行手順[Pre-Assert手順] - Assert 前の手順[確認] - Assert による確認内容[Pre-Assert確認] - Pre-Assert による確認exec_test_c_cpp.sh)
results/<test_id>/results.log に以下を出力:
results/all_tests/summary.log に全体サマリを出力exec_test_dotnet.sh は以下の機能を提供:
dotnet test の一括実行 (TRX ロガーによる結果収集)results/<TestClass>.<TestMethod>/results.log への個別ログ生成results/all_tests/summary.log への全体サマリ出力[手順]、[確認] などのタグによるサマリ生成test/src/calc.net/CalcLib.Tests/
├── results/
│ ├── all_tests/
│ │ └── summary.log # 全体サマリ
│ ├── CalcLibraryTests.Add_ShouldReturnCorrectResult/
│ │ ├── results.log # データセット全体のログ
│ │ ├── a_10_b_20_expected_30.log # 個別パラメータのログ (オプション)
│ │ ├── a_-5_b_5_expected_0.log
│ │ └── ...
│ ├── CalcLibraryTests.Divide_ByZero_ShouldReturnError/
│ │ └── results.log
│ └── ...
注記: Theory テストの個別パラメータログは、必要に応じて実装する。
Running test: CalcLibraryTests.Add_ShouldReturnCorrectResult
----
## テスト項目
### 状態
### 手順
- CalcLibrary.Add(a, b) を呼び出す。
### 確認内容 (3)
- 結果が成功であること。
- 期待値と一致すること。
- エラーコードが 0 であること。
----
[Theory]
[InlineData(10, 20, 30)]
[InlineData(-5, 5, 0)]
[InlineData(0, 0, 0)]
[InlineData(100, -50, 50)]
[InlineData(-10, -20, -30)]
public void Add_ShouldReturnCorrectResult(int a, int b, int expected)
{
var result = CalcLibrary.Add(a, b); // [手順] - CalcLibrary.Add(a, b) を呼び出す。
Assert.True(result.IsSuccess); // [確認] - 結果が成功であること。
Assert.Equal(expected, result.Value); // [確認] - 期待値と一致すること。
Assert.Equal(0, result.ErrorCode); // [確認] - エラーコードが 0 であること。
}
----
dotnet test --filter "FullyQualifiedName=CalcLib.Tests.CalcLibraryTests.Add_ShouldReturnCorrectResult"
成功 CalcLib.Tests.CalcLibraryTests.Add_ShouldReturnCorrectResult(a: 10, b: 20, expected: 30) [2 ms]
成功 CalcLib.Tests.CalcLibraryTests.Add_ShouldReturnCorrectResult(a: -5, b: 5, expected: 0) [< 1 ms]
成功 CalcLib.Tests.CalcLibraryTests.Add_ShouldReturnCorrectResult(a: 0, b: 0, expected: 0) [< 1 ms]
成功 CalcLib.Tests.CalcLibraryTests.Add_ShouldReturnCorrectResult(a: 100, b: -50, expected: 50) [< 1 ms]
成功 CalcLib.Tests.CalcLibraryTests.Add_ShouldReturnCorrectResult(a: -10, b: -20, expected: -30) [< 1 ms]
テストの実行に成功しました。
テストの合計数: 5
成功: 5
ファイル名: testfw/cmnd/get_test_code_dotnet.py
機能:
- .NET テストファイル (.cs) から特定のテストメソッドを抽出
- メソッド直前の XML コメントや通常のコメントを含める
- xUnit の属性 ([Fact], [Theory], [InlineData]) を含める
入力:
- テストファイルパス (.cs)
- テストクラス名
- テストメソッド名
出力:
- 抽出されたテストコード (標準出力)
実装例:
#!/usr/bin/env python3
import sys
import re
def extract_test_code(file_path, class_name, method_name):
"""
.NET テストファイルからテストメソッドを抽出する
Args:
file_path: テストファイルのパス
class_name: テストクラス名
method_name: テストメソッド名
"""
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
# クラス内部のフラグ
in_class = False
# メソッド検出フラグ
in_method = False
# 括弧のカウント
brace_count = 0
# バッファ (コメント用)
buffer = []
# 出力フラグ
output_started = False
for line in lines:
# クラス定義を検出
if re.search(rf'class\s+{re.escape(class_name)}\s*', line):
in_class = True
continue
if not in_class:
continue
# コメント行をバッファに追加
if re.match(r'^\s*(//|/\*|\*)', line):
buffer.append(line)
continue
# 空行でバッファをクリア (メソッド検出前のみ)
if re.match(r'^\s*$', line):
if not in_method:
buffer = []
continue
# 属性行をバッファに追加
if re.match(r'^\s*\[', line):
buffer.append(line)
continue
# メソッド定義を検出
if re.search(rf'\s+{re.escape(method_name)}\s*\(', line):
in_method = True
output_started = True
# バッファの内容を出力
for buf_line in buffer:
sys.stdout.write(buf_line)
buffer = []
sys.stdout.write(line)
brace_count += line.count('{') - line.count('}')
continue
# メソッド内の処理
if in_method:
sys.stdout.write(line)
brace_count += line.count('{') - line.count('}')
# メソッド終了を検出
if brace_count <= 0:
break
else:
# メソッド外はバッファをクリア
buffer = []
if not output_started:
print(f"Error: Test method '{class_name}.{method_name}' not found.", file=sys.stderr)
sys.exit(1)
if __name__ == '__main__':
if len(sys.argv) != 4:
print("Usage: get_test_code_dotnet.py <file_path> <class_name> <method_name>", file=sys.stderr)
sys.exit(1)
extract_test_code(sys.argv[1], sys.argv[2], sys.argv[3])ファイル名: testfw/cmnd/insert_summary_dotnet.py
機能:
- insert_summary.awk の Python 移植
- コード内の [状態]、[手順]、[確認] などのタグを検出
- マークダウン形式のサマリを生成
入力:
- テストコード (標準入力)
出力:
- サマリ付きテストコード (標準出力)
実装例:
#!/usr/bin/env python3
import sys
import re
def trim(s):
"""先頭の空白1つと末尾の空白群を削除"""
s = re.sub(r'^ ', '', s, count=1)
s = re.sub(r'[ \t]+$', '', s)
return s
def is_list_item(s):
"""リスト項目かどうかを判定"""
return bool(re.match(r'^[-*+]|^[0-9]+\.', s))
def insert_summary():
"""標準入力からテストコードを読み込み、サマリを挿入して標準出力"""
lines = sys.stdin.readlines()
# カテゴリ別の配列
state = []
act = []
pre_step = []
pre_chk = []
asrt_chk = []
check_count = 0
# 各行を解析してタグを検出
for line in lines:
# [状態]
match = re.search(r'\[状態\]', line)
if match:
s = trim(line[match.end():])
if s:
state.append(s)
continue
# [手順]
match = re.search(r'\[手順\]', line)
if match:
s = trim(line[match.end():])
if s:
act.append(s)
continue
# [Pre-Assert手順]
match = re.search(r'\[Pre-Assert手順\]', line)
if match:
s = trim(line[match.end():])
if s:
pre_step.append(s)
continue
# [Pre-Assert確認]
match = re.search(r'\[Pre-Assert確認\]', line)
if match:
s = trim(line[match.end():])
if s:
pre_chk.append(s)
if is_list_item(s):
check_count += 1
continue
# [確認]
match = re.search(r'\[確認\]', line)
if match:
s = trim(line[match.end():])
if s:
asrt_chk.append(s)
if is_list_item(s):
check_count += 1
continue
# サマリ項目が存在するかチェック
has_summary = len(state) > 0 or len(act) > 0 or len(pre_step) > 0 or len(pre_chk) > 0 or len(asrt_chk) > 0
# サマリを出力
if has_summary:
print("## テスト項目")
# --- 状態 ---
print("\n### 状態\n")
for s in state:
print(s)
# --- 手順 ---
if len(state) > 0:
print("\n### 手順\n")
else:
print("### 手順\n")
for s in act:
print(s)
for s in pre_step:
print(s)
# --- 確認内容 ---
if len(act) > 0 or len(pre_step) > 0:
print(f"\n### 確認内容 ({check_count})\n")
else:
print(f"### 確認内容 ({check_count})\n")
for s in pre_chk:
print(s)
for s in asrt_chk:
print(s)
print("----")
# 元のコードを出力
for line in lines:
sys.stdout.write(line)
if __name__ == '__main__':
insert_summary()機能:
- dotnet test --list-tests でテスト一覧を取得
- dotnet test を1回だけ一括実行 (TRX ロガーで結果を記録)
- parse_trx_results.py で TRX XML を解析し、テストごとの Passed/Failed を取得
- extract_dotnet_output.py でバッチ出力から個別テスト結果を抽出
- get_test_code_dotnet.py と insert_summary_dotnet.py を使用してログ生成
- results/<TestClass>.<TestMethod>/results.log に出力
一括実行の流れ:
function list_tests() {
dotnet test --list-tests --no-build -c "$CONFIG" -o "$OUTPUT_DIR" 2>/dev/null | \
grep -E '^\s+' | \
sed -e 's/^[ \t]*//'
}
function run_all_tests_batch() {
# 1. テスト一覧取得 (パラメータ付きテストは重複除去)
local tests=$(list_tests | sed 's/(.*//' | sort -u)
# 2. dotnet test を1回だけ実行 (TRX ロガー付き)
dotnet test --no-build -c "$CONFIG" -o "$OUTPUT_DIR" \
--verbosity normal \
--logger "trx;LogFileName=results.trx" \
--results-directory "$trx_dir" > "$batch_output" 2>&1
# 3. TRX を解析してテストごとの結果を取得
python3 "$SCRIPT_DIR/parse_trx_results.py" "$trx_file" > "$trx_results"
# 4. 各テストについてループ:
# - テストコード抽出 + サマリ生成
# - バッチ出力から該当テスト分を抽出
# - results.log に保存
for test in $tests; do
# ...
python3 "$SCRIPT_DIR/extract_dotnet_output.py" "$batch_output" "$test_id" "$test_result"
done
}パフォーマンス改善:
- 従来: テストごとに dotnet test --filter を個別起動 (1回あたり約1.7秒のランタイム起動オーバーヘッド)
- 現在: dotnet test を1回だけ実行し、結果をパースして同一の results 構造を生成
本設計の実現性を検証するため、以下の項目について実装と動作確認を行った。
get_test_code_dotnet.py の検証:
- ✅ .NET テストファイルから特定のメソッドを正確に抽出できることを確認
- ✅ [Theory] 属性と [InlineData] を含めて抽出できることを確認
- ✅ クラス検出とメソッド検出のロジックが適切に動作することを確認
insert_summary_dotnet.py の検証:
- ✅ [手順] タグから手順を抽出できることを確認
- ✅ [確認] タグから確認内容を抽出できることを確認
- ✅ マークダウン形式のサマリが正しく生成されることを確認
検証コマンド例:
python3 get_test_code_dotnet.py CalcLibraryTests.cs CalcLibraryTests Add_ShouldReturnCorrectResult | \
python3 insert_summary_dotnet.py検証コマンド:
dotnet test --filter "FullyQualifiedName~CalcLibraryTests.Add_ShouldReturnCorrectResult" \
--no-build -c RelWithDebInfo --verbosity normal結果:
- ✅ Theory テストの全データセット (5件) を実行できることを確認
- ✅ フィルター機能が期待通りに動作することを確認
- ✅ テスト結果が適切に出力されることを確認
検証対象: test/src/calc.net/CalcLib.Tests/CalcLibraryTests.cs
確認事項:
- ✅ [手順] と [確認] のタグが既に適切に記述されている
- ✅ コメントの書き方が統一されている
- ✅ 設計書の例と実際のコードが一致している
結論: 実現性は非常に高い (95%)。技術的な障壁はほぼなく、設計通りの実装が可能。
課題: dotnet test で個別のテストケースを実行する方法
対策 (フェーズ1): --filter オプションを使用した個別実行
dotnet test --filter "FullyQualifiedName~ClassName.MethodName"対策 (フェーズ1.5): 一括実行 + TRX パースに変更
dotnet test --no-build --verbosity normal \
--logger "trx;LogFileName=results.trx" \
--results-directory "$trx_dir"個別実行では1回あたり約1.7秒のランタイム起動オーバーヘッドが発生するため、
一括実行方式に変更し、TRX XML のパースで個別テスト結果を取得する。
検証済み: ✅ 一括実行で生成される results.log は、合計時間と個別テスト実行時間を除き従来版と同一
課題: Theory テストは複数のデータセットで実行される
dotnet test –list-tests の出力例:
CalcLib.Tests.CalcLibraryTests.Add_ShouldReturnCorrectResult(a: 10, b: 20, expected: 30)
CalcLib.Tests.CalcLibraryTests.Add_ShouldReturnCorrectResult(a: -5, b: 5, expected: 0)
...
対策:
- 初期実装では、Theory メソッド全体で1つの results.log を生成
- パラメータ部分を除去して基本メソッド名を取得: sed 's/(.*//'
- --filter に ~ (部分一致) を使用して全データセットを実行
- 将来的には、各データセットごとに個別ログを生成することも検討 (フェーズ2)
検証済み: ✅ パラメータ付きテスト名からメソッド名を抽出できる
課題: テストクラス名からテストファイルのパスを特定する必要がある
対策:
- find コマンドで .cs ファイルを検索
- 命名規則 (ClassName.cs) に従っていることを前提
test_file=$(find . -name "${class_name}.cs" -type f | head -1)検証済み: ✅ CalcLibraryTests.cs はこの規則に従っている
課題: 完全修飾名から名前空間、クラス名、メソッド名を分離する
完全修飾名の例: CalcLib.Tests.CalcLibraryTests.Add_ShouldReturnCorrectResult
- 名前空間: CalcLib.Tests
- クラス名: CalcLibraryTests
- メソッド名: Add_ShouldReturnCorrectResult
対策:
namespace_and_class="${fully_qualified_name%.*}" # 最後の '.' より前
method_name="${fully_qualified_name##*.}" # 最後の '.' より後
class_name="${namespace_and_class##*.}" # 最後の '.' より後検証済み: ✅ bash の文字列操作で適切に分離できる
課題: C# と C/C++ では構文が異なる
C# 特有の要素:
- XML ドキュメントコメント (///)
- 属性 ([Fact], [Theory], [InlineData])
- #region / #endregion
- #pragma warning
対策:
- Python スクリプトで C# の構文に対応したパーサーを実装
- 正規表現で属性 ([Fact], [Theory]) やメソッド定義を検出
- XML コメント (///) のサポートを追加
改善点:
if re.match(r'^\s*(//|/\*|\*|///)', line):
buffer.append(line)
continue
if re.match(r'^\s*#pragma', line):
if not in_method:
buffer.append(line)
continue| リスク | 影響 | 確率 | 緩和策 |
|---|---|---|---|
| Theory テストの扱いが複雑 | 中 | 低 | フェーズ1では全データセットを1ログに集約 |
| テストファイル検出の失敗 | 中 | 低 | find コマンドでの検索 + エラーハンドリング |
| .NET のバージョン差異 | 低 | 低 | .NET 9.0 で動作確認済み |
| カバレッジツールとの統合 | 高 | 中 | フェーズ1では除外、フェーズ2で検討 |
| 既存の exec_test_dotnet.sh の破壊 | 高 | 低 | 従来版との results 差分検証で同一性を確認 |
設計書の実装例に以下の改善を提案:
if re.match(r'^\s*(//|/\*|\*|///)', line):
buffer.append(line)
continue
if re.match(r'^\s*#pragma', line):
if not in_method:
buffer.append(line)
continue一括実行方式を採用:
#!/bin/bash
function run_all_tests_batch() {
# 1. テスト一覧取得
# 2. dotnet test を1回だけ一括実行 (TRX ロガー付き)
# 3. parse_trx_results.py で TRX を解析
# 4. 各テストについて:
# - テストコード抽出 + サマリ生成
# - extract_dotnet_output.py でバッチ出力から該当テスト分を抽出
# - results.log に保存
# 5. サマリ表示 + バナー
}results/
├── all_tests/
│ └── summary.log # 全体サマリ (SUCCESS/FAILURE 集計)
├── <TestClass>.<TestMethod>/
│ └── results.log # テストコード + サマリ + 実行結果
└── (将来) coverage/ # カバレッジ情報 (フェーズ2)
[状態], [手順], [確認] タグの抽出が正常に動作testfw/cmnd/get_test_code_dotnet.py (211行)testfw/cmnd/insert_summary_dotnet.py (155行)testfw/cmnd/exec_test_dotnet.sh (237行、既存のバックアップも保存)dotnet test を1回だけ一括実行し、TRX ロガーで結果を記録summary.log: 日時のみ異なるresults.log: 合計時間 と個別テスト実行時間 [XX ms] のみ異なるtestfw/cmnd/parse_trx_results.py (TRX XML パーサー)testfw/cmnd/extract_dotnet_output.py (バッチ出力抽出)testfw/cmnd/exec_test_c_cpp.sh - C/C++ テスト実行スクリプトtestfw/cmnd/get_test_code_c_cpp.awk - テストコード抽出 (AWK)testfw/cmnd/insert_summary_c_cpp.awk - サマリ生成 (AWK)testfw/cmnd/exec_test_dotnet.sh - .NET テスト実行スクリプト (一括実行)testfw/cmnd/get_test_code_dotnet.py - .NET テストコード抽出testfw/cmnd/insert_summary_dotnet.py - .NET サマリ生成testfw/cmnd/parse_trx_results.py - TRX XML パーサーtestfw/cmnd/extract_dotnet_output.py - バッチ出力から個別テスト結果を抽出test/src/calc.net/CalcLib.Tests/CalcLibraryTests.cs - サンプルテストtest/src/calc.net/CalcLib.Tests/results/summary.log - 現状の全体サマリテストコード内のコメントに以下のタグを記述:
[状態] - テストの前提条件[手順] - 実行手順 (Act)[Pre-Assert手順] - Assert 前の手順[確認] - Assert による確認内容[Pre-Assert確認] - Pre-Assert による確認例:
var result = CalcLibrary.Add(a, b); // [手順] - CalcLibrary.Add(a, b) を呼び出す。
Assert.True(result.IsSuccess); // [確認] - 結果が成功であること。
Assert.Equal(expected, result.Value); // [確認] - 期待値と一致すること。本設計により、.NET テストでも C テストフレームワークと同様の詳細な results 生成が可能となる。これにより、以下のメリットが得られる: