markharness

0016: Behavior/Condition/ExpectedResultへの前提・手順・結果モデルの導入(0015を置き換え)

ステータス

Accepted (2026-09-01)。0015Supersededに変更した。

背景

0015のPhase 1は、behavior.ymlsteps: Vec<String>(必須・順序付き配列)を導入し、同一Behavior配下の全Conditionがこのstepsを共有する設計を決定・実装した(commit 5480b85)。

Phase 1実装後、実際にこのモデルでテストケースを組み立てたところ、次の問題が発生した。

  1. 条件ごとに操作内容が異なる。例えば「TODO追加」というBehaviorに対し、「空白のみのテキストを入力する」条件と「有効なテキストを入力する」条件では、入力するテキスト自体が異なる。behavior.steps一本では、この差分を表現できず、Test Designerは無理にどちらか一方に寄せたstepsを書くか、条件間の差分をdescriptionの自然文に逃がすしかなかった。
  2. 手順だけでは到達できない前提が条件ごとに存在する。例えば「対象のTODOが既に削除されている」ことを前提とする条件は、behavior共通のstepsだけでは再現できない。
  3. 期待結果が単一の文(0015以前からのexpected_result.description)にしか対応しておらず、複数の観測可能な結果(例:「一覧に追加される」「入力欄がリセットされる」「入力欄にフォーカスが戻る」)を1つのExpectedResultにまとめて書けない、または実装詳細の理由づけとユーザー観点の結果が同じ1文に混在してしまう。
  4. 「追加の操作を挟んでから確認する」検証(例: 永続化確認のためページを再読み込みしてから確認する)を表現する手段がない。現行のgenerate.rs::load_knowledge_snapshotは1つのConditionに紐づくexpected/*.ymlを全てフラットなVec<ExpectedSnapshot>へ合流させ、TestCaseは単一のsteps: Vec<String>と単一のexpected: Vec<String>しか持たない(src/generate.rs)。そのため「reloadしてから見えるはずの結果」を、reload不要な他の結果と区別して表現できない。

これらは0015がPhase 2として想定していた「同じ手順の重複・コピー更新漏れ」問題とは異なる。Phase 2は共有Stepレジストリの必要性を扱うものだったが、今回顕在化したのはbehavior単位の共有粒度そのものが、条件ごとの実際のばらつきに対して粗すぎるという問題であり、0015のPhase 1で確定した「Behavior.stepsは全Conditionで共有する」という前提そのものが実データで崩れた。

この検討にあたり、.markharness本来のschemaを一時的に無視し、Gherkin(Given/When/Then/Background)の考え方をディレクトリ構造にそのまま反映したスクラッチサンプル(examples/bdd-sample/、本ADR確定後に削除)を作成し、何が過不足なく必要かを検討した。本ADRはその検討結果を反映する。

決定内容

1. schema変更

behavior.schema.jsonstepspreconditionsに改名し、意味を「全Conditionに共通する前提」に変更する。実際の操作手順はConditionへ完全に移す。

# behavior.yml
id: add-todo
feature: todo-management
label: TODOの追加
axis: [ui]
description: |
  フォーム送信時に入力テキストからTODOを追加する
preconditions:
  - TODOアプリを開く
  - 入力欄が空であることを確認する
# condition.yml (valid-text)
id: valid-text
behavior: add-todo
label: 有効なテキスト
description: |
  空でない有効なテキストを入力欄に入力して送信した場合
additional_preconditions: []
steps:
  - 入力欄に "牛乳を買う" と入力する
  - 「追加」ボタンをクリックする
# expected/001.yml(同じ valid-text ディレクトリ配下)
id: valid-text-001
condition: valid-text
generated_by: manual
description: 有効なテキストがTODOとして追加され、入力欄がリセットされる
results:
  - TODO一覧の末尾に "牛乳を買う" という未完了のTODOが表示される
  - 入力欄は空にリセットされる
  - 入力欄にフォーカスが戻る
implementation_note: |
  addTodo() が trim 後のテキストで {id, text, completed:false} を todos に push し、
  render() が呼ばれる。submit ハンドラが input.value = "" と input.focus() を実行する
# expected/002.yml (追加操作を挟む例)
id: valid-text-002
condition: valid-text
generated_by: manual
description: 再読み込み後もTODOが永続化されている
additional_steps:
  - ページを再読み込みする
results:
  - "牛乳を買う" のTODOが引き続き一覧に表示される
implementation_note: |
  addTodo() 内で saveTodos() が呼ばれ localStorage に保存されるため、
  再読み込み時の loadTodos() で復元される

フィールド一覧:

entity フィールド 必須度 意味
behavior preconditions(stepsから改名) Vec<String> 空配列許容(minItemsなし) 全Conditionに共通する前提
condition steps(新規) Vec<String> minItems: 1必須 条件固有の操作手順(旧behavior.stepsの粒度規約を継承)
condition additional_preconditions(新規) Vec<String> 空配列許容 条件固有の追加前提(手順だけでは到達できない前提)
expected_result description(既存、変更なし) String 必須 人間向け1文要約。生成には使わない
expected_result results(新規) Vec<String> minItems: 1必須 観測可能な複数の結果。テストケース生成に使う
expected_result additional_steps(新規) Vec<String> Condition内でファイル名順が先頭のexpected_resultのみ省略可。2番目以降は非空(1操作以上)必須 この結果を確認する前に必要な追加操作
expected_result implementation_note(新規) String 任意 実装根拠メモ。生成には使わない

expected/*.ymlのファイル分割規約: 同じ操作の後に確認する独立した複数の観測結果は、別ファイルに分割せず同じexpected_result.results配列内に複数行として書く。別ファイル(002.yml等)を作るのは、新しい操作(additional_steps)を挟んでから確認する新しいphaseを表現する場合に限る。この規約を執筆時のレビューだけに委ねず機械的に強制するため、Condition内で2番目以降(ファイル名順)のexpected_resultadditional_stepsが非空でなければならない(先頭のexpected_resultのみ省略可、または空でよい)。

この制約はexpected_result.schema.json単体のJSON Schemaでは表現できない——validate.rsvalidate_fileexpected/*.ymlを1ファイルずつ独立に検証しており、あるファイルが同じConditionディレクトリ内で何番目かをJSON Schemaは知り得ない。したがって本制約は、axisタグの参照整合性やforked_fromの参照先実在チェックと同じく、validate.rs側のクロスリファレンスチェック(Conditionディレクトリ配下のexpected/*.ymlをファイル名順に列挙し、2番目以降でadditional_stepsが空の場合にエラーとする)として実装する。これにより「追加操作なしに新しいファイルを作る」こと自体がKnowledge検証エラーとなり、2026-09-01のStandards/Specレビューが指摘した「002が独立した観測結果なのか、操作を再実行するのか、状態を保持したまま追加操作だけ行うのか」という曖昧さを構造的に排除する。

2. TestCase構造の変更(generate.rs)

TestCaseの粒度は従来通り「1 Condition = 1 TestCase」を維持する。内部構造を次のように変更する。

これにより、title/steps/expectedという既存のフラットな3フィールドは廃止し、preconditions/phasesに置き換える。

3. 命名方針

Gherkin用語(Given/When/Then/Background)は採用せず、既存スキーマの命名慣習(id/label/description/axisのような非BDD用語)に寄せる。preconditions/steps/additional_preconditions/additional_steps/results/implementation_noteはいずれもGherkin固有語彙ではない。

4. 粒度規約

0015behavior.stepsに定めた「1要素=1事実」の規約を、対象を変えて全ての新規配列フィールドへ引き継ぐ。ただし各フィールドの「1事実」の単位はフィールドの性質によって異なることを明記する。

0015同様、この規約はKnowledge検証では機械的に強制せず、Test Designerのレビュー運用に委ねる。

5. 実行モデルとの関係(自動実行エンジンではない)

execution_result.schema.jsonが定義する通り、markharnessに自動実行エンジンはなく、テストは人間のTest Executorが手順書を読んで手動で実施し、TestCase全体につき1つのpass/fail/skipのみを記録する(この点は0016前から変わらない)。したがって本ADRが導入するphasesは、人間が上から順に読んで実施する一続きの手順書であり、自動実行を前提にしたステートマシン(Phase単位の合否記録、失敗時の分岐・リトライ)ではない。Phase間の状態(reload後の永続化確認など)は、同じ人間が同じ環境で連続して手順を実施することで自然に引き継がれ、明示的な状態管理機構は不要である。teardown/cleanupの概念は本ADRでは導入しない——具体的な必要性が実データで確認された場合、0015のPhase 2以降と同じ判断基準(定量閾値を設けず、実際に不便が発生したら検討する)で別途検討する。

「自動実行のステートマシンではない」ことは、「phaseの境界に意味がない」ことを意味しない。§1でexpected/*.ymlの2番目以降にadditional_stepsを必須化したのは、自動実行の状態遷移を厳密化するためではなく、人間が手順書を読んだときに一意に読めるようにするためである。曖昧な手順書は、実行を自動化していないからこそ人間の誤読・誤実施に直結する(自動実行なら仕様の曖昧さはプログラムの分岐として顕在化するが、手動実行では読み手ごとに異なる解釈をされたまま気づかれない)。したがってadditional_stepsの必須化は、実行時のステートマシンではなく執筆時の一意性(手順書としての明確さ)を担保するための規約であり、本節の「自動実行エンジンではない」という前提とは矛盾しない。

Acceptedへ変更する条件

対象外

実装時の留意事項(本ADRでは決定しない)