Back to list
sunbluesome

bdd-feature

by sunbluesome

0🍴 0📅 Jan 13, 2026

SKILL.md


name: bdd-feature description: | Use immediately when user mentions: "テスト", "test", "BDD", "feature", "フィーチャー", "scenario", "シナリオ", "Given", "When", "Then", "カバレッジ", "coverage", "pytest-bdd", "Gherkin".

MUST USE this skill for:

  • Creating BDD feature files with Gherkin syntax
  • Implementing step definitions with pytest-bdd
  • Designing test scenarios and fixtures
  • Any work in tests/ directory

BDD Testing スキル

配置ルール

tests/
├── features/           # Gherkin Feature files
│   └── <module>/       # src構造をミラー
│       └── <name>.feature
├── src/                # Step definitions
│   └── <module>/
│       └── test_<name>.py
└── conftest.py         # 共通fixtures

基本構造

Feature ファイル

Feature: 販売記録DTOのバリデーション
  販売記録が正しく構築・検証されることを確認する

  # 正常系
  Scenario: 有効なデータでDTOを作成
    Given 以下の販売データがある
      | seller_id | sku_id | date       | quantity |
      | user1     | itemA  | 2023-01-01 | 10       |
    When SalesRecordを作成する
    Then DTOが正常に作成される

  # 異常系
  Scenario: 必須カラム欠落でエラー
    Given quantity列が欠落した販売データがある
    When SalesRecordを作成する
    Then ValueError が発生する
    And エラーメッセージに "Missing required columns" が含まれる

Step definitions

from pytest_bdd import given, when, then, scenarios, parsers

scenarios("schemas/s_sales_record.feature")

class Context:
    data: dict | None = None
    result: Any = None
    error: Exception | None = None

@pytest.fixture
def context() -> Context:
    return Context()

@given("以下の販売データがある", target_fixture="context")
def step_given_data(context: Context, datatable: list[list[str]]) -> Context:
    context.data = parse_datatable(datatable)
    return context

@when("SalesRecordを作成する")
def step_create_dto(context: Context) -> None:
    try:
        context.result = SalesRecord(**context.data)
    except Exception as e:
        context.error = e

@then("DTOが正常に作成される")
def step_check_success(context: Context) -> None:
    assert context.error is None
    assert isinstance(context.result, SalesRecord)

@then(parsers.parse("{error_type} が発生する"))
def step_check_error(context: Context, error_type: str) -> None:
    assert context.error is not None
    assert type(context.error).__name__ == error_type

テストカテゴリ【必須網羅】

重要: 全コンポーネントで「正常系」「境界値」「異常系」の3カテゴリを必ず網羅すること。

必須チェックリスト

各コンポーネント実装時に以下を確認:

□ 正常系: 基本動作が正しく動くか?(最低1ケース)
□ 境界値: 端の値で正しく動くか?(0, 1, 空, 最大値など)
□ 異常系: 不正入力でエラーが発生するか?(null, inf, 欠落など)【最重要】

メソッドレベルのテスト網羅【最重要】

原則: 処理の最小単位はメソッド/関数であり、各メソッドに対して正常系・境界値・異常系を網羅する。

なぜメソッドレベルか

  • コンポーネントレベルでは異常系があっても、特定メソッドの異常系が漏れる可能性がある
  • メソッドごとにテストがあれば、意図せぬ動作を防げる
  • リファクタリング時の回帰テストとして機能する

メソッドレベルテストのチェックリスト

各メソッドに対して以下を確認:

□ 正常系: 期待通りの入力で正しく動作するか?
□ 境界値: 引数の端の値で正しく動作するか?
□ 異常系: 不正入力でエラーが発生するか?

例: select_forecast_day(day: int) メソッド

# 正常系
Scenario: forecast_day=7で予測を抽出
  Given 有効な予測データがある
  When forecast_day=7で予測を抽出する
  Then PeriodForecastが返される

# 境界値
Scenario: forecast_day=1(最小値)で予測を抽出
  ...

Scenario: forecast_day=365(最大値)で予測を抽出
  ...

# 異常系(このメソッド固有のエラーケース)
Scenario: forecast_day=0でエラー
  When forecast_day=0で予測を抽出する
  Then ValueError が発生する

Scenario: forecast_day=-1でエラー
  When forecast_day=-1で予測を抽出する
  Then ValueError が発生する

Scenario: 存在しないforecast_dayカラムでエラー
  Given day1とday7のみの予測データがある
  When forecast_day=30で予測を抽出する
  Then ColumnNotFoundError が発生する

異常系テストの徹底【最重要】

目的: どのような入力でエラーが発生するかを明確にし、エラーハンドリングの網羅性を保証する。

異常系テストが重要な理由

  1. システムの堅牢性: 不正入力に対する挙動を明確化
  2. エラーメッセージの品質: ユーザーが問題を特定できるか確認
  3. エラー伝播の検証: 例外がどこでキャッチされるか確認
  4. ドキュメント代わり: テストがエラー条件の仕様書になる

