コンテンツにスキップ

テストデータ管理ガイド

[!NOTE] 最新の実装状況は 機能実装ステータス (Remaining Functionality) を参照してください。

概要

EvoSpikeNetプロジェクトでは、テストデータの管理に以下の戦略を採用しています:

  1. 動的生成: 小さく単純なテストデータはfixtureで動的生成
  2. Git LFS: 大きなテストデータはGit LFSで管理
  3. 自動セットアップ: conftest.pyでテスト環境を自動構築

テストデータの分類

動的生成(推奨)

対象: - ファイルサイズ < 1 MB - ランダムデータや単純なパターン - 生成時間 < 100ms - 再現性が不要なデータ

メリット: - リポジトリサイズが小さい - 常に最新の形式 - 依存関係なし - メンテナンスが容易

例:

@pytest.fixture
def dummy_audio_data(test_data_dir):
    """Generate dummy audio data dynamically."""
    import wave
    import struct

    audio_file = test_data_dir / "generated_audio.wav"

    # Generate 1 second of audio at 16kHz
    sample_rate = 16000
    duration = 1.0
    frequency = 440.0  # A4 note
    num_samples = int(sample_rate * duration)

    # Generate sine wave
    samples = []
    for i in range(num_samples):
        value = int(32767.0 * 0.3 * np.sin(2.0 * np.pi * frequency * i / sample_rate))
        samples.append(value)

    # Write WAV file
    with wave.open(str(audio_file), 'w') as wav_file:
        wav_file.setnchannels(1)
        wav_file.setsampwidth(2)
        wav_file.setframerate(sample_rate)
        wav_file.writeframes(struct.pack('h' * len(samples), *samples))

    return audio_file

Git LFS管理

対象: - ファイルサイズ > 1 MB - 実データやリアルなサンプル - 生成に時間がかかるデータ - 再現性が必要なデータ

メリット: - リアルなテストケース - 一貫性のあるテスト - CI/CDの高速化(生成不要)

デメリット: - LFSストレージが必要 - セットアップが必要 - 更新の管理が必要

例:

@pytest.fixture
def large_pretrained_model():
    """Load large pre-trained model from Git LFS."""
    model_file = Path(__file__).parent / "data" / "large_model_weights.pt"

    if not model_file.exists():
        pytest.skip("Large model file not available. Run: git lfs pull")

    import torch, inspect

    load_kwargs = {"map_location": "cpu"}
    try:
        sig = inspect.signature(torch.load)
        if "weights_only" in sig.parameters:
            load_kwargs["weights_only"] = True
    except Exception:
        pass

    weights = torch.load(model_file, **load_kwargs)
    return weights

利用可能なFixture

セッションスコープ

test_data_dir

一時的なテストデータディレクトリを提供します。セッションごとに作成され、自動的にクリーンアップされます。

def test_example(test_data_dir):
    # test_data_dirは Path オブジェクト
    my_file = test_data_dir / "mydata.txt"
    my_file.write_text("test content")

setup_test_environment

テスト環境を自動セットアップします(autouse=True)。以下のディレクトリを作成: - models/: モデルファイル用 - artifacts/: アーティファクト用 - logs/: ログファイル用 - cache/: キャッシュファイル用

環境変数も設定: - TEST_DATA_ROOT: テストデータのルートパス - TEST_MODE: "true"に設定

関数スコープ

Audio/Video

dummy_audio_data(test_data_dir) - 1秒間の440Hz正弦波(A4音) - 16kHz サンプリングレート - モノラル、16ビット - WAV形式

def test_audio_processing(dummy_audio_data):
    import wave
    with wave.open(str(dummy_audio_data), 'r') as f:
        assert f.getnchannels() == 1
        assert f.getframerate() == 16000

Text

dummy_text_corpus(test_data_dir) - 複数行のテキストコーパス - UTF-8エンコーディング - テキストファイルとして保存

def test_text_tokenization(dummy_text_corpus):
    with open(dummy_text_corpus, 'r') as f:
        text = f.read()
    # テキスト処理のテスト

