デシジョンテーブル(決定表)とは、複数の条件の組み合わせと、そのとき行う動作を1枚の表にまとめたものです。 条件が2つ、3つと重なる仕様を文章だけで読むと抜けに気づきにくいのですが、表にすると「この組み合わせのときはどうなるのか」が1列ずつ目で追えるようになります。

この記事では、条件2個から4個までの例5題を、Markdownの表と、表計算ソフトに貼れるタブ区切りの両方で載せます。規則数の計算、起こり得ない組み合わせの除外、「-」でまとめる簡単化を1手ずつ表で追い、Pythonで全組み合わせを作って簡単化の前後で結果が変わらないことを検算します。最後に、決定表をpytestのテストコードへ写す方法と実行結果を示します。

確認環境:Python 3.11.15/pytest 9.1.1(Linux)。掲載したPythonコードはすべて実行し、出力をそのまま載せています。用語と記号は JIS X 0125:1986「決定表」に沿っています。

デシジョンテーブルの早見表

知りたいこと 答え
表の構成 上半分が条件、下半分が動作。縦の1列が1つの規則(条件の組み合わせと動作の組)
条件の欄に書く記号 Y(満たす)、N(満たさない)、-(無関係、または起こり得ない)、#(起こり得ないことを強調)
動作の欄に書く記号 X(実行する)、-(実行しない)
規則数の計算 YとNだけの条件がn個なら 2のn乗。3つ以上の値を持つ条件があれば、各条件の値の数を掛け合わせる
条件3個・4個の規則数 3個で8、4個で16
簡単化のやり方 動作が同じで、条件が1つだけ違う2列を探し、その条件を「-」にして1列にまとめる
簡単化の検算 全組み合わせを作り、どの組み合わせも「ちょうど1つの規則」に当てはまり、動作が元と同じかを確かめる
テストケースの数 簡単化後の1規則につき最低1件。余裕があれば「-」をYとNに展開して全組み合わせ
向いている場面 複数の条件の組み合わせで結果が変わる仕様(権限、割引、受付の可否など)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「デシジョンテーブルの書き方と例5題|条件の簡単化とテストケースの作り方」

デシジョンテーブルの構成と記号(JIS X 0125の用語早見表)

決定表は4つの部分でできています。左上に条件の文、左下に動作の文、右上にY・N、右下にXを書きます。

左側(記述部) 右側(指定部)
上半分 条件記述部:考慮する条件を並べる 条件指定部:規則ごとに Y・N・- を書く
下半分 動作記述部:実行する動作を並べる 動作指定部:規則ごとに X・- を書く

JIS X 0125 で定義されている用語を、よく使うものに絞って一覧にします。

用語(JIS X 0125) 英語 意味
決定表 decision table 起こり得るすべての条件と、それに対して実行すべき動作を組み合わせた表
条件 condition 問題の記述で考慮する事柄
動作 action 実行すべき処理
規則 rule 条件指定部と動作指定部を通る1つの列。条件の組み合わせ1つと、対応する動作の組
条件記述部 condition stub 考慮するすべての条件を並べた部分
動作記述部 action stub 実行するすべての動作を並べた部分
条件指定 condition entry ある条件と特定の規則との関連付け(Y・N・- など)
動作指定 action entry ある動作と特定の規則との関連付け(X・- など)
表見出し table heading 他の文書から参照するための表の名前や説明
制限指定表 limited entry table 条件・動作の文が記述部だけで完成している表(指定部は Y・N・X)
拡張指定表 extended entry table 記述部の文を、指定部に書く語句や値で完成させる表
混合指定表 mixed entry table 制限指定と拡張指定の両方を含む表
完備した表 complete table すべての条件の組み合わせに対して、当てはまる規則がある表
単適合決定表 single-hit decision table どの条件の組み合わせも、ただ1つの規則にだけ当てはまる表
多重適合決定表 multiple-hit decision table 2つ以上の規則に当てはまる組み合わせが少なくとも1つある表
補完規則 ELSE-rule 値で指定していない残りの組み合わせをまとめて受ける規則

指定部に書く記号の意味は次のとおりです。

欄 記号 意味
条件指定 Y この条件を満たすとき、規則に当てはまる
条件指定 N この条件を満たさないとき、規則に当てはまる
条件指定 - この条件は無関係、または論理的に起こり得ない
条件指定 # 起こり得ないことを強調したいときに「-」の代わりに書く
条件指定 語句・値 拡張指定。「配送先」の欄に「近隣」と書く、など
動作指定 X この動作を実行する
動作指定 - この動作を実行しない
動作指定 語句・値 拡張指定。「送料(円)」の欄に「400」と書く、など

テスト設計では、単適合で完備した表(どの組み合わせも、ちょうど1つの規則に当てはまる表)を目指します。この記事の検算も、この2点を確かめています。

デシジョンテーブルの書き方(6つの手順)

手順 すること 確かめること
1 仕様から条件を抜き出し、条件記述部に並べる 1つの条件に「かつ」「または」が混ざっていないか
2 動作を抜き出し、動作記述部に並べる 「何もしない」「エラーを表示する」も動作として書いたか
3 規則数を計算する 各条件の値の数を掛け合わせた数になっているか
4 全組み合わせを条件指定部に書く 上の条件から順に、Yを半分・Nを半分と機械的に並べたか
5 列ごとに動作を決めて X を書く 仕様に書かれていない組み合わせが見つかったら、仕様の確認事項として記録する
6 起こり得ない組み合わせを除き、簡単化する 簡単化の前後で動作が変わっていないか(検算する)

