"""exporter.py の単体テスト.
仕様 (SPEC_01_アーキテクチャ設計.md):
- export_results: 単一結果を CSV 出力
- export_summary: バッチ結果を集約 CSV 出力
- ensure_output_dirs: output/figures/ と output/results/ を作成
テスト方針 (GUIDE_08_テスト方針.md):
- pytest を使用
- tmp_path フィクスチャで一時ディレクトリを利用
"""
import csv
from pathlib import Path
import pytest
from src.export.exporter import (
SUMMARY_FIELDNAMES,
ensure_output_dirs,
export_results,
export_summary,
)
# ---------------------------------------------------------------------------
# ensure_output_dirs テスト
# ---------------------------------------------------------------------------
class TestEnsureOutputDirs:
"""ensure_output_dirs 関数のテスト群."""
def test_creates_figures_directory(self, tmp_path: Path) -> None:
"""正常: figures/ サブディレクトリが作成されること."""
ensure_output_dirs(str(tmp_path))
assert (tmp_path / "figures").is_dir()
def test_creates_results_directory(self, tmp_path: Path) -> None:
"""正常: results/ サブディレクトリが作成されること."""
ensure_output_dirs(str(tmp_path))
assert (tmp_path / "results").is_dir()
def test_idempotent_when_dirs_already_exist(self, tmp_path: Path) -> None:
"""正常: 既にディレクトリが存在する場合でもエラーにならないこと."""
ensure_output_dirs(str(tmp_path))
# 2 回目の呼び出しでも例外が発生しないこと
ensure_output_dirs(str(tmp_path))
assert (tmp_path / "figures").is_dir()
assert (tmp_path / "results").is_dir()
def test_creates_nested_base_directory(self, tmp_path: Path) -> None:
"""正常: base_path が存在しない場合でも自動作成されること."""
nested_base = tmp_path / "new_output"
ensure_output_dirs(str(nested_base))
assert (nested_base / "figures").is_dir()
assert (nested_base / "results").is_dir()
# ---------------------------------------------------------------------------
# export_results テスト
# ---------------------------------------------------------------------------
class TestExportResults:
"""export_results 関数のテスト群."""
@pytest.fixture
def sample_results(self) -> dict:
"""テスト用の均一性指標辞書を返す fixture."""
return {
"cov": 0.05,
"std": 10.0,
"max_min_ratio": 1.2,
"mean": 200.0,
"max": 220.0,
"min": 180.0,
}
def test_csv_file_is_created(self, sample_results: dict, tmp_path: Path) -> None:
"""正常: CSV ファイルが生成されること."""
output_path = str(tmp_path / "results.csv")
export_results(sample_results, output_path)
assert Path(output_path).exists()
def test_csv_has_correct_header(self, sample_results: dict, tmp_path: Path) -> None:
"""正常: CSV の 1 行目にヘッダーが含まれること."""
output_path = str(tmp_path / "results.csv")
export_results(sample_results, output_path)
with open(output_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
fieldnames = reader.fieldnames
assert set(fieldnames) == set(sample_results.keys())
def test_csv_has_correct_values(self, sample_results: dict, tmp_path: Path) -> None:
"""正常: CSV の値が正しく書き込まれていること."""
output_path = str(tmp_path / "results.csv")
export_results(sample_results, output_path)
with open(output_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
rows = list(reader)
assert len(rows) == 1
row = rows[0]
assert float(row["cov"]) == pytest.approx(0.05)
assert float(row["std"]) == pytest.approx(10.0)
assert float(row["mean"]) == pytest.approx(200.0)
assert float(row["max"]) == pytest.approx(220.0)
assert float(row["min"]) == pytest.approx(180.0)
def test_creates_parent_directory_automatically(
self, sample_results: dict, tmp_path: Path
) -> None:
"""正常: 出力ディレクトリが存在しない場合に自動作成されること."""
nested_path = str(tmp_path / "results" / "uniformity.csv")
export_results(sample_results, nested_path)
assert Path(nested_path).exists()
# ---------------------------------------------------------------------------
# export_summary テスト
# ---------------------------------------------------------------------------
class TestExportSummary:
"""export_summary 関数のテスト群."""
@pytest.fixture
def sample_all_results(self) -> list[dict]:
"""テスト用の複数画像の均一性指標リストを返す fixture."""
return [
{
"image_name": "image_001",
"mean": 200.0,
"std": 10.0,
"cov": 0.05,
"max_min_ratio": 1.2,
"max": 220.0,
"min": 180.0,
},
{
"image_name": "image_002",
"mean": 150.0,
"std": 20.0,
"cov": 0.133,
"max_min_ratio": 1.5,
"max": 200.0,
"min": 100.0,
},
]
def test_csv_file_is_created(
self, sample_all_results: list[dict], tmp_path: Path
) -> None:
"""正常: CSV ファイルが生成されること."""
output_path = str(tmp_path / "summary.csv")
export_summary(sample_all_results, output_path)
assert Path(output_path).exists()
def test_csv_has_correct_summary_fieldnames(
self, sample_all_results: list[dict], tmp_path: Path
) -> None:
"""正常: CSV のヘッダーが SUMMARY_FIELDNAMES と一致すること."""
output_path = str(tmp_path / "summary.csv")
export_summary(sample_all_results, output_path)
with open(output_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
fieldnames = reader.fieldnames
assert list(fieldnames) == SUMMARY_FIELDNAMES
def test_csv_has_correct_row_count(
self, sample_all_results: list[dict], tmp_path: Path
) -> None:
"""正常: CSV の行数が入力リストのサイズと一致すること."""
output_path = str(tmp_path / "summary.csv")
export_summary(sample_all_results, output_path)
with open(output_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
rows = list(reader)
assert len(rows) == len(sample_all_results)
def test_csv_first_row_values_are_correct(
self, sample_all_results: list[dict], tmp_path: Path
) -> None:
"""正常: CSV の 1 行目の値が正しいこと."""
output_path = str(tmp_path / "summary.csv")
export_summary(sample_all_results, output_path)
with open(output_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
rows = list(reader)
first = rows[0]
assert first["image_name"] == "image_001"
assert float(first["mean"]) == pytest.approx(200.0)
assert float(first["std"]) == pytest.approx(10.0)
def test_csv_second_row_values_are_correct(
self, sample_all_results: list[dict], tmp_path: Path
) -> None:
"""正常: CSV の 2 行目の値が正しいこと."""
output_path = str(tmp_path / "summary.csv")
export_summary(sample_all_results, output_path)
with open(output_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
rows = list(reader)
second = rows[1]
assert second["image_name"] == "image_002"
assert float(second["mean"]) == pytest.approx(150.0)
def test_summary_fieldnames_constant_has_correct_keys(self) -> None:
"""正常: SUMMARY_FIELDNAMES 定数が期待されるキーを持つこと."""
expected = ["image_name", "mean", "std", "cov", "max_min_ratio", "max", "min"]
assert SUMMARY_FIELDNAMES == expected
def test_creates_parent_directory_automatically(
self, sample_all_results: list[dict], tmp_path: Path
) -> None:
"""正常: 出力ディレクトリが存在しない場合に自動作成されること."""
nested_path = str(tmp_path / "results" / "summary.csv")
export_summary(sample_all_results, nested_path)
assert Path(nested_path).exists()
def test_empty_list_creates_header_only_csv(self, tmp_path: Path) -> None:
"""エッジケース: 空リストを渡した場合にヘッダーのみの CSV が生成されること."""
output_path = str(tmp_path / "empty_summary.csv")
export_summary([], output_path)
with open(output_path, encoding="utf-8") as f:
reader = csv.DictReader(f)
rows = list(reader)
assert len(rows) == 0
assert list(reader.fieldnames) == SUMMARY_FIELDNAMES