異常系テスト必須項目

全コンポーネントで以下を必ずテストすること:

カテゴリテスト内容エラー例必須度
必須カラム欠落必要なカラムが全てないDataFrameColumnNotFoundError必須
必須引数欠落必要な引数がNone/未指定ValueError, TypeError必須
無効なフォーマット日付形式不正、型不一致ValueError必須
範囲外の値0以下、上限超過、負値ValueError範囲制約時
存在しないリソースファイル不存在、キー不存在FileNotFoundError, KeyError該当時
null/None値必須フィールドがnullValueError or 特定動作必須
inf/nan値無限大・非数値ValueError or 特定動作数値処理時

カラム欠落テストの徹底【必須】

原則: 必須カラムが複数ある場合、全カラムについて欠落テストを行う

なぜ全カラムか

  • 1カラムだけテストしても、他のカラム欠落時の挙動が保証されない
  • 各カラムへのアクセスパスが異なる可能性がある
  • 全カラムテストで網羅性を確保する

実装方法: pytest.mark.parametrize

import pytest
from pytest_bdd import scenarios, given, when, then, parsers

scenarios("schemas/s_records.feature")

# 必須カラムを定義
REQUIRED_COLUMNS = ["sku_id", "date", "quantity", "product_code_id"]


@pytest.mark.parametrize("missing_column", REQUIRED_COLUMNS)
def test_missing_column_raises_error(missing_column: str) -> None:
    """各必須カラム欠落時にエラーが発生することを確認"""
    # missing_column以外のカラムでDataFrame作成
    columns = {col: [...] for col in REQUIRED_COLUMNS if col != missing_column}
    data = pl.DataFrame(columns).lazy()

    dto = SalesRecords(data=data)

    with pytest.raises(ColumnNotFoundError):
        # 欠落カラムにアクセスするとエラー
        _ = dto.data.select(pl.col(missing_column)).collect()

実装方法: Featureファイルでの列挙

# === 異常系: 必須カラム欠落 ===
# Note: 全必須カラムについてテストすること

Scenario Outline: <column>カラム欠落でエラー
  Given <column>カラムが欠落したデータがある
  When DTOを作成してcollectする
  Then ColumnNotFoundError が発生する

  Examples:
    | column              |
    | sku_id              |
    | date                |
    | quantity            |
    | product_code_id     |

Step定義でのループ処理

@given(parsers.parse("{column}カラムが欠落したデータがある"), target_fixture="context")
def step_given_missing_column(context: Context, column: str) -> Context:
    """指定カラムが欠落したデータを作成"""
    all_columns = {
        "sku_id": ["sku001"],
        "date": [date(2024, 1, 1)],
        "quantity": [10],
        "product_code_id": ["prod001"],
    }
    # 指定カラムを除外
    columns = {k: v for k, v in all_columns.items() if k != column}
    context.data = pl.DataFrame(columns).lazy()
    return context

DTOごとのカラム欠落テスト必須項目

DTO必須カラムテスト数
SalesRecordssku_id, date, quantity, product_code_id4件
PeriodSalessku_id, actual_quantity_cum2件
CumulativeForecastWidesku_id, base_date_later_day*2件
SkuErrorssku_id, error, smoothed_actual, is_top_20pct4件

異常系テストの書き方

# === 異常系 ===
# 注: どんな入力でどんなエラーが発生するかを明確に

Scenario: 必須カラム欠落でエラー
  Given sku_idカラムが欠落したデータがある
  When 処理を実行する
  Then ColumnNotFoundError が発生する
  And エラーメッセージに "sku_id" が含まれる

Scenario: 無効な日付形式でエラー
  Given base_date="invalid-date"である
  When Processorを作成する
  Then ValueError が発生する
  And エラーメッセージに "Invalid date format" が含まれる

Scenario: 範囲外の値でエラー
  Given forecast_day=0である
  When select_forecast_day()を呼ぶ
  Then ValueError が発生する
  And エラーメッセージに "must be between 1 and 365" が含まれる

Scenario: 存在しないカラム指定でエラー
  Given day=999の予測カラムが存在しない
  When select_forecast_day(999)を呼ぶ
  Then ColumnNotFoundError が発生する

エラーメッセージの検証

異常系テストでは、エラーの種類だけでなくメッセージ内容も検証すること:

@then(parsers.parse('エラーメッセージに "{text}" が含まれる'))
def step_then_error_message_contains(context: Context, text: str) -> None:
    assert context.error is not None
    assert text in str(context.error)

LazyFrameのエラー検出

Polars LazyFrameを使用する場合、エラーはcollect()時に発生する。 Whenステップで必ずcollectしてエラーを捕捉すること:

@when("処理を実行する")
def step_when_process(context: Context) -> None:
    try:
        context.result = processor.process(context.data)
        # LazyFrameのエラーはcollect時に発生
        _ = context.result.data.collect()
    except Exception as e:
        context.error = e