手順5で「仕様に書かれていない組み合わせ」が見つかることが、決定表を作るいちばんの収穫です。空欄のまま埋められない列は、仕様を決める人に確認する材料になります。

規則数の計算(2のn乗と、値の数の掛け算)

YとNの2つの値だけを持つ条件がn個あるとき、規則数は2のn乗です。

条件の数 1 2 3 4 5 6 7 8 9 10
規則数(2のn乗) 2 4 8 16 32 64 128 256 512 1024

3つ以上の値を持つ条件(拡張指定)がある場合は、各条件の値の数を掛け合わせます。たとえば「配送先(近隣・遠方・離島)」「注文額が3,000円以上(Y・N)」「プレミアム会員(Y・N)」なら 3 × 2 × 2 = 12 です。

この記事の例5題の規則数は、次のようになります(後で示す検算スクリプトの出力と同じ数です)。

例 条件の数 計算 全組み合わせ 起こり得ない 簡単化後
例1 備品予約の受付 2 2 × 2 4 0 3
例2 図書の貸出 3 2 × 2 × 2 8 0 4
例3 送料の決定 3 3 × 2 × 2 12 0 7
例4 ログイン 4 2 × 2 × 2 × 2 16 7 5
例5 会議室の予約 4 2 × 2 × 2 × 2 16 0 5

条件が6個で64列、7個で128列になります。1枚の表は条件4〜5個までを目安にし、それを超えるときは「受付できるかどうかの表」と「料金を決める表」のように表を分けると読みやすくなります。

例1:備品予約の受付(条件2個)

仕様:利用者登録が済んでいない人は登録画面へ案内する。登録済みの人は、在庫があれば予約を確定し、在庫がなければ「在庫なし」を表示する。

仕様をPythonの関数で書くと次のとおりです(後の検算で使います。先頭の import は examples.py の1行目です)。

from dt import YN

# ---- 例1: 備品予約の受付(条件2個)----
C1 = [("利用者登録が済んでいる", YN), ("在庫がある", YN)]
A1 = ["予約を確定する", "「在庫なし」を表示する", "利用者登録の画面へ案内する"]
def spec1(reg, stock):
    if reg == "N":
        return ("利用者登録の画面へ案内する",)
    return ("予約を確定する",) if stock == "Y" else ("「在庫なし」を表示する",)
S1 = [(("Y", "Y"), ("予約を確定する",)),
      (("Y", "N"), ("「在庫なし」を表示する",)),
      (("N", "-"), ("利用者登録の画面へ案内する",))]

全組み合わせ(2 × 2 = 4規則)

規則 1 2 3 4
利用者登録が済んでいる Y Y N N
在庫がある Y N Y N
予約を確定する X - - -
「在庫なし」を表示する - X - -
利用者登録の画面へ案内する - - X X

規則3と規則4は、動作が同じで「在庫がある」だけが違います。在庫の有無は結果に関係しないので「-」にまとめます。

簡単化後(3規則)

規則 1 2 3
利用者登録が済んでいる Y Y N
在庫がある Y N -
予約を確定する X - -
「在庫なし」を表示する - X -
利用者登録の画面へ案内する - - X
規則  1   2   3
利用者登録が済んでいる Y   Y   N
在庫がある   Y   N   -
予約を確定する X   -   -
「在庫なし」を表示する -   X   -
利用者登録の画面へ案内する   -   -   X

例2:図書の貸出(条件3個)と簡単化の1手ずつ

仕様:利用者証が有効でなければ更新を案内する。有効でも、延滞中の本があれば返却を依頼する。延滞がなくても、貸出冊数が上限に達していれば上限を案内する。どれにも当たらなければ貸出を行う。

# ---- 例2: 図書の貸出(条件3個)----
C2 = [("利用者証が有効", YN), ("延滞中の本がない", YN), ("貸出冊数が上限未満", YN)]
A2 = ["貸出を行う", "利用者証の更新を案内する", "延滞中の本の返却を依頼する", "貸出上限を案内する"]
def spec2(card, no_overdue, under_limit):
    if card == "N":
        return ("利用者証の更新を案内する",)
    if no_overdue == "N":
        return ("延滞中の本の返却を依頼する",)
    if under_limit == "N":
        return ("貸出上限を案内する",)
    return ("貸出を行う",)
S2 = [(("Y", "Y", "Y"), ("貸出を行う",)),
      (("Y", "Y", "N"), ("貸出上限を案内する",)),
      (("Y", "N", "-"), ("延滞中の本の返却を依頼する",)),
      (("N", "-", "-"), ("利用者証の更新を案内する",))]

全組み合わせ(2 × 2 × 2 = 8規則)

規則 1 2 3 4 5 6 7 8
利用者証が有効 Y Y Y Y N N N N
延滞中の本がない Y Y N N Y Y N N
貸出冊数が上限未満 Y N Y N Y N Y N
貸出を行う X - - - - - - -
利用者証の更新を案内する - - - - X X X X
延滞中の本の返却を依頼する - - X X - - - -
貸出上限を案内する - X - - - - - -