Images

dummy_image_data(test_data_dir) - MNIST形式の画像(28x28 グレースケール) - バッチサイズ: 4 - PyTorch tensor または NumPy array - ファイルも保存(.pt または .npy)

@pytest.mark.requires_torch
def test_image_classification(dummy_image_data):
    images = dummy_image_data['tensor']
    assert images.shape == (4, 1, 28, 28)

Neural Data

dummy_spike_train(test_data_dir) - 100ニューロン × 1000タイムステップ - スパース(5%の発火確率) - バイナリ(0/1) - NumPy uint8形式

def test_spike_analysis(dummy_spike_train):
    spikes = dummy_spike_train['data']
    assert spikes.shape == (1000, 100)
    assert spikes.dtype == np.uint8

dummy_embeddings(test_data_dir) - 100サンプル × 128次元 - 正規化済みベクトル - NumPy float32形式

def test_similarity_search(dummy_embeddings):
    embeddings = dummy_embeddings['data']
    # 各ベクトルは正規化されている
    norms = np.linalg.norm(embeddings, axis=1)
    assert np.allclose(norms, 1.0, atol=1e-5)

Structured Data

dummy_csv_data(test_data_dir) - 簡単なCSVデータ - ヘッダー付き - 数値とカテゴリカルデータ

def test_data_loading(dummy_csv_data):
    import pandas as pd
    df = pd.read_csv(dummy_csv_data)
    assert len(df) == 4
    assert 'label' in df.columns

Model Weights

dummy_model_weights(test_data_dir) - 簡単なニューラルネットワークの重み - 2層(64→32ニューロン) - PyTorch state_dict または NumPy dict

@pytest.mark.requires_torch
def test_model_loading(dummy_model_weights):
    weights = dummy_model_weights['weights']
    assert 'layer1.weight' in weights
    assert weights['layer1.weight'].shape == (64, 128)

Multimodal

mock_multimodal_data(dummy_image_data, dummy_audio_data, dummy_text_corpus) - 画像、音声、テキストを組み合わせ - マルチモーダルテスト用

def test_multimodal_processing(mock_multimodal_data):
    image = mock_multimodal_data['image']
    audio = mock_multimodal_data['audio']
    text = mock_multimodal_data['text']
    # マルチモーダル処理のテスト

動的サイズ対応

large_test_dataset(test_data_dir, request) - マーカーでサイズを指定可能 - Git LFS対応(ファイルがあれば使用) - なければ動的生成

@pytest.mark.dataset_size("medium")
def test_with_medium_dataset(large_test_dataset):
    data = large_test_dataset['data']
    # dataは1000サンプル
    assert len(data) == 1000

@pytest.mark.dataset_size("large")
def test_with_large_dataset(large_test_dataset):
    data = large_test_dataset['data']
    # dataは10000サンプル(Git LFSから読み込みまたは生成)
    assert len(data) == 10000

Git LFSセットアップ

初期セットアップ

# Git LFSをインストール
brew install git-lfs  # macOS
apt-get install git-lfs  # Ubuntu/Debian

# リポジトリでGit LFSを有効化
git lfs install

# LFSファイルをダウンロード
git lfs pull

新しいファイルをLFSに追加

# 大きなテストデータを作成
python scripts/generate_test_data.py --size large --output tests/data/large_dataset_large.npy

# Git LFSで追跡
git lfs track "tests/data/large_dataset_large.npy"

# コミット(.gitattributesも一緒に)
git add .gitattributes tests/data/large_dataset_large.npy
git commit -m "Add large test dataset (Git LFS)"
git push

LFSファイルの確認

# LFSで追跡されているファイルを確認
git lfs ls-files

# LFSストレージの使用状況
git lfs status

ベストプラクティス

1. 小さいデータは動的生成

# ✅ 良い例: 動的生成
@pytest.fixture
def small_test_data():
    return np.random.randn(100, 10)

