状態遷移図とは、対象が取りうる「状態」と、状態を切り替える「イベント」を矢印でつないだ図です。状態遷移表は同じ内容を「状態×イベント」のマス目に書いたもので、図では見えない「何も起きないはずの組み合わせ」まで一覧できます。 図は流れをつかむために、表は抜けを見つけてテストケースを作るために使います。

この記事では、架空の注文ステータスを題材に、同じ内容を Mermaid と PlantUML の両方で書き、状態遷移表から 0スイッチ・1スイッチ・無効遷移のテストケースを Python で数え上げます。コードと表はそのまま貼って使えます。

確認環境:Ubuntu 24.04、Mermaid 11.14.0(@mermaid-js/mermaid-cli 11.14.0 と Chromium 141 で SVG に描画)、PlantUML 1.2024.7(OpenJDK 21、レイアウトは内蔵の Smetana)、Python 3.11。

状態遷移図と状態遷移表の早見表

項目 状態遷移図 状態遷移表
形 状態を箱、遷移を矢印で描く 行に状態、列にイベントを並べ、セルに遷移先を書く
得意なこと 全体の流れ、戻りやループをひと目でつかむ すべての組み合わせを漏れなく確かめる
見えにくいこと 矢印の無い組み合わせ(遷移できないケース) 流れの順番
向く場面 仕様の説明、レビューの最初の5分 テスト設計、実装(テーブル駆動)の元データ
書く順番 先に図で流れを決める 図から表に起こし、空いたセルを埋める
テストの種類 何を網羅するか 今回の題材での件数
0スイッチ 遷移1本ずつ(状態→状態) 8件
1スイッチ 連続する遷移2本の並び(状態→状態→状態) 8件
無効遷移 遷移できないセル(状態×イベント) 28件
ガード条件 条件が成り立つとき/成り立たないとき 条件ごとに2件以上

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

(記載例)出典:株式会社RJC「状態遷移図と状態遷移表の書き方|テスト設計とMermaid・PlantUML」

状態遷移図を構成する5つの要素

要素 意味 今回の例 図での書き方
状態 対象がとどまっている状況。名詞や「〜中」「〜済」で書く 下書き、確定、準備中、発送済、完了、取消 角の丸い箱
イベント 遷移のきっかけ。動詞で書く 確定する、取り消す、発送する 矢印のラベル
遷移 ある状態から次の状態への移動 下書き → 確定 矢印
ガード条件 イベントが起きても、成り立つときだけ遷移する条件 [明細が1件以上] ラベルの後ろに角かっこ
アクション 遷移のときに行う処理 入荷待ちを通知 ラベルの後ろに「/ 処理名」

開始は黒丸(開始擬似状態)、終了は二重丸(終了状態)で表します。Mermaid と PlantUML ではどちらも [*] と書きます。

状態遷移図の書き方:4つの手順

題材は次の仕様です(架空の注文管理)。

  • 注文は「下書き」で作られ、明細が1件以上あれば「確定」できる
  • 確定した注文は、修正のために「下書き」へ戻せる
  • 在庫があれば準備を始めて「準備中」になり、発送すると「発送済」、受け取ると「完了」になる
  • 発送するまでは取り消せる。発送後は取り消せない
手順 やること 今回の結果
1. 状態を洗い出す 仕様から「〜の間は○○できる/できない」が変わる区切りを探す 6状態
2. イベントを洗い出す 利用者の操作、時間の経過、他システムからの通知を動詞で並べる 6イベント
3. 遷移を矢印でつなぐ 状態ごとに「どのイベントでどこへ行くか」を書く 8本
4. 開始・終了・条件を足す 開始と終了を置き、条件つきの矢印にガードを書く ガード2つ

状態を見つけるコツは「受け付ける操作が変わるところで区切る」ことです。 「確定」と「準備中」は、修正できるかどうかが変わるので別の状態にします。画面の表示だけが違い、受け付ける操作が同じものは、1つの状態にまとめます。

Mermaid の stateDiagram-v2 で書く

stateDiagram-v2
    direction LR
    [*] --> 下書き
    下書き --> 確定 : 確定する [明細が1件以上]
    下書き --> 取消 : 取り消す
    確定 --> 下書き : 修正する
    確定 --> 準備中 : 準備を始める [在庫あり]
    確定 --> 取消 : 取り消す
    準備中 --> 発送済 : 発送する
    準備中 --> 取消 : 取り消す
    発送済 --> 完了 : 受け取る
    完了 --> [*]
    取消 --> [*]