簡単化を1手ずつ追う

簡単化の規則は1つだけです。「動作の列がまったく同じ」で「条件が1つだけ違う」2列を見つけたら、その条件を「-」にして1列にする。 これを、まとめられる組がなくなるまで繰り返します。以下の表は、後で載せる merge_steps 関数が出力した順番どおりです(条件の並びを Y・N の3文字で表します。YNY は「有効・延滞あり・上限未満」)。

1手目:YNY と YNN をまとめて YN- にする(8規則 → 7規則)。延滞中の本があるときは、上限未満かどうかに関係なく返却を依頼するからです。

規則 1 2 3 4 5 6 7
利用者証が有効 Y Y Y N N N N
延滞中の本がない Y Y N Y Y N N
貸出冊数が上限未満 Y N - Y N Y N
貸出を行う X - - - - - -
利用者証の更新を案内する - - - X X X X
延滞中の本の返却を依頼する - - X - - - -
貸出上限を案内する - X - - - - -

2手目:NYY と NYN をまとめて NY- にする(7規則 → 6規則)。

規則 1 2 3 4 5 6
利用者証が有効 Y Y Y N N N
延滞中の本がない Y Y N Y N N
貸出冊数が上限未満 Y N - - Y N
貸出を行う X - - - - -
利用者証の更新を案内する - - - X X X
延滞中の本の返却を依頼する - - X - - -
貸出上限を案内する - X - - - -

3手目:NNY と NNN をまとめて NN- にする(6規則 → 5規則)。

規則 1 2 3 4 5
利用者証が有効 Y Y Y N N
延滞中の本がない Y Y N Y N
貸出冊数が上限未満 Y N - - -
貸出を行う X - - - -
利用者証の更新を案内する - - - X X
延滞中の本の返却を依頼する - - X - -
貸出上限を案内する - X - - -

4手目:NY- と NN- をまとめて N-- にする(5規則 → 4規則)。「-」の位置が同じ列どうしは、さらにまとめられます。

規則 1 2 3 4
利用者証が有効 Y Y Y N
延滞中の本がない Y Y N -
貸出冊数が上限未満 Y N - -
貸出を行う X - - -
利用者証の更新を案内する - - - X
延滞中の本の返却を依頼する - - X -
貸出上限を案内する - X - -

これ以上まとめられる組はありません。規則1(貸出を行う)と規則2(貸出上限を案内する)は条件が1つだけ違いますが、動作が違うのでまとめません。

規則  1   2   3   4
利用者証が有効 Y   Y   Y   N
延滞中の本がない    Y   Y   N   -
貸出冊数が上限未満   Y   N   -   -
貸出を行う   X   -   -   -
利用者証の更新を案内する    -   -   -   X
延滞中の本の返却を依頼する   -   -   X   -
貸出上限を案内する   -   X   -   -

例3:送料の決定(3つの値を持つ条件・拡張指定)

仕様:プレミアム会員は送料0円。それ以外は、配送先(近隣・遠方・離島)と注文額(3,000円以上かどうか)で送料が決まる。

# ---- 例3: 送料の決定(条件3個・3つの値を持つ条件を含む)----
C3 = [("配送先", ["近隣", "遠方", "離島"]), ("注文額が3,000円以上", YN), ("プレミアム会員", YN)]
A3 = ["送料(円)"]
FEE = {("近隣", "Y"): 0, ("近隣", "N"): 400, ("遠方", "Y"): 300,
       ("遠方", "N"): 600, ("離島", "Y"): 800, ("離島", "N"): 1100}
def spec3(area, over, premium):
    if premium == "Y":
        return ("0",)
    return (str(FEE[(area, over)]),)
S3 = [(("-", "-", "Y"), ("0",)),
      (("近隣", "Y", "N"), ("0",)),
      (("近隣", "N", "N"), ("400",)),
      (("遠方", "Y", "N"), ("300",)),
      (("遠方", "N", "N"), ("600",)),
      (("離島", "Y", "N"), ("800",)),
      (("離島", "N", "N"), ("1100",))]

条件に3つの値があり、動作も金額で書くので、拡張指定(指定部に語句や値を書く形)を使います。全組み合わせは 3 × 2 × 2 = 12規則です。

規則 1 2 3 4 5 6 7 8 9 10 11 12
配送先 近隣 近隣 近隣 近隣 遠方 遠方 遠方 遠方 離島 離島 離島 離島
注文額が3,000円以上 Y Y N N Y Y N N Y Y N N
プレミアム会員 Y N Y N Y N Y N Y N Y N
送料(円) 0 0 0 400 0 300 0 600 0 800 0 1100

プレミアム会員がYの6列(規則1・3・5・7・9・11)は、配送先にも注文額にも関係なく0円です。まず注文額を「-」にして3列にし、次に配送先を「-」にして1列にします。3つの値を持つ条件を「-」にできるのは、3つの値すべてで動作が同じときだけです(近隣と遠方だけが同じ、という場合は「-」にできません)。

簡単化後(7規則)

