直前の状態を覚える仕様における、状態遷移テスト

# 概要
テスト担当者が状態遷移テストをする際、テスト対象の仕様を理解し、どんな状態遷移をモデル化するか、モデルからどのようなテストケースを生成するについての事例紹介です。
状態遷移図描画とテストケース生成には、[GIHOZ](https://gihoz.com/)を使ってみました。
また、文中の「戻る操作」というのは、明示的に設置されたボタンやリンクによる操作を示しており、例えば、ブラウザの戻るボタンは含みません。
UnDoのような機能、あるいは、商品・サービスで中心的な機能や画面に戻る操作、戻りすぎて状態が大きく変わってしまうのを制限したい仕様など、評価する上での参考になればと思います。
汎化して抽象的過ぎる感もありますが、テスト設計のお役になればと思います。<br>
#### 2024/3/3 遷移しないケースが丸っと抜けていましたので、追記しています。事例2と事例3が対象です。

# 状態遷移の例
2つの状態では、直前を覚えたとしても、2つの状態を行き来するだけなので、ここでは、最低でも3つの状態がある場合を考えてみます。

![簡単な状態遷移0スイッチ002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/cd0b8d54-8c8a-9f6d-9e86-33f072315dbd.png)

例えば上記のような場合の1回だけ遷移するテスト（0-スイッチ）は、
|testID|開始状態|操作1|終了状態|
|:----|:----|:----|:----|
|1|状態A|event_AB|状態B|
|2|状態B|event_BA|状態A|
|3|状態B|event_BC|状態C|
|4|状態C|event_CB|状態B|
|5|状態A|event_AC|状態C|
|6|状態C|event_CA|状態A|
|7|状態A|keep_AA|状態A|
|8|状態B|keep_BB|状態B|
|9|状態C|keep_CC|状態C|

直前の状態に戻る（UnDo）かのテストでは、一回目の操作で遷移した状態が、次のUnDo操作で戻るのかのテストとなるため、2回状態遷移する（1-スイッチ）で、
|testID|開始状態|操作1|後状態|操作2|終了状態|
|:----|:----|:----|:----|:----|:----|
|1|状態A|event_AB|状態B|event_BA<br>UnDo_BA|状態A|
|2|状態A|event_AB|状態B|event_BC|状態C|
|3|状態A|event_AB|状態B|keep_BB|状態B|
|4|状態B|event_BA|状態A|event_AB<br>UnDo_AB|状態B|
|5|状態B|event_BA|状態A|event_AC|状態C|
|6|状態B|event_BA|状態A|keep_AA|状態A|
|7|状態B|event_BC|状態C|event_CB<br>UnDo_CB|状態B|
|8|状態B|event_BC|状態C|event_CA|状態A|
|9|状態B|event_BC|状態C|keep_CC|状態C|
|10|状態C|event_CB|状態B|event_BA|状態A|
|11|状態C|event_CB|状態B|event_BC<br>UnDo_BC|状態C|
|12|状態C|event_CB|状態B|keep_BB|状態B|
|13|状態A|event_AC|状態C|event_CB|状態B|
|14|状態A|event_AC|状態C|event_CA<br>UnDo_CA|状態A|
|15|状態A|event_AC|状態C|keep_CC|状態C|
|16|状態C|event_CA|状態A|event_AB|状態B|
|17|状態C|event_CA|状態A|event_AC<br>UnDo_AC|状態C|
|18|状態C|event_CA|状態A|keep_AA|状態A|
|19|状態A|keep_AA|状態A|event_AB|状態B|
|20|状態A|keep_AA|状態A|event_AC|状態C|
|21|状態A|keep_AA|状態A|keep_AA|状態A|
|22|状態B|keep_BB|状態B|event_BA|状態A|
|23|状態B|keep_BB|状態B|event_BC|状態C|
|24|状態B|keep_BB|状態B|keep_BB|状態B|
|25|状態C|keep_CC|状態C|event_CB|状態B|
|26|状態C|keep_CC|状態C|event_CA|状態A|
|27|状態C|keep_CC|状態C|keep_CC|状態C|

 操作の表記は、状態Aから状態Bに遷移する場合、event_AB
 状態Bから状態Cに遷移する場合、event_BCといったように記述しています。
 また、状態遷移図と生成されたテストケース内で、UnDoに相当するテストについて、手修正しています。
 testID 1 のevent_BAは、UnDoなので、UnDo_BA と修正するなど。
 
# 直前の状態を覚える仕様における状態遷移
3つの状態があるような場合に、状態Aから状態Bに遷移した後、戻る操作で状態Aには戻ることを確認する必要があります。
また、直前の状態が無い（開始状態）から、最初の状態遷移を考慮すると、

1. 最初（直前の状態を覚えていない）の状態遷移操作（初めて直前の状態を覚える）
2. 直前の状態を覚えている状態からの操作
3. 戻る（UnDo）操作

の3操作を確認するには、最低でも2-スイッチの状態遷移で考えることになります。
これから紹介する状態遷移図の事例以外にも、実際に適用すべきモデルはあるかと思います。ここはぜひ、適用したモデル選定を開発・実装側ともレビューを通じて共有したいところですね。
以下に、考えてみた状態遷移図の3例と、生成したテストケースを紹介します。
### 直前を覚えた状態を個々に定義した例（事例1）
状態aBというのは、直前が状態Aだったことを覚えている状態Bのことをさします。
![直前のみ2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/ce7805d8-5682-a6e1-40f6-ae1726c592a1.png)
開始状態は状態A / 状態B / 状態C です。さらに状態D、状態Eと増えた場合には、もっと複雑になります。3つでもゴチャゴチャなのに。
開始状態を明確に表現している点では、他の2例のモデルよりも適当だとは思います。
GIHOZで生成したテストケースから、操作3に戻る操作が含まれるものだけ取り出してみました。

|testID|開始状態|操作1|後状態1|操作2|後状態2|操作3|終了状態|
|:----|:----|:----|:----|:----|:----|:----|:----|
|1|状態A|event_AC|状態aC|event_CB2|状態aB|UnDo_BC2|状態aC|
|2|状態A|event_AC|状態aC|event_CA2|状態cA|UnDo_AC2|状態aC|
|3|状態A|event_AC|状態aC|event_CA4|状態bA|UnDo_AC4|状態aC|
|4|状態A|event_AC|状態aC|event_CB6|状態cB|UnDo_BC6|状態aC|
|5|状態A|event_AB|状態aB|event_BA2|状態bA|UnDo_AB2|状態aB|
|6|状態A|event_AB|状態aB|event_BC2|状態aC|UnDo_CB2|状態aB|
|7|状態A|event_AB|状態aB|event_BC3|状態bC|UnDo_CB3|状態aB|
|8|状態A|event_AB|状態aB|event_BA4|状態cA|UnDo_AB4|状態aB|
|9|状態B|event_BC|状態bC|event_CA2|状態bA|UnDo_AC2|状態bC|
|10|状態B|event_BC|状態bC|event_CB3|状態aB|UnDo_BC3|状態bC|
|11|状態B|event_BC|状態bC|event_CA5|状態cA|UnDo_AC5|状態bC|
|12|状態B|event_BC|状態bC|event_CB5|状態cB|UnDo_BC5|状態bC|
|13|状態B|event_BA|状態bA|event_AB2|状態aB|UnDo_BA2|状態bA|
|14|状態B|event_BA|状態bA|event_AB3|状態cB|UnDo_BA3|状態bA|
|15|状態B|event_BA|状態bA|event_AC2|状態bC|UnDo_CA2|状態bA|
|16|状態B|event_BA|状態bA|event_AC4|状態aC|UnDo_CA4|状態bA|
|17|状態C|event_CA|状態cA|event_AC2|状態aC|UnDo_CA2|状態cA|
|18|状態C|event_CA|状態cA|event_AB4|状態aB|UnDo_BA4|状態cA|
|19|状態C|event_CA|状態cA|event_AC5|状態bC|UnDo_CA5|状態cA|
|20|状態C|event_CA|状態cA|event_AB5|状態cB|UnDo_BA5|状態cA|
|21|状態C|event_CB|状態cB|event_BA3|状態bA|UnDo_AB3|状態cB|
|22|状態C|event_CB|状態cB|event_BC5|状態bC|UnDo_CB5|状態cB|
|23|状態C|event_CB|状態cB|event_BA5|状態cA|UnDo_AB5|状態cB|
|24|状態C|event_CB|状態cB|event_BC6|状態aC|UnDo_CB6|状態cB|


### 戻った状態を別の状態として定義した例（事例2）
事例1は、UnDo操作が期待通りに動くかのテストとしては十分かもしれませんが、UnDoの前後に別の操作まで含めたテストにしたい場合は、3-スイッチになってしまい、生成されたテストケースからUnDoを含むテストだけ取り出したり、修正したりするのが煩雑になります。
そこで、別の状態を「戻った状態」として分離しました。
##### 2024/03/03 直前のみ覚える仕様以外に、さらにその前も覚える仕様の時は、戻った状態を増やしていくことで対応できるかと思います。

![直前のみ24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/2a164598-ae6b-3fa4-959d-9cd08c6ef931.png)

3－スイッチで生成したテストケースの内、戻る操作を含むのは、戻った状態　とか　さらに戻る　とかを状態遷移図見ながら修正するのが、かなり面倒ですが、

|testID|開始状態|操作1|後状態1|操作2|後状態2|操作3|後状態3|操作4|終了状態|
|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|
|1|状態A|event_AB|状態B|event_BA|状態A|keep_AA|状態A|keep_AA|状態A|
|2|状態A|event_AB|状態B|event_BA|状態A|keep_AA|状態A|keep_AA|状態A|
|3|状態A|event_AB|状態B|event_BC|状態C|event_CB|状態B|UnDo_BC|状態C|
|4|状態A|event_AB|状態B|event_BC|状態C|event_CA|状態A|keep_AA|状態A|
|5|状態A|event_AB|状態B|event_BC|状態C|UnDo_CB|状態B|さらに戻る|状態B|
|6|状態A|event_AB|状態B|UnDo_BA|状態A|さらに戻る|状態A|さらに戻る|状態A|
|7|状態A|event_AB|状態B|keep_BB|状態B|event_BA|状態A|UnDo_AB|状態B|
|8|状態A|event_AB|状態B|keep_BB|状態B|event_BC|状態C|UnDo_CB|状態B|
|9|状態A|event_AB|状態B|keep_BB|状態B|UnDo_BA|状態A|さらに戻る|状態A|
|10|状態A|event_AB|状態B|keep_BB|状態B|keep_BB|状態B|UnDo_BA|状態A|
|11|状態B|event_BA|状態A|event_AB|状態B|UnDo_BA|状態A|さらに戻る|状態A|
|12|状態B|event_BA|状態A|event_AB|状態B|keep_BB|状態B|UnDo_BA|状態A|
|13|状態B|event_BA|状態A|event_AC|状態C|UnDo_CA|状態A|さらに戻る|状態A|
|14|状態B|event_BA|状態A|event_AC|状態C|keep_CC|状態C|UnDo_CA|状態A|
|15|状態B|event_BA|状態A|UnDo_AB|状態B|さらに戻る|状態B|さらに戻る|状態B|
|16|状態B|event_BA|状態A|keep_AA|状態A|event_AB|状態B|UnDo_BA|状態A|
|17|状態B|event_BA|状態A|keep_AA|状態A|event_AC|状態C|UnDo_CA|状態A|
|18|状態B|event_BA|状態A|keep_AA|状態A|UnDo_AB|状態B|さらに戻る|状態B|
|19|状態B|event_BA|状態A|keep_AA|状態A|keep_AA|状態A|UnDo_AB|状態B|
|20|状態B|event_BC|状態C|event_CB|状態B|UnDo_BC|状態C|さらに戻る|状態C|
|21|状態B|event_BC|状態C|event_CB|状態B|keep_BB|状態B|UnDo_BC|状態C|
|22|状態B|event_BC|状態C|event_CA|状態A|UnDo_AC|状態C|さらに戻る|状態C|
|23|状態B|event_BC|状態C|event_CA|状態A|keep_AA|状態A|UnDo_AC|状態C|
|24|状態B|event_BC|状態C|UnDo_CB|状態B|さらに戻る|状態B|さらに戻る|状態B|
|25|状態B|event_BC|状態C|keep_CC|状態C|event_CB|状態B|UnDo_BC|状態C|
|26|状態B|event_BC|状態C|keep_CC|状態C|event_CA|状態A|UnDo_AC|状態C|
|27|状態B|event_BC|状態C|keep_CC|状態C|UnDo_CB|状態B|さらに戻る|状態B|
|28|状態B|event_BC|状態C|keep_CC|状態C|keep_CC|状態C|UnDo_CB|状態B|
|29|状態C|event_CB|状態B|event_BA|状態A|UnDo_AB|状態B|さらに戻る|状態B|
|30|状態C|event_CB|状態B|event_BA|状態A|keep_AA|状態A|UnDo_AB|状態B|
|31|状態C|event_CB|状態B|event_BC|状態C|UnDo_CB|状態B|さらに戻る|状態B|
|32|状態C|event_CB|状態B|event_BC|状態C|keep_CC|状態C|UnDo_CB|状態B|
|33|状態C|event_CB|状態B|UnDo_BC|状態C|さらに戻る|状態C|さらに戻る|状態C|
|34|状態C|event_CB|状態B|keep_BB|状態B|event_BA|状態A|UnDo_AB|状態B|
|35|状態C|event_CB|状態B|keep_BB|状態B|event_BC|状態C|UnDo_CB|状態B|
|36|状態C|event_CB|状態B|keep_BB|状態B|UnDo_BC|状態C|さらに戻る|状態C|
|37|状態C|event_CB|状態B|keep_BB|状態B|keep_BB|状態B|UnDo_BC|状態C|
|38|状態A|event_AC|状態C|event_CB|状態B|UnDo_BC|状態C|さらに戻る|状態C|
|39|状態A|event_AC|状態C|event_CB|状態B|keep_BB|状態B|UnDo_BC|状態C|
|40|状態A|event_AC|状態C|event_CA|状態A|UnDo_AC|状態C|さらに戻る|状態C|
|41|状態A|event_AC|状態C|event_CA|状態A|keep_AA|状態A|UnDo_AC|状態C|
|42|状態A|event_AC|状態C|UnDo_CA|状態A|さらに戻る|状態A|さらに戻る|状態A|
|43|状態A|event_AC|状態C|keep_CC|状態C|event_CB|状態B|UnDo_BC|状態C|
|44|状態A|event_AC|状態C|keep_CC|状態C|event_CA|状態A|UnDo_AC|状態C|
|45|状態A|event_AC|状態C|keep_CC|状態C|UnDo_CA|状態A|さらに戻る|状態A|
|46|状態A|event_AC|状態C|keep_CC|状態C|keep_CC|状態C|UnDo_CA|状態A|
|47|状態C|event_CA|状態A|event_AB|状態B|UnDo_BA|状態A|さらに戻る|状態A|
|48|状態C|event_CA|状態A|event_AB|状態B|keep_BB|状態B|UnDo_BA|状態A|
|49|状態C|event_CA|状態A|event_AC|状態C|UnDo_CA|状態A|さらに戻る|状態A|
|50|状態C|event_CA|状態A|event_AC|状態C|keep_CC|状態C|UnDo_CA|状態A|
|51|状態C|event_CA|状態A|keep_AA|状態A|UnDo_AC|状態C|さらに戻る|状態C|
|52|状態C|event_CA|状態A|keep_AA|状態A|keep_AA|状態A|UnDo_AC|状態C|
|53|状態A|keep_AA|状態A|event_AB|状態B|UnDo_BA|状態A|さらに戻る|状態A|
|54|状態A|keep_AA|状態A|event_AB|状態B|keep_BB|状態B|UnDo_BA|状態A|
|55|状態A|keep_AA|状態A|event_AC|状態C|UnDo_CA|状態A|さらに戻る|状態A|
|56|状態A|keep_AA|状態A|event_AC|状態C|keep_CC|状態C|UnDo_CA|状態A|
|57|状態A|keep_AA|状態A|keep_AA|状態A|event_AB|状態B|UnDo_BA|状態A|
|58|状態A|keep_AA|状態A|keep_AA|状態A|event_AC|状態C|UnDo_CA|状態A|
|59|状態B|keep_BB|状態B|event_BA|状態A|UnDo_AB|状態B|さらに戻る|状態B|
|60|状態B|keep_BB|状態B|event_BA|状態A|keep_AA|状態A|UnDo_AB|状態B|
|61|状態B|keep_BB|状態B|event_BC|状態C|event_CB|状態B|UnDo_BC|状態C|
|62|状態B|keep_BB|状態B|event_BC|状態C|event_CA|状態A|UnDo_AC|状態C|
|63|状態B|keep_BB|状態B|event_BC|状態C|UnDo_CB|状態B|さらに戻る|状態B|
|64|状態B|keep_BB|状態B|event_BC|状態C|keep_CC|状態C|UnDo_CB|状態B|
|65|状態B|keep_BB|状態B|keep_BB|状態B|event_BA|状態A|UnDo_AB|状態B|
|66|状態B|keep_BB|状態B|keep_BB|状態B|event_BC|状態C|UnDo_CB|状態B|
|67|状態B|keep_BB|状態B|keep_BB|状態B|keep_BB|状態B|Bから戻る|状態B|
|68|状態C|keep_CC|状態C|event_CB|状態B|event_BA|状態A|UnDo_AB|状態B|
|69|状態C|keep_CC|状態C|event_CB|状態B|event_BC|状態C|UnDo_CB|状態B|
|70|状態C|keep_CC|状態C|event_CB|状態B|UnDo_BC|状態C|さらに戻る|状態C|
|71|状態C|keep_CC|状態C|event_CB|状態B|keep_BB|状態B|UnDo_BC|状態C|
|72|状態C|keep_CC|状態C|event_CA|状態A|event_AB|状態B|UnDo_BA|状態A|
|73|状態C|keep_CC|状態C|event_CA|状態A|event_AC|状態C|UnDo_CA|状態A|
|74|状態C|keep_CC|状態C|event_CA|状態A|UnDo_AC|状態C|さらに戻る|状態C|
|75|状態C|keep_CC|状態C|event_CA|状態A|keep_AA|状態A|UnDo_AC|状態C|
|76|状態C|keep_CC|状態C|keep_CC|状態C|event_CB|状態B|UnDo_BC|状態C|
|77|状態C|keep_CC|状態C|keep_CC|状態C|event_CA|状態A|UnDo_AC|状態C|

- さらに戻る操作が無効であることのテスト
- 戻る操作をした後の、次の状態遷移テスト
- 遷移しない操作の後の、戻る操作で、戻るかどうかのテスト（2024/3/3 追加）

がテストできるようになりました。<br>
ここで注意が必要なのは、開始状態でリセット処理が必要になる点です。最初のモデルも２つ目のモデルも、テスト件数分だけリセット処理（例えば、ブラウザの履歴削除や電源OffOnなど）を入れないと開始状態にならないので、仮にバグらしきものが見つかった場合、再現しないなどに陥る要因になる時があります。
自動テストにしておいて、毎回ブラウザをプロセス再起動するなど
で対応している場合は良いのですが、手動テストになると、大変な作業です。

図は、MagicPodのブラウザ起動オプションの例
![magicpod001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/f1a3770e-190f-a3e8-bac5-a1043d217e2c.png)


### ３状態のモデルをそのまま流用（事例3）
３つ目に紹介する状態遷移図は、[最初に3状態で記載したもの](#状態遷移の例)をそのまま使い、生成したテストケースは、終了状態が、次の開始状態になるように、並べ替えて使うというものです。
テストケース生成も、0-スイッチで生成したので、3つの事例の中では、一番軽いものになりました。

|testID|開始状態|操作1|終了状態|
|:----|:----|:----|:----|
|1|状態A|event_AB|状態aB|
|2|状態aB|UnDo_BA|状態bA|
|3|状態bA|keep_AA|状態bA|
|4|状態bA|event_AC|状態aC|
|5|状態aC|UnDo_CA|状態cA|
|6|状態cA|リセット|状態Bで起動|
|7|状態B|event_BC|状態bC|
|8|状態bC|UnDo_CB|状態cB|
|9|状態cB|keep_BB|状態cB|
|10|状態cB|event_BC|状態bC|
|11|状態bC|keep_CC|状態bC|


testID 1の終了状態がtestID 2の開始状態になるように並べ替えてテストします。
実行順序に依存したテストである点、例えば、途中のtestIDでバグが見つかった場合、以降のテストができなくなることはあり得ます。
どうしても一筆書きでテストを並べられなかった場合は、途中にリセット操作が挟まる場合もあります。（testID 6）
一番軽いテストである点、手動テストに向いていると思います。
開始状態（起動した後、利用開始直後　など）が、状態A / 状態B / 状態C であるかの確認もできますね。

# まとめ
今回は直前のみ覚える仕様で考えましたが、2回前を覚えたり、操作によって、覚えたり・覚えなかったりする仕様もあり得ます。<br>
状態が3つだけなんてことも、実際には無いですね。2回前まで覚えるなら操作の数と状態の数は増えます、結果的に生成されるテストケース数も増えます。<br>
事例3のように、旨くは並べ替えられなくて、途中のリセット操作が増えるかもしれませんね。<br>
状態が再現されているかどうかの確認は、単純に画面表示だけでは無いこともあります。（APIの戻り値を確認する等）<br>
完成度によっては、0-スイッチで期待通りに動くかどうかを見てから、さらに状態遷移していくテストをした方が近道という場合もあります。<br>
正しい状態遷移図を描くことが目的では無く、限られた時間などのリソース内で、いかに効果的なテストをしていくかで、テストやプロジェクト全体の進捗が左右されることが多いです。<br>
いろんなプロジェクトでソフトウェアテストをする経験を重ねると、様々なバグに遭遇する機会が増えてきて、そんな経験を状態遷移テストに反映させるようなケースがあるかと思います。
モデルを共有して有効なテストをレビューから洗練するツールとしても、GIHOZが使えるかなと思います。<br>
モデル化して、テストの規模感がなんとなく判ってきたら、全てをテストケース生成するのでは無く、
商品やサービスの中で、特に重要な状態やイベントに絞った状態遷移テストを開発者とイメージを合わせてテスト実施というのが現実的です。