書き方 意味
stateDiagram-v2 状態遷移図の宣言。1行目に書く
direction LR 左から右へ並べる(省略すると上から下)
[*] --> 下書き 開始から最初の状態へ
A --> B : ラベル 遷移。コロンの後ろがイベント名
完了 --> [*] 終了状態へ

状態名に日本語をそのまま使えます。空白や記号を含む名前にしたいときは state "発送 済み" as Shipped のように別名を付けます。

PlantUML で同じ図を書く

@startuml
left to right direction
hide empty description

state "下書き" as Draft
state "確定" as Confirmed
state "準備中" as Preparing
state "発送済" as Shipped
state "完了" as Done
state "取消" as Cancelled

[*] --> Draft
Draft --> Confirmed : 確定する [明細が1件以上]
Draft --> Cancelled : 取り消す
Confirmed --> Draft : 修正する
Confirmed --> Preparing : 準備を始める [在庫あり]
Confirmed --> Cancelled : 取り消す
Preparing --> Shipped : 発送する
Preparing --> Cancelled : 取り消す
Shipped --> Done : 受け取る
Done --> [*]
Cancelled --> [*]
@enduml

PlantUML では state "表示名" as 別名 で状態を先に定義しておくと、矢印の行を英字の別名で短く書けます。hide empty description を入れると、説明欄が空の状態が小さな箱で描かれます。

今回の描画では、2行目に !pragma layout smetana を足して内蔵のレイアウト機能を使いました。Graphviz を入れていない環境でも、この1行で SVG を出力できます。

java -jar plantuml.jar -charset UTF-8 -tsvg order.puml

Mermaid と PlantUML の書き方の対応

やりたいこと Mermaid(stateDiagram-v2) PlantUML
図の始まりと終わり stateDiagram-v2 @startuml 〜 @enduml
開始・終了 [*] [*]
遷移 A --> B : イベント A --> B : イベント
日本語の状態名 そのまま書ける state "下書き" as Draft
横向きに並べる direction LR left to right direction
分岐(条件で行き先が変わる) state 名前 <<choice>> state 名前 <<choice>>
向く置き場所 Markdown の中に書いてそのまま表示したい文書 図のファイルを分けて管理したい文書

遷移の行はほぼ同じ書き方なので、片方で書いたものをもう片方へ移すのは短時間で済みます。

状態遷移表の書き方(Markdown とタブ区切り)

行に状態、列にイベントを並べ、セルに遷移先を書きます。遷移しない組み合わせは空欄にせず「-」を書くと、「検討して遷移なしと決めた」ことが伝わります。

状態 \ イベント 確定する 修正する 準備を始める 発送する 受け取る 取り消す
下書き 確定 - - - - 取消
確定 - 下書き 準備中 - - 取消
準備中 - - - 発送済 - 取消
発送済 - - - - 完了 -
完了 - - - - - -
取消 - - - - - -

36セルのうち、遷移があるのは8セル、「-」は28セルです。図には8本の矢印しか現れませんが、表にすると残り28個の組み合わせが見えるようになります。

表計算ソフトに貼るときは、次のタブ区切りをコピーしてセル A1 に貼り付けます。このあとの Python コードも、この内容を order_states.tsv として保存して読み込みます。

状態  確定する    修正する    準備を始める  発送する    受け取る    取り消す
下書き 確定  -   -   -   -   取消
確定  -   下書き 準備中 -   -   取消
準備中 -   -   -   発送済 -   取消
発送済 -   -   -   -   完了  -
完了  -   -   -   -   -   -
取消  -   -   -   -   -   -

セルに書く内容を増やす場合

セルの書き方 例 向く場面
遷移先だけ 確定 状態の流れを確かめる段階
遷移先 / アクション 取消 / 在庫を戻す 処理の内容まで決める段階
遷移先 [ガード] 準備中 [在庫あり] 条件つきの遷移がある場合
「-」の代わりに扱いを書く 無視、エラー表示 無効な操作の扱いを仕様として決める段階

遷移表からテストケースを数え上げる Python コード