規則 1 2 3 4 5 6 7
配送先 - 近隣 近隣 遠方 遠方 離島 離島
注文額が3,000円以上 - Y N Y N Y N
プレミアム会員 Y N N N N N N
送料(円) 0 0 400 300 600 800 1100

規則1と規則2はどちらも0円ですが、条件が2つ以上違うので1列にはまとめられません。

規則  1   2   3   4   5   6   7
配送先 -   近隣  近隣  遠方  遠方  離島  離島
注文額が3,000円以上    -   Y   N   Y   N   Y   N
プレミアム会員 Y   N   N   N   N   N   N
送料(円)   0   0   400 300 600 800 1100

例4:ログイン(条件4個・起こり得ない組み合わせの除外)

仕様:登録されていないアカウントには「IDまたはパスワードが違います」を表示する。登録済みでロック中なら「ロック中です」を表示する。ロック中でなくパスワードが一致しなければ、同じメッセージを表示して誤入力の回数を1増やす。パスワードが一致したら、2段階認証が有効なら確認コードの入力画面へ進み、無効ならログインを完了する。

# ---- 例4: ログイン(条件4個・起こり得ない組み合わせあり)----
C4 = [("アカウントが登録されている", YN), ("パスワードが一致する", YN),
      ("アカウントがロック中", YN), ("2段階認証が有効", YN)]
A4 = ["「IDまたはパスワードが違います」を表示する", "誤入力の回数を1増やす",
      "「ロック中です」を表示する", "確認コードの入力画面へ進む", "ログインを完了する"]
def spec4(exists, pw, locked, mfa):
    if exists == "N":
        # 登録されていないアカウントには、パスワードもロックも2段階認証の設定も存在しない
        if "Y" in (pw, locked, mfa):
            return None
        return ("「IDまたはパスワードが違います」を表示する",)
    if locked == "Y":
        return ("「ロック中です」を表示する",)
    if pw == "N":
        return ("「IDまたはパスワードが違います」を表示する", "誤入力の回数を1増やす")
    return ("確認コードの入力画面へ進む",) if mfa == "Y" else ("ログインを完了する",)
S4 = [(("Y", "Y", "N", "Y"), ("確認コードの入力画面へ進む",)),
      (("Y", "Y", "N", "N"), ("ログインを完了する",)),
      (("Y", "N", "N", "-"), ("「IDまたはパスワードが違います」を表示する", "誤入力の回数を1増やす")),
      (("Y", "-", "Y", "-"), ("「ロック中です」を表示する",)),
      (("N", "-", "-", "-"), ("「IDまたはパスワードが違います」を表示する",))]

登録されていないアカウントには、パスワードも、ロックの状態も、2段階認証の設定もありません。つまり「アカウントが登録されている=N」で、ほかの条件のどれかがYになる組み合わせは論理的に起こり得ません。関数では None を返して区別しています。

全組み合わせ(2 × 2 × 2 × 2 = 16規則。うち7規則は起こり得ない)

規則 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
アカウントが登録されている Y Y Y Y Y Y Y Y N N N N N N N N
パスワードが一致する Y Y Y Y N N N N Y Y Y Y N N N N
アカウントがロック中 Y Y N N Y Y N N Y Y N N Y Y N N
2段階認証が有効 Y N Y N Y N Y N Y N Y N Y N Y N
「IDまたはパスワードが違います」を表示する - - - - - - X X - - - - - - - X
誤入力の回数を1増やす - - - - - - X X - - - - - - - -
「ロック中です」を表示する X X - - X X - - - - - - - - - -
確認コードの入力画面へ進む - - X - - - - - - - - - - - - -
ログインを完了する - - - X - - - - - - - - - - - -
起こり得ない組み合わせ # # # # # # #

起こり得ない7列(規則9〜15)を除くと9列が残ります。ここから簡単化します。

  • 規則1・2・5・6(登録済みでロック中)は、パスワードと2段階認証に関係なく同じ動作 → Y-Y- の1列
  • 規則7・8(登録済み・ロック中でない・パスワード不一致)は、2段階認証に関係なく同じ動作 → YNN- の1列
  • 規則16(未登録)は、ほかの3条件が起こり得ないので「-」にする → N--- の1列

簡単化後(5規則)

規則 1 2 3 4 5
アカウントが登録されている Y Y Y Y N
パスワードが一致する Y Y N - -
アカウントがロック中 N N N Y -
2段階認証が有効 Y N - - -
「IDまたはパスワードが違います」を表示する - - X - X
誤入力の回数を1増やす - - X - -
「ロック中です」を表示する - - - X -
確認コードの入力画面へ進む X - - - -
ログインを完了する - X - - -

規則5の「-」は「無関係」ではなく「起こり得ない」の意味です。JIS X 0125 では、これを強調したいときに「-」の代わりに「#」を書けます(N### と書く)。規則3は1つの規則で2つの動作にXが付く例です。

規則  1   2   3   4   5
アカウントが登録されている   Y   Y   Y   Y   N
パスワードが一致する  Y   Y   N   -   -
アカウントがロック中  N   N   N   Y   -
2段階認証が有効    Y   N   -   -   -
「IDまたはパスワードが違います」を表示する  -   -   X   -   X
誤入力の回数を1増やす -   -   X   -   -
「ロック中です」を表示する   -   -   -   X   -
確認コードの入力画面へ進む   X   -   -   -   -
ログインを完了する   -   X   -   -   -