# ❌ 悪い例: 小さいデータをファイルに保存
@pytest.fixture
def small_test_data():
    return np.load("tests/data/small_data.npy")  # 不要

2. 大きいデータはGit LFS

# ✅ 良い例: Git LFSから読み込み
@pytest.fixture
def large_pretrained_model():
    model_file = Path(__file__).parent / "data" / "pretrained_model.pt"
        if model_file.exists():
            import inspect
            load_kwargs = {"map_location": "cpu"}
            try:
                sig = inspect.signature(torch.load)
                if "weights_only" in sig.parameters:
                    load_kwargs["weights_only"] = True
            except Exception:
                pass
            return torch.load(model_file, **load_kwargs)
    pytest.skip("Model not available")

# ❌ 悪い例: 大きいデータを動的生成(遅い)
@pytest.fixture
def large_pretrained_model():
    # 毎回トレーニング(数分かかる)
    return train_large_model()

3. テンポラリディレクトリを使用

# ✅ 良い例: test_data_dirを使用
def test_file_writing(test_data_dir):
    output_file = test_data_dir / "output.txt"
    output_file.write_text("test")
    # 自動クリーンアップ

# ❌ 悪い例: カレントディレクトリに書き込み
def test_file_writing():
    with open("output.txt", "w") as f:
        f.write("test")
    # クリーンアップ忘れ

4. Fixtureの再利用

# ✅ 良い例: 既存のfixtureを組み合わせ
@pytest.fixture
def prepared_dataset(dummy_image_data, test_data_dir):
    images = dummy_image_data['tensor']
    # 前処理
    normalized = (images - images.mean()) / images.std()
    return normalized

# ❌ 悪い例: すべて一から実装
@pytest.fixture
def prepared_dataset(test_data_dir):
    # 画像生成から全部やり直し
    images = torch.rand(4, 1, 28, 28)  # dummy_image_dataと重複
    normalized = (images - images.mean()) / images.std()
    return normalized

5. 条件付きスキップ

# ✅ 良い例: LFSファイルがない場合はスキップ
@pytest.fixture
def real_world_data():
    data_file = Path(__file__).parent / "data" / "real_data.npy"
    if not data_file.exists():
        pytest.skip("Real data not available (run 'git lfs pull')")
    return np.load(data_file)

# ❌ 悪い例: ファイルがなければエラー
@pytest.fixture
def real_world_data():
    data_file = Path(__file__).parent / "data" / "real_data.npy"
    return np.load(data_file)  # FileNotFoundError

CI/CDでの考慮事項

GitHub Actions

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout with LFS
        uses: actions/checkout@v3
        with:
          lfs: true

      - name: Pull LFS files (selective)
        run: |
          # 小さいファイルのみ取得(コスト削減)
          git lfs pull --include="tests/data/small_*.npy"

      - name: Run tests
        run: |
          # LFSファイルが必要なテストはスキップ
          pytest -m "not requires_lfs" tests/

ローカル開発

# LFSなしでクローン(高速)
GIT_LFS_SKIP_SMUDGE=1 git clone <repo>
cd <repo>

# 必要なLFSファイルのみ取得
git lfs pull --include="tests/data/specific_file.npy"

# または全LFSファイルを取得
git lfs pull

トラブルシューティング

LFSファイルがダウンロードされない

# LFSファイルの状態確認
git lfs ls-files

# 強制的に再ダウンロード
git lfs fetch --all
git lfs checkout

テストが「file not found」で失敗

# Fixtureでスキップを実装
@pytest.fixture
def optional_large_data():
    data_file = Path(__file__).parent / "data" / "large_data.npy"
    if not data_file.exists():
        pytest.skip("Large data not available. Run: git lfs pull")
    return np.load(data_file)

ストレージクォータ超過

# 古いLFSファイルをクリーンアップ
git lfs prune

# 特定のファイルのみ追跡解除
git lfs untrack "tests/data/old_file.npy"
git rm tests/data/old_file.npy
git add .gitattributes
git commit -m "Remove old LFS file"

参考リンク