0スイッチは「遷移1本」、1スイッチは「連続する遷移2本」を1件のテストとして数えます。数が増えると手で数えるのは大変なので、タブ区切りの遷移表を読み込んで列挙します。

import csv
import sys

path = sys.argv[1] if len(sys.argv) > 1 else "order_states.tsv"
with open(path, encoding="utf-8", newline="") as f:
    rows = list(csv.reader(f, delimiter="\t"))

events = rows[0][1:]
states = [r[0] for r in rows[1:]]

# 遷移表を「有効な遷移」と「遷移できないセル」に分ける
valid, invalid = [], []
for r in rows[1:]:
    for event, dest in zip(events, r[1:]):
        if dest in ("", "-"):
            invalid.append((r[0], event))
        else:
            valid.append((r[0], event, dest))


def n_switch(n):
    """n スイッチ = 連続する n+1 本の遷移の並びをすべて返す"""
    paths = [[t] for t in valid]
    for _ in range(n):
        paths = [p + [t] for p in paths for t in valid if p[-1][2] == t[0]]
    return paths


def show(path):
    text = path[0][0]
    for _, event, dest in path:
        text += f" --{event}--> {dest}"
    return text


print(f"状態 {len(states)} 個 x イベント {len(events)} 個 = {len(states) * len(events)} セル")
for n in (0, 1):
    cases = n_switch(n)
    print(f"\n[{n}スイッチ] {len(cases)} 件")
    for i, p in enumerate(cases, 1):
        print(f"  S{n}-{i:02d} {show(p)}")

print(f"\n[無効遷移] {len(invalid)} 件")
for i, (state, event) in enumerate(invalid, 1):
    print(f"  N-{i:02d} {state} で「{event}」")

reachable = {states[0]} | {t[2] for t in valid}
print("\n到達できない状態:", [s for s in states if s not in reachable] or "なし")
print("出口のない状態:", [s for s in states if not any(t[0] == s for t in valid)])
python3 switch_cover.py order_states.tsv

実行結果です。

状態 6 個 x イベント 6 個 = 36 セル

[0スイッチ] 8 件
  S0-01 下書き --確定する--> 確定
  S0-02 下書き --取り消す--> 取消
  S0-03 確定 --修正する--> 下書き
  S0-04 確定 --準備を始める--> 準備中
  S0-05 確定 --取り消す--> 取消
  S0-06 準備中 --発送する--> 発送済
  S0-07 準備中 --取り消す--> 取消
  S0-08 発送済 --受け取る--> 完了

[1スイッチ] 8 件
  S1-01 下書き --確定する--> 確定 --修正する--> 下書き
  S1-02 下書き --確定する--> 確定 --準備を始める--> 準備中
  S1-03 下書き --確定する--> 確定 --取り消す--> 取消
  S1-04 確定 --修正する--> 下書き --確定する--> 確定
  S1-05 確定 --修正する--> 下書き --取り消す--> 取消
  S1-06 確定 --準備を始める--> 準備中 --発送する--> 発送済
  S1-07 確定 --準備を始める--> 準備中 --取り消す--> 取消
  S1-08 準備中 --発送する--> 発送済 --受け取る--> 完了

[無効遷移] 28 件
  N-01 下書き で「修正する」
  N-02 下書き で「準備を始める」
  N-03 下書き で「発送する」
  N-04 下書き で「受け取る」
  N-05 確定 で「確定する」
  N-06 確定 で「発送する」
  N-07 確定 で「受け取る」
  N-08 準備中 で「確定する」
  N-09 準備中 で「修正する」
  N-10 準備中 で「準備を始める」
  N-11 準備中 で「受け取る」
  N-12 発送済 で「確定する」
  N-13 発送済 で「修正する」
  N-14 発送済 で「準備を始める」
  N-15 発送済 で「発送する」
  N-16 発送済 で「取り消す」
  N-17 完了 で「確定する」
  N-18 完了 で「修正する」
  N-19 完了 で「準備を始める」
  N-20 完了 で「発送する」
  N-21 完了 で「受け取る」
  N-22 完了 で「取り消す」
  N-23 取消 で「確定する」
  N-24 取消 で「修正する」
  N-25 取消 で「準備を始める」
  N-26 取消 で「発送する」
  N-27 取消 で「受け取る」
  N-28 取消 で「取り消す」

到達できない状態: なし
出口のない状態: ['完了', '取消']

0スイッチのテストケース(8件)

No. 開始状態 イベント 期待する状態
S0-01 下書き 確定する 確定
S0-02 下書き 取り消す 取消
S0-03 確定 修正する 下書き
S0-04 確定 準備を始める 準備中
S0-05 確定 取り消す 取消
S0-06 準備中 発送する 発送済
S0-07 準備中 取り消す 取消
S0-08 発送済 受け取る 完了

0スイッチは、状態遷移表の「遷移があるセル」と1対1で対応します。件数はセルの数と同じ8件です。

1スイッチのテストケース(8件)

No. 1本目 2本目 確かめられること
S1-01 下書き →(確定する)→ 確定 →(修正する)→ 下書き 確定した直後に戻せる
S1-02 下書き →(確定する)→ 確定 →(準備を始める)→ 準備中 確定から準備へ進める
S1-03 下書き →(確定する)→ 確定 →(取り消す)→ 取消 確定した直後に取り消せる
S1-04 確定 →(修正する)→ 下書き →(確定する)→ 確定 戻した注文をもう一度確定できる
S1-05 確定 →(修正する)→ 下書き →(取り消す)→ 取消 戻した注文を取り消せる
S1-06 確定 →(準備を始める)→ 準備中 →(発送する)→ 発送済 準備から発送へ進める
S1-07 確定 →(準備を始める)→ 準備中 →(取り消す)→ 取消 準備を始めた後でも取り消せる
S1-08 準備中 →(発送する)→ 発送済 →(受け取る)→ 完了 発送から完了へ進める

1スイッチの件数は「1本目の行き先から出ている矢印の本数」を足し合わせた数です。 行き先が「確定」の遷移は1本(S0-01)で、確定からは3本出ているので 1×3=3件。同じように下書き行きが1本×2本、準備中行きが1本×2本、発送済行きが1本×1本で、合計 3+2+2+1=8件になります。行き先が完了・取消の遷移は、その先に矢印が無いので0件です。

1スイッチで見つかりやすいのは、S1-04 のような「戻ってからもう一度進む」並びです。戻したときに前回の値が残っていると、2回目の確定で動きが変わることがあります。0スイッチではこの並びを通りません。

開始状態が「確定」や「準備中」のケースは、テスト手順の前に、その状態まで進める操作(下書き → 確定 など)を事前条件として書きます。

n_switch(2) と呼べば2スイッチ(連続3本)も列挙できます。今回の題材では2スイッチも8件でした。ループが多い図では件数が急に増えるので、まず0スイッチと無効遷移をそろえ、戻りやループのある部分に1スイッチを足す進め方が効率的です。

無効遷移テスト:遷移できないセルを洗い出す

状態遷移表の「-」のセルは、「その状態でそのイベントが起きても状態が変わらない」ことを確かめるテストになります。上の実行結果の [無効遷移] 28件がその一覧です。状態ごとにまとめると次の表になります。

状態 遷移できないイベント 件数 期待する結果
下書き 修正する、準備を始める、発送する、受け取る 4 下書きのまま
確定 確定する、発送する、受け取る 3 確定のまま
準備中 確定する、修正する、準備を始める、受け取る 4 準備中のまま
発送済 確定する、修正する、準備を始める、発送する、取り消す 5 発送済のまま
完了 6イベントすべて 6 完了のまま
取消 6イベントすべて 6 取消のまま

28件すべてを同じ重みで試す必要はありません。次の観点で優先度を付けます。

優先度 観点 今回の例
高 仕様に「できない」と明記されている操作 発送済で「取り消す」(N-16)
高 終了した状態への操作 完了・取消で「確定する」「発送する」(N-17、N-20、N-23、N-26)
中 同じ操作の二重実行 確定で「確定する」(N-05)、準備中で「準備を始める」(N-10)
中 手順を飛ばす操作 下書きで「発送する」(N-03)、確定で「発送する」(N-06)
低 画面にボタンが出ない組み合わせ API を直接呼べる場合は優先度を上げる

期待する結果には、状態が変わらないことに加えて「エラーメッセージを出す」「何もせず無視する」のどちらなのかも書きます。ここが仕様書に無ければ、設計者に確認する項目になります。

ガード条件付き遷移の書き方

ガード条件は、イベント名の後ろに角かっこで書きます。条件によって行き先が分かれるときは、分岐(choice)を使うと図が読みやすくなります。

Mermaid での書き方です。

stateDiagram-v2
    state 在庫の判定 <<choice>>
    [*] --> 確定
    確定 --> 在庫の判定 : 準備を始める
    在庫の判定 --> 準備中 : [在庫あり]
    在庫の判定 --> 確定 : [在庫なし] / 入荷待ちを通知
    準備中 --> [*]

PlantUML での書き方です。

@startuml
hide empty description

state "確定" as Confirmed
state "準備中" as Preparing
state stock <<choice>>

[*] --> Confirmed
Confirmed --> stock : 準備を始める
stock --> Preparing : [在庫あり]
stock --> Confirmed : [在庫なし] / 入荷待ちを通知
Preparing --> [*]
@enduml

状態遷移表では、ガードごとに行(または列)を分けて書きます。

状態 イベント ガード条件 遷移先 アクション
下書き 確定する 明細が1件以上 確定 -
下書き 確定する 明細が0件 下書き 明細の追加を促す
確定 準備を始める 在庫あり 準備中 在庫を引き当てる
確定 準備を始める 在庫なし 確定 入荷待ちを通知

ガード条件は「成り立つとき」と「成り立たないとき」を必ず対で書きます。 片方だけだと、条件が成り立たないときに何が起きるのかが決まらないまま実装に進むことになります。テストケースは条件ごとに2件、数値の条件(1件以上など)なら境界の値(0件と1件)を選びます。

よくあるつまずきと直し方

つまずき 起きること 直し方
状態と処理が混ざる(「確定処理」を状態にする) 一瞬で終わる処理が状態として並び、図が長くなる とどまる時間があるものだけを状態にし、処理はアクションに書く
表の空欄をそのままにする 検討もれなのか、遷移なしなのかが区別できない 遷移なしは「-」、対象外は「N/A」と書き分ける
取り消しや戻りの矢印を書き忘れる 正常の流れだけの図になる 表にして「取り消す」列を上から順に埋める
出口の無い状態が終了以外にある その状態から動けなくなる コードの「出口のない状態」の出力が、意図した終了状態だけかを確かめる
状態が10個を超えて読みにくい 矢印が交差する 入れ子の状態(複合状態)にまとめるか、図を分ける

よくある質問

Q. 状態遷移図と状態遷移表は、どちらを先に書きますか? A. 先に図で流れを決め、表に起こして空いたセルを埋める順番が進めやすいです。表を埋める途中で見つかった遷移は、図に戻して矢印を足します。

Q. 0スイッチと1スイッチは、どこまでやればよいですか? A. 0スイッチと無効遷移を基本にし、戻りやループがある部分に1スイッチを足すのが目安です。1スイッチは「前の遷移の影響が次の遷移に残る」不具合を見つけるためのものなので、値を持ち越す状態のまわりで効果が出ます。

Q. フローチャートとはどう使い分けますか? A. フローチャートは処理の順番を、状態遷移図は「いま何を受け付けるか」を表します。同じ操作でも状態によって結果が変わるもの(注文、申請、会員、タスクなど)は状態遷移図が向いています。

Q. Mermaid と PlantUML はどちらを選べばよいですか? A. Markdown の文書に図を埋め込んで表示したいなら Mermaid、図をファイルとして分けて管理したいなら PlantUML が扱いやすいです。遷移の行はほぼ同じ書き方なので、あとから移すこともできます。

まとめ

  • 状態遷移図は流れを、状態遷移表は抜けを見るために使う。図を書いたら必ず表に起こす
  • 状態は「受け付ける操作が変わるところ」で区切る。イベントは動詞で書く
  • Mermaid は stateDiagram-v2、PlantUML は @startuml で始め、遷移は「A --> B : イベント」と書く
  • 0スイッチは遷移1本、1スイッチは連続する2本。今回の6状態・6イベントでは、どちらも8件
  • 表の「-」のセルは無効遷移テスト(今回は28件)になる。優先度を付けて選ぶ
  • ガード条件は成り立つとき/成り立たないときを対で表に書き、境界の値でテストする
  • 件数の数え上げは Python に任せ、遷移表を直したら実行し直す

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

(記載例)出典:株式会社RJC「状態遷移図と状態遷移表の書き方|テスト設計とMermaid・PlantUML」

株式会社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で一緒に開発しながら、 成長を楽しみませんか?