例5:会議室の予約(条件4個)

仕様:希望の時間帯が空いていなければ別の時間帯を案内する。空いていても定員を超えるなら別の部屋を案内する。定員以内で、利用時間が2時間以内、または申請者が管理者なら予約を確定する。それ以外(2時間を超え、管理者でもない)は承認依頼を送る。

# ---- 例5: 会議室の予約(条件4個)----
C5 = [("希望の時間帯が空いている", YN), ("利用人数が定員以内", YN),
      ("利用時間が2時間以内", YN), ("申請者が管理者", YN)]
A5 = ["予約を確定する", "承認依頼を送る", "別の部屋を案内する", "別の時間帯を案内する"]
def spec5(free, capacity, short, admin):
    if free == "N":
        return ("別の時間帯を案内する",)
    if capacity == "N":
        return ("別の部屋を案内する",)
    if short == "Y" or admin == "Y":
        return ("予約を確定する",)
    return ("承認依頼を送る",)
S5 = [(("Y", "Y", "Y", "-"), ("予約を確定する",)),
      (("Y", "Y", "N", "Y"), ("予約を確定する",)),
      (("Y", "Y", "N", "N"), ("承認依頼を送る",)),
      (("Y", "N", "-", "-"), ("別の部屋を案内する",)),
      (("N", "-", "-", "-"), ("別の時間帯を案内する",))]

全組み合わせ(2 × 2 × 2 × 2 = 16規則)

規則 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
希望の時間帯が空いている Y Y Y Y Y Y Y Y N N N N N N N N
利用人数が定員以内 Y Y Y Y N N N N Y Y Y Y N N N N
利用時間が2時間以内 Y Y N N Y Y N N Y Y N N Y Y N N
申請者が管理者 Y N Y N Y N Y N Y N Y N Y N Y N
予約を確定する X X X - - - - - - - - - - - - -
承認依頼を送る - - - X - - - - - - - - - - - -
別の部屋を案内する - - - - X X X X - - - - - - - -
別の時間帯を案内する - - - - - - - - X X X X X X X X

簡単化後(5規則)

規則 1 2 3 4 5
希望の時間帯が空いている Y Y Y Y N
利用人数が定員以内 Y Y Y N -
利用時間が2時間以内 Y N N - -
申請者が管理者 - Y N - -
予約を確定する X X - - -
承認依頼を送る - - X - -
別の部屋を案内する - - - X -
別の時間帯を案内する - - - - X
規則  1   2   3   4   5
希望の時間帯が空いている    Y   Y   Y   Y   N
利用人数が定員以内   Y   Y   Y   N   -
利用時間が2時間以内  Y   N   N   -   -
申請者が管理者 -   Y   N   -   -
予約を確定する X   X   -   -   -
承認依頼を送る -   -   X   -   -
別の部屋を案内する   -   -   -   X   -
別の時間帯を案内する  -   -   -   -   X

規則1と規則2はどちらも「予約を確定する」です。もう1列にまとめて YY-- と書きたくなりますが、そうすると YYNN(2時間を超え、管理者でもない)まで含んでしまい、規則3と重なります。次の検算で、この「まとめすぎ」が数字で見つかることを確かめます。

なお、簡単化の結果は1通りとは限りません。例5は「YY-Y・YYYN・YYNN」の組でも同じ5規則になり、どちらも正しい表です。チームで読む表は、仕様の文章の順番に近いほうを選ぶと読みやすくなります。

Pythonで全組み合わせを作り、簡単化の前後を検算する

簡単化は手作業なので、まとめすぎや抜けが入ることがあります。itertools.product で全組み合わせを作り、どの組み合わせも「ちょうど1つの規則」に当てはまり、動作が元の仕様と同じであることを機械的に確かめます。

# dt.py  決定表の全組み合わせ生成・簡単化・検算(Python 3.11、標準ライブラリのみ)
from itertools import product

YN = ["Y", "N"]


def full_table(conds, spec):
    """全組み合わせを作り、仕様の関数 spec で動作を求める。起こり得ない組み合わせは spec が None を返す。"""
    rows = []
    for combo in product(*[values for _, values in conds]):
        rows.append((combo, spec(*combo)))
    return rows


def matches(entry, combo):
    return all(e == "-" or e == c for e, c in zip(entry, combo))


def verify(conds, spec, simplified):
    """簡単化した表が、起こり得る全組み合わせで元の仕様と同じ動作になるかを調べる。"""
    total = impossible = mismatch = multi = nohit = 0
    for combo, expected in full_table(conds, spec):
        total += 1
        if expected is None:
            impossible += 1
            continue
        hits = [actions for entry, actions in simplified if matches(entry, combo)]
        if len(hits) == 0:
            nohit += 1
        elif len(hits) > 1:
            multi += 1
        elif set(hits[0]) != set(expected):
            mismatch += 1
    return dict(全組み合わせ=total, 起こり得ない=impossible, 簡単化後の規則数=len(simplified),
                動作の不一致=mismatch, 複数の規則に適合=multi, 適合する規則なし=nohit)


def merge_steps(conds, rows):
    """動作が同じで、1つの条件だけが違い、その条件の値をすべて網羅する規則をまとめる。手順を返す。"""
    rules = [(tuple(c), tuple(a)) for c, a in rows if a is not None]
    steps = []
    changed = True
    while changed:
        changed = False
        for i, (_, values) in reversed(list(enumerate(conds))):
            groups = {}
            for entry, actions in rules:
                if entry[i] == "-":
                    continue
                key = (entry[:i] + ("-",) + entry[i + 1:], actions)
                groups.setdefault(key, []).append(entry)
            for (merged, actions), members in groups.items():
                if {m[i] for m in members} == set(values):
                    rules = [r for r in rules if r[0] not in members] + [(merged, actions)]
                    steps.append((members, merged, actions, list(rules)))
                    changed = True
                    break
            if changed:
                break
    return rules, steps

例5題の定義をまとめた examples.py は、各例の節に載せたコード(1行目の import と、例1〜例5の定義)を順に並べ、末尾に次の1文を加えたものです。

EXAMPLES = [("例1 備品予約の受付", C1, A1, spec1, S1), ("例2 図書の貸出", C2, A2, spec2, S2),
            ("例3 送料の決定", C3, A3, spec3, S3), ("例4 ログイン", C4, A4, spec4, S4),
            ("例5 会議室の予約", C5, A5, spec5, S5)]

検算スクリプトです。

# verify_all.py  例5題の簡単化が正しいことを全組み合わせで検算する
from math import prod
from dt import verify, full_table, merge_steps
from examples import EXAMPLES

for title, conds, actions, spec, simplified in EXAMPLES:
    sizes = [len(values) for _, values in conds]
    print(f"{title}: 規則数 = {' × '.join(map(str, sizes))} = {prod(sizes)}")
    print("  ", verify(conds, spec, simplified))
$ python3 verify_all.py
例1 備品予約の受付: 規則数 = 2 × 2 = 4
   {'全組み合わせ': 4, '起こり得ない': 0, '簡単化後の規則数': 3, '動作の不一致': 0, '複数の規則に適合': 0, '適合する規則なし': 0}
例2 図書の貸出: 規則数 = 2 × 2 × 2 = 8
   {'全組み合わせ': 8, '起こり得ない': 0, '簡単化後の規則数': 4, '動作の不一致': 0, '複数の規則に適合': 0, '適合する規則なし': 0}
例3 送料の決定: 規則数 = 3 × 2 × 2 = 12
   {'全組み合わせ': 12, '起こり得ない': 0, '簡単化後の規則数': 7, '動作の不一致': 0, '複数の規則に適合': 0, '適合する規則なし': 0}
例4 ログイン: 規則数 = 2 × 2 × 2 × 2 = 16
   {'全組み合わせ': 16, '起こり得ない': 7, '簡単化後の規則数': 5, '動作の不一致': 0, '複数の規則に適合': 0, '適合する規則なし': 0}
例5 会議室の予約: 規則数 = 2 × 2 × 2 × 2 = 16
   {'全組み合わせ': 16, '起こり得ない': 0, '簡単化後の規則数': 5, '動作の不一致': 0, '複数の規則に適合': 0, '適合する規則なし': 0}

5題とも「動作の不一致」「複数の規則に適合」「適合する規則なし」が0でした。簡単化の前後で結果が変わっていないことと、表が単適合で完備していることが確かめられます。例4の「起こり得ない:7」は、16通りのうち7通りを検算の対象から外したという意味です。

まとめすぎた表を検算にかける

例5で、規則1と規則2をさらにまとめた表を検算にかけます。

# wrong.py  まとめすぎた表を検算にかける(例5)
from dt import verify
from examples import C5, spec5

too_much = [(("Y", "Y", "-", "-"), ("予約を確定する",)),      # 規則1と2をさらにまとめたつもり
            (("Y", "Y", "N", "N"), ("承認依頼を送る",)),
            (("Y", "N", "-", "-"), ("別の部屋を案内する",)),
            (("N", "-", "-", "-"), ("別の時間帯を案内する",))]
print(verify(C5, spec5, too_much))
$ python3 wrong.py
{'全組み合わせ': 16, '起こり得ない': 0, '簡単化後の規則数': 4, '動作の不一致': 0, '複数の規則に適合': 1, '適合する規則なし': 0}

「複数の規則に適合:1」と出ました。YYNN が「YY--」と「YYNN」の両方に当てはまっているためです。規則数が減っていても、この値が0でなければ簡単化は成り立っていません。

簡単化の手順を出力する

dt.py の merge_steps は、例2の「1手ずつ」で使った手順を返します。

# steps.py  例2の簡単化の手順を出力する
from dt import full_table, merge_steps
from examples import C2, spec2

rules, steps = merge_steps(C2, full_table(C2, spec2))
for no, (members, merged, actions, snapshot) in enumerate(steps, 1):
    before = " と ".join("".join(m) for m in members)
    print(f"{no}手目: {before} → {''.join(merged)}({actions[0]}) 残り{len(snapshot)}規則")
$ python3 steps.py
1手目: YNY と YNN → YN-(延滞中の本の返却を依頼する) 残り7規則
2手目: NYY と NYN → NY-(利用者証の更新を案内する) 残り6規則
3手目: NNY と NNN → NN-(利用者証の更新を案内する) 残り5規則
4手目: NY- と NN- → N--(利用者証の更新を案内する) 残り4規則

この関数は、YとNのように値をすべて網羅できる列だけをまとめます。例4のように起こり得ない組み合わせを「-」に含める簡単化は人が判断し、verify で検算する、という分担が扱いやすい方法です。

決定表からテストケースを作る(pytestのparametrize)

決定表の1列(1規則)が、テストケース1件になります。簡単化後の表をそのまま parametrize の行に写し、「-」はYとNの両方に展開して全組み合わせをテストします。

テスト対象のコードです(例2と例3を実装したもの)。

# lending.py  テスト対象:図書の貸出判定(例2)と送料の決定(例3)
def judge_lending(card_valid: bool, no_overdue: bool, under_limit: bool) -> str:
    if not card_valid:
        return "利用者証の更新を案内する"
    if not no_overdue:
        return "延滞中の本の返却を依頼する"
    if not under_limit:
        return "貸出上限を案内する"
    return "貸出を行う"


def shipping_fee(area: str, amount: int, premium: bool) -> int:
    if premium:
        return 0
    over = amount >= 3000
    fees = {"近隣": (0, 400), "遠方": (300, 600), "離島": (800, 1100)}
    return fees[area][0 if over else 1]

テストコードです。LENDING_TABLE の1行が、例2の簡単化後の表の1列に対応します。

# test_lending.py  決定表を parametrize に写したテスト(pytest 9.1.1)
from itertools import product

import pytest

from lending import judge_lending, shipping_fee

# 例2の簡単化後の決定表。1行が1規則(表の1列)に対応する
LENDING_TABLE = [
    # 規則番号, 利用者証が有効, 延滞中の本がない, 貸出冊数が上限未満, 期待する動作
    ("R1", "Y", "Y", "Y", "貸出を行う"),
    ("R2", "Y", "Y", "N", "貸出上限を案内する"),
    ("R3", "Y", "N", "-", "延滞中の本の返却を依頼する"),
    ("R4", "N", "-", "-", "利用者証の更新を案内する"),
]


def expand(table):
    """「-」を Y と N の両方に展開して、pytest.param のリストにする。"""
    params = []
    for rule, *entries, expected in table:
        choices = [("Y", "N") if e == "-" else (e,) for e in entries]
        for combo in product(*choices):
            args = [c == "Y" for c in combo]
            params.append(pytest.param(*args, expected, id=f"{rule}-{''.join(combo)}"))
    return params


@pytest.mark.parametrize("card_valid, no_overdue, under_limit, expected", expand(LENDING_TABLE))
def test_judge_lending(card_valid, no_overdue, under_limit, expected):
    assert judge_lending(card_valid, no_overdue, under_limit) == expected


# 例3の簡単化後の決定表(拡張指定)。注文額は Y を 3000、N を 2999 として写す
@pytest.mark.parametrize(
    "area, amount, premium, expected",
    [
        pytest.param("近隣", 2999, True, 0, id="R1-near"),
        pytest.param("遠方", 3000, True, 0, id="R1-far"),
        pytest.param("離島", 2999, True, 0, id="R1-island"),
        pytest.param("近隣", 3000, False, 0, id="R2"),
        pytest.param("近隣", 2999, False, 400, id="R3"),
        pytest.param("遠方", 3000, False, 300, id="R4"),
        pytest.param("遠方", 2999, False, 600, id="R5"),
        pytest.param("離島", 3000, False, 800, id="R6"),
        pytest.param("離島", 2999, False, 1100, id="R7"),
    ],
)
def test_shipping_fee(area, amount, premium, expected):
    assert shipping_fee(area, amount, premium) == expected
$ python3 -m pytest -v test_lending.py
============================= test session starts ==============================
platform linux -- Python 3.11.15, pytest-9.1.1, pluggy-1.6.0 -- /usr/bin/python3
rootdir: /home/taro/decision
collecting ... collected 17 items

test_lending.py::test_judge_lending[R1-YYY] PASSED                       [  5%]
test_lending.py::test_judge_lending[R2-YYN] PASSED                       [ 11%]
test_lending.py::test_judge_lending[R3-YNY] PASSED                       [ 17%]
test_lending.py::test_judge_lending[R3-YNN] PASSED                       [ 23%]
test_lending.py::test_judge_lending[R4-NYY] PASSED                       [ 29%]
test_lending.py::test_judge_lending[R4-NYN] PASSED                       [ 35%]
test_lending.py::test_judge_lending[R4-NNY] PASSED                       [ 41%]
test_lending.py::test_judge_lending[R4-NNN] PASSED                       [ 47%]
test_lending.py::test_shipping_fee[R1-near] PASSED                       [ 52%]
test_lending.py::test_shipping_fee[R1-far] PASSED                        [ 58%]
test_lending.py::test_shipping_fee[R1-island] PASSED                     [ 64%]
test_lending.py::test_shipping_fee[R2] PASSED                            [ 70%]
test_lending.py::test_shipping_fee[R3] PASSED                            [ 76%]
test_lending.py::test_shipping_fee[R4] PASSED                            [ 82%]
test_lending.py::test_shipping_fee[R5] PASSED                            [ 88%]
test_lending.py::test_shipping_fee[R6] PASSED                            [ 94%]
test_lending.py::test_shipping_fee[R7] PASSED                            [100%]

============================== 17 passed in 0.02s ==============================

(rootdir の表示は環境に合わせて置き換えています。)例2は簡単化後の4規則を展開して8件、例3は7規則を9件(規則1だけ配送先を3通り)にしています。テストIDに規則番号と条件の並び(R3-YNY など)を入れておくと、テストが止まったときに決定表のどの列かがすぐわかります。

テストケース数の決め方

方針 件数(例2の場合) 向いている場面
簡単化後の1規則につき1件 4件 条件が多く、全組み合わせが多すぎるとき。「-」の欄は代表としてどちらか一方を選ぶ
「-」を展開して全組み合わせ 8件 自動テストで件数を増やしても負担が小さいとき。条件の判定順を変える改修にも気づける
全組み合わせ+境界値 8件+境界の件数 条件に数値の範囲(3,000円以上など)が含まれるとき

例3のテストでは、注文額のYを3000、Nを2999としています。決定表は「3,000円以上かどうか」をY・Nで表すだけなので、YとNの境目の値は境界値分析で選びます。

同値分割・境界値分析との使い分け

技法 見るもの 向いている仕様 成果物
同値分割 1つの入力を、同じ扱いになる範囲に分ける 年齢区分、文字数、数量など1項目の範囲 同値クラス表
境界値分析 範囲の境目とその隣の値 「以上」「未満」「以内」を含む条件 境界値表
デシジョンテーブル 複数の条件の組み合わせと動作 条件が2つ以上重なって結果が変わる仕様 決定表

3つは組み合わせて使います。決定表で「どの組み合わせをテストするか」を決め、同値分割と境界値分析で「YやNとして具体的にどの値を入れるか」を決める、という順番です。

仕様の特徴 選ぶ技法
入力が1項目で、範囲によって結果が変わる 同値分割+境界値分析
入力が複数あり、互いに関係なく結果が決まる 項目ごとに同値分割+境界値分析
入力が複数あり、組み合わせで結果が変わる デシジョンテーブル
組み合わせで結果が変わり、条件に数値の範囲がある デシジョンテーブル+境界値分析
条件が7個以上で1枚に収まらない 表を分ける。動作に影響しない条件を別の表に移す

決定表のチェックリスト

確認項目 見方
規則数は計算どおりか 簡単化前の列数が、各条件の値の数の積と一致する
条件は互いに独立した文になっているか 1つの条件に2つの判定が入っていない
すべての列に動作があるか Xが1つもない列は、仕様の確認事項にする
起こり得ない組み合わせの理由を書いたか 表の下に「未登録のアカウントにはロックの状態がない」のように1行で残す
簡単化後も単適合か どの組み合わせも、当てはまる規則は1つだけ
簡単化後も完備しているか 当てはまる規則がない組み合わせがない
簡単化前の表を残したか 仕様が変わったとき、簡単化前の表から作り直せる

よくある質問

Q. デシジョンテーブルと決定表は同じものですか? A. 同じものです。JIS X 0125 の名称が「決定表」で、英語の decision table をカタカナにしたものがデシジョンテーブルです。

Q. 「-」と「#」はどう使い分けますか? A. JIS X 0125 では、「-」は「その条件は無関係、または論理的に起こり得ない」を表し、「#」は起こり得ないことを強調したいときに「-」の代わりに使います。例4の規則5は N--- と書いても N### と書いても同じ意味です。

Q. 簡単化は必ず行うものですか? A. 必須ではありません。簡単化すると表が小さくなり読みやすくなりますが、テストは「-」をYとNに展開して全組み合わせで行うこともできます。簡単化前の表を残し、簡単化後の表は検算してから使うと安心です。

Q. 同じ動作の列は、すべて1列にまとめられますか? A. まとめられるのは、動作が同じで、条件が1つだけ違う列の組です。例5の規則1(YYY-)と規則2(YYNY)は動作が同じですが、1列にすると別の動作の組み合わせ(YYNN)まで含んでしまうため、2列のままにします。

まとめ

  • 決定表は、上半分に条件(Y・N・-)、下半分に動作(X・-)を書き、縦の1列が1つの規則になる
  • 規則数は各条件の値の数の積。YとNだけなら2のn乗で、条件3個は8、4個は16
  • 起こり得ない組み合わせは理由を添えて除く。JIS X 0125 では「-」または「#」で表す
  • 簡単化は「動作が同じで条件が1つだけ違う2列を1列にする」の繰り返し。3つの値を持つ条件は、すべての値で動作が同じときだけ「-」にできる
  • 簡単化の前後は itertools.product で検算する。不一致・複数適合・適合なしがすべて0であることを確かめる
  • 決定表の1列がテストケース1件。parametrize に写し、「-」を展開すれば全組み合わせをテストできる
  • YとNに入れる具体的な値は、同値分割と境界値分析で選ぶ

参考・出典

  • JIS X 0125:1986「決定表」(用語の定義、指定部の記号 Y・N・-・#・X の意味。閲覧に使用したページ): https://kikakurui.com/x0/X0125-1986-01.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「デシジョンテーブルの書き方と例5題|条件の簡単化とテストケースの作り方」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?