正常系(Happy Path)【必須】

カテゴリテスト内容必須度
基本作成有効なデータでDTO作成必須
基本処理正常データでの処理結果確認必須
オプショナル省略可能フィールドの扱い該当時
複数データリスト・バッチ処理該当時

境界値(Boundary)【必須】

カテゴリテスト内容必須度
空データ (n=0)空入力での動作確認必須
単一データ (n=1)1件データでの動作確認必須
ゼロ値0での動作確認数値処理時
最小値下限値での動作確認範囲制約時
最大値上限値での動作確認範囲制約時

異常系(Error Cases)【必須】

カテゴリテスト内容必須度
必須カラム欠落カラム欠落時のエラー必須
null値null含むデータの挙動必須
無効値inf, nan, 負値の挙動数値処理時
型間違い期待と異なる型該当時
範囲外許容範囲外の値範囲制約時

数値・行列系

カテゴリテスト内容
スカラー境界0, 1, 1e10, inf, -inf, nan
ベクトル空, 長さ不一致, ゼロベクトル
行列形状(0,n), (n,0), 行数不一致
数値精度pytest.approx, 非負制約

日付系

カテゴリテスト内容
年境界12/31→1/1, 週番号の切り替え
月境界月末→月初, 月初フラグ
特殊日うるう年2/29, 祝日
曜日・週isoweekday, 週開始日truncate
# 異常系の例
Scenario Outline: 無効な数量でエラー
  Given 数量が <value> の販売データがある
  When SalesRecordを作成する
  Then ValueError が発生する

  Examples:
    | value   |
    | null    |
    | -1      |
    | inf     |
    | nan     |

コンポーネント別テスト指針【3カテゴリ必須】

全コンポーネントで正常系・境界値・異常系を必ず実装すること。

対象正常系境界値異常系
DTO作成・プロパティ空データ, n=1null/inf/nan/カラム欠落
Processor変換の正確性空入力, n=1, ゼロ値DTO未担保の異常系のみ
Calculator計算結果確認空データ, n=1, ゼロ値空データでNaN返却
Transformerfit/transform整合性空データ, n=1未fitでtransform
Modelfit/predict動作空データ, n=1未fitでpredict
PipelineE2E処理空入力, 最小データ途中エラーの伝播

異常系テストの責務分担【重要】

原則: DTOでバリデーションされるエラー条件は、DTOテストで網羅すればよい。Processorで重複テストは不要。

DTOで担保されるエラー → DTOテストのみ(Processor側で重複不要)
Processor固有のエラー → Processorテストで実装

DTOで担保される異常系(Processorテスト不要)

以下のエラー条件はDTOのバリデーションでカバーされるため、Processorテストでは省略可能:

エラー条件DTOで担保Processorテスト
必須カラム欠落DTO.model_validator省略可
カラム型不一致DTO.SCHEMA省略可
必須フィールドnullDTO.model_validator省略可

Processor固有の異常系(テスト必須)

以下のエラー条件はDTOでは担保されず、Processorテストで実装が必要:

エラー条件テスト必須
Processor引数の不正outlier_percentile=-0.1必須
処理条件の不正forecast_day=0必須
処理ロジック固有のエラーデータなしでquantile計算必須

判断フローチャート

異常系テストを書く際の判断:
1. このエラーはDTOのバリデーションで発生するか?
   → Yes: DTOテストで網羅済みならProcessorテスト省略可
   → No: Processorテストで実装必須

2. Processorが追加のバリデーションをするか?
   → Yes: そのバリデーションをテスト
   → No: DTOテストに委譲

例: ForecastActualJoiner

# ForecastActualJoinerが受け取るDTO
# - PeriodForecast: sku_id, predicted_quantity
# - PeriodSales: sku_id, actual_quantity_cum

# DTOで担保される異常系(Joinerテスト不要):
# - sku_id欠落 → PeriodForecast/PeriodSalesのテストで網羅
# - predicted_quantity欠落 → PeriodForecastのテストで網羅

# Joiner固有の異常系(テスト必須):
# - なし(Joinerは単純なleft joinのみ)

Processor必須テスト一覧

# 正常系(必須)
Scenario: 正常データで処理
  Given 有効なデータがある
  When 処理を実行する
  Then 結果が正しい

# 境界値(必須)
Scenario: 空データで処理
  Given 空のデータがある
  When 処理を実行する
  Then 空の結果が返る(またはエラー)

Scenario: 1件のみで処理
  Given 1件のデータがある
  When 処理を実行する
  Then 結果が正しい

# 異常系(必須)
Scenario: 必須カラム欠落でエラー
  Given 必須カラムが欠落したデータがある
  When 処理を実行する
  Then ColumnNotFoundError が発生する

詳細リファレンス

Score

Total Score

40/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

0/10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon