今回の記事では、状態遷移テストについて紹介します。以下が参考図書です。
同値クラステストやデシジョンテーブルテスト等はこれまでの記事で紹介してきました。
- 同値クラステスト / 境界値テスト
- デシジョンテーブルテスト
- 状態遷移テスト
- 組み合わせテスト
概要
状態遷移テストは「状態遷移図」や「状態遷移表」を用いたテストのことです。これらを用いると、状態遷移の全体像を把握でき、ソフトウェアの動きを網羅的に確認することができます。
状態
状態は以下のようなカテゴリーに分けられます。
Tab 1. Category of states and examples
カテゴリー | 例 |
機器の部位の状態 | ランプ :点灯 / 点滅 / 消灯 スイッチ:ON / OFF |
処理の状態 | 計算中 / データ読み込み中 |
モード | ストップウォッチ:計測中 / 停止中 ポット :保温中 / 給湯中 |
状態遷移図
状態遷移図とは状態の遷移を図式化したもので、たとえば以下の仕様の状態遷移図は Fig 1 のように表せます。
- state S1 で event E1 が発生すると、state S2 に遷移する
- state S1 で event E3 が発生すると、state S3 に遷移する
- state S2 で event E2 が発生すると、state S3 に遷移する
Fig 1. Basic state transition diagram
作り方
状態遷移図は、以下の表記方法にしたがって記述します。
Fig 2. Basic components of state transition diagram
最近のソフトウェアは構造が複雑なものが多いため、状態遷移図が巨大になってしまうことがあります。そのため、機能ごとに状態遷移図を分割するなどの工夫が必要です。たとえば、携帯電話には「メール / アドレス帳 / 予定表 / カメラ / ブラウザ」等の機能がありますが、これらは一つ一つ別の状態遷移図に書き表した方が見通しがよくなります。
メリット
- 状態遷移の流れを簡単に把握できる
- 状態遷移図を使うことでエンジニア間のコミュニケーションがスムーズになる
- 以下のような仕様の不備に気づける
- どこにも遷移しない状態がある
- 複数の異なる状態に遷移するイベントがある など
- ユーザーが実際に使うシナリオを想定する際の手助けになる
テストケース
状態遷移図をベースにテストケースを作る際は、以下のルールに従って作っていくと良いです。
- すべての状態を1回は通る
- すべてのイベントを1回は発生させる
- すべての遷移を1回は通る
1のテストケースはソフトウェアの単体機能が完成したら、2のテストケースはそれぞれのイベント処理が完成したら、3のテストケースはソフトウェア全体が完成したら、といった具合で段階的にテストを実施していくと良いそうです。
状態遷移表
状態遷移表は状態遷移を表で表したもので、以下の仕様の状態遷移は Tab 2 のように表せます。
- state S1 で event E1 が発生すると、state S2 に遷移する
- state S1 で event E3 が発生すると、state S3 に遷移する
- state S2 で event E2 が発生すると、state S3 に遷移する
Tab 2. Sample state transition table
event E1 | event E2 | event E3 | |
state S1 | state S2 | – | state S3 |
state S2 | – | state S3 | – |
state S3 | – | – | – |
たとえば表の左上端(太字)は state S1 で event E1 が発生すると state S2 に遷移することを表しています。つまり、升目の中の状態が遷移先の状態となっています。
また、「-」となっている升目は「どこにも遷移しない」という意味になります。参考図書では、どこにも遷移しないイベントを「-」で表し、起こり得ないイベントを「N/A」で表しています。たとえば、CDプレイヤーで「再生中」にCDを挿入することは物理的に不可能なので N/A となります。
「状態 x 状態」の状態遷移表
縦軸と横軸両方を状態にして、状態遷移表を作ることもできます。
Tab 3. Sample state transtion table (state x state case)
state S1 | state S2 | state S3 | |
state S1 | – | event E1 | event E3 |
state S2 | – | – | event E2 |
state S3 | – | – | – |
「状態 x イベント」の状態遷移表(Tab 2 のような状態遷移表)はイベントの数に応じて表が大きくなってしまうものの、表を作成していくことで状態やイベントの抽出漏れに気づけるという利点があるため、「状態 x イベント」の状態遷移表が推奨されています。
メリット
- すべての状態とすべてのイベントの組み合わせを一覧表示できるため、仕様が曖昧な箇所を特定できる
- 状態遷移図の不備を見つけられる
テストケース
状態遷移表をベースとしてテストケースを作る場合は、1つの升目を1つのテストケースとします。たとえば、「Tab 2. Sample state transition table」を元にテストケースを作成すると以下のようになります。
Tab 4. Testcase for sample state transition table
No. | 遷移前の状態 | イベント | 遷移後の状態 |
1 | state S1 | event E1 | state S2 |
2 | state S1 | event E3 | state S3 |
3 | state S2 | event E2 | state S3 |
4 | state S1 | event E2 | 状態遷移しない |
5 | state S2 | event E1 | 状態遷移しない |
6 | state S2 | event E3 | 状態遷移しない |
7 | state S3 | event E1 | 状態遷移しない |
8 | state S3 | event E2 | 状態遷移しない |
9 | state S3 | event E3 | 状態遷移しない |
「状態遷移するケース」 -> 「状態遷移しないケース(-)」 -> 「起こり得ないケース(N/A)」の順で重要なので、この順にテストケースを作成することが推奨されています。
このように状態遷移表を用いると「状態遷移しないケース」や「起こり得ないケース」も含めて網羅的にテストケースを作成できます。そのため、想定されるユーザーシナリオ通りにシステムが動くかどうかを確認する際は状態遷移図をベースにしたテスト、使い方として想定されていない状態とイベントの組み合わせも網羅的にテストしたい場合は状態遷移表をベースにしたテストが適しています。
例題
ドローンのシステムを考えます。全体システムの流れや状態遷移について今回は注目します。
仕様
仕様は以下を満たしているものとします。
状態遷移
- 電源 ON で unarmed 状態になる
- armed 状態もしくは unarmed 状態のときのみ電源 OFF 可(飛行時の電源 OFF は危険なため)
- armed 状態と unarmed 状態はユーザーが切り替えられる
- armed 状態か go_destination 状態で hovering ボタンを押すと hovering 状態に切り替わる
- armed 状態か hovering 状態で go_destination ボタンを押すと go_desitination 状態に切り替わる
- go_destination 状態か hovering 状態で stop ボタンを押すと、landing 状態に切り替わる
- go_destination 状態か hovering 状態でセンサー異常を検知すると landing 状態に切り替わる
- go_destination 状態で目的地に到達すると、hovering 状態に切り替わる
- landing 状態で着地すると、armed 状態に切り替わる
状態の説明
Tab 5. Explanation of states of a drone system example
状態 | 説明 |
unarmed | ドローンがコマンドを受け付けない状態(準備状態) |
armed | ドローンがコマンドを受け付ける状態 |
hovering | 定点ホバリングしている状態 |
go_destination | 目的地に向かって飛行している状態 |
landing | 着地動作をしている状態 |
状態とイベント
Tab 5. States and events of a drone system example
状態 | イベント |
S1: unarmed S2: armed S3: hovering S4: go_destination S5: landing | E1: アーム E2: ディスアーム E3: hovering ボタンを押す E4: go_destination ボタンを押す E5: stop ボタンを押す E6: センサが異常値を検知 E7: 目的地に到達 E8: 着地終了 |
状態遷移図
Fig 3. State transition diagram of a drone system example
状態遷移表
Tab 5. State transition table of a drone system example
E1 | E2 | E3 | E4 | E5 | E6 | E7 | E8 | |
S1 | S2 | – | – | – | – | – | – | – |
S2 | – | S1 | S3 | S4 | – | – | – | – |
S3 | – | – | – | S4 | S5 | S5 | – | – |
S4 | – | – | S3 | – | S5 | S5 | S3 | – |
S5 | – | – | – | – | – | – | – | S2 |
状態遷移図の状態に注目して、矢印が伸びているイベントがある場合は升目を埋めていくようにすると状態遷移表を作りやすいです。(上の表だと、横一列を上から順に埋めていくイメージで状態遷移表を作っていくと作りやすいです。)
テストケース
今回は、状態遷移するケースのみをテストケースとして抽出します。
Tab 4. Testcase for a drone system example
No. | previous state | event | transferred state |
1 | S1: unarmed | E1: Arm | S2: armed |
2 | S2: armed | E2: Disarm | S1: unarmed |
3 | S2: armed | E3: Push hover button | S3: hovering |
4 | S2: armed | E4: Push go_destination button | S4: go_destination |
5 | S3: hovering | E4: Push go_destination button | S4: go_destination |
6 | S3: hovering | E5: Push stop button | S5: landing |
7 | S3: hovering | E6: Detect invalid sensory measurement | S5: landing |
8 | S4: go_destination | E3: Push hover button | S3: hovering |
9 | S4: go_destination | E5: Push stop button | S5: landing |
10 | S4: go_destination | E6: Detect invalid sensory measurement | S5: landing |
11 | S4: go_destination | E7: Reach the destination | S3: hovering |
12 | S5: landing | E8: Landing finish | S2: armed |
改善点
状態遷移図と表を作っていて、以下のような疑問や改善点が浮かんできました。
- landing 状態で異常なふるまいを検知した場合はどうするか?
- 目的地を設定したりドローンの運航速度を決めたりする setting 状態があるとよい
- 目的地が設定されていなかったときはどうする?
- 目的地に着いたらホバリングを続けるべきか着地すべきか?
- センサの観測結果はどれくらいの周期でチェックすべきか?
このように、状態遷移図や表を作っている最中にシステムの欠陥や不備、仕様の曖昧な部分を洗い出すことができます。
ソースコード
ソースコードは github にもアップロードしています。
ドローンシステムのサンプルコード
状態遷移を担う、state_manager.h と state_manager.cpp を以下のソースコードで実現しました。state_manager.cpp のそれぞれの API を呼ぶことで、State 型の private 変数の値が変わります。
本来はどのようにセンサ異常を検知するかを考えたり、ボタン押下の検知をコールバック関数で行う処理を書いたりも必要ですが、今回は状態遷移テストに関する説明なので割愛しました。
#include <bits/stdc++.h>
#include <string>
enum State {
armed,
unarmed,
hovering,
go_destination,
landing
};
class StateManager
{
private:
State state_ = unarmed;
std::string destination_ = "";
std::string position_ = "";
bool is_sensory_measurement_invalid_ = false;
bool is_touch_ground_ = false;
public:
StateManager() {}
State get_state() { return state_; }
State set_state(State state) { state_ = state; }
// E1: Arm
void push_arm_button();
// E2: Disarm
void push_disarm_button();
// E3: Push hover button
void push_hover_button();
// E4: Push go_destination button
void set_destination(std::string destination) { destination_ = destination; };
void push_go_destination_button();
// E5: Push stop button
void push_stop_button();
// E6: Detect invalid sensory measurement
void set_invalid_sensory_measurement_flag(bool flag) {
is_sensory_measurement_invalid_ = flag;
};
void check_sensory_measurement();
// E7: Reach the destination
void set_position(std::string position) { position_ = position; };
void check_position();
// E8: Landing finish
void set_touch_ground_flag(bool flag) { is_touch_ground_ = flag; };
void check_touch_ground();
};
#include "../include/state_manager.h"
void StateManager::push_arm_button() {
if (state_ == unarmed)
{
state_ = armed;
}
}
void StateManager::push_disarm_button() {
if (state_ == armed)
{
state_ = unarmed;
}
}
void StateManager::push_hover_button() {
if (state_ == armed || state_ == go_destination)
{
state_ = hovering;
}
}
void StateManager::push_go_destination_button() {
if ((state_ == armed || state_ == hovering) && destination_ != "")
{
state_ = go_destination;
}
}
void StateManager::push_stop_button() {
if (state_ == hovering || state_ == go_destination)
{
state_ = landing;
}
}
void StateManager::check_sensory_measurement() {
if ((state_ == hovering || state_ == go_destination) &&
is_sensory_measurement_invalid_)
{
state_ = landing;
}
}
void StateManager::check_position() {
if (state_ == go_destination && (position_ == destination_))
{
state_ = hovering;
}
}
void StateManager::check_touch_ground() {
if (state_ == landing && is_touch_ground_)
{
state_ = armed;
}
}
テストコード
たとえば、4_PushGoDestinationButton のテストでは、
- 初期状態として armed 状態をセットする
- 目的地を “area_a” とする
- go_destination ボタンを押す
という一連の処理を行ったときに、内部状態が armed 状態から go_destination 状態に変わっているかを確かめています。これは Tab 4 のテストケースの No. 4 に該当します。
#include <gtest/gtest.h>
#include "../include/state_manager.h"
TEST(StateManagerTransition, 1_Arm) {
StateManager sm = StateManager();
sm.set_state(unarmed);
sm.push_arm_button();
EXPECT_EQ(armed, sm.get_state());
}
TEST(StateManagerTransition, 2_Disarm) {
StateManager sm = StateManager();
sm.set_state(armed);
sm.push_disarm_button();
EXPECT_EQ(unarmed, sm.get_state());
}
TEST(StateManagerTransition, 3_PushHoverButton) {
StateManager sm = StateManager();
sm.set_state(armed);
sm.push_hover_button();
EXPECT_EQ(hovering, sm.get_state());
}
TEST(StateManagerTransition, 4_PushGoDestinationButton) {
StateManager sm = StateManager();
sm.set_state(armed);
sm.set_destination("area_a");
sm.push_go_destination_button();
EXPECT_EQ(go_destination, sm.get_state());
}
TEST(StateManagerTransition, 5_PushGoDestinationButton) {
StateManager sm = StateManager();
sm.set_state(hovering);
sm.set_destination("area_a");
sm.push_go_destination_button();
EXPECT_EQ(go_destination, sm.get_state());
}
TEST(StateManagerTransition, 6_PushStopButton) {
StateManager sm = StateManager();
sm.set_state(hovering);
sm.push_stop_button();
EXPECT_EQ(landing, sm.get_state());
}
TEST(StateManagerTransition, 7_DetectInvalidSensoryMeasurement) {
StateManager sm = StateManager();
sm.set_state(hovering);
sm.set_invalid_sensory_measurement_flag(true);
sm.check_sensory_measurement();
EXPECT_EQ(landing, sm.get_state());
}
TEST(StateManagerTransition, 8_PushHoverButton) {
StateManager sm = StateManager();
sm.set_state(go_destination);
sm.push_hover_button();
EXPECT_EQ(hovering, sm.get_state());
}
TEST(StateManagerTransition, 9_PushStopButton) {
StateManager sm = StateManager();
sm.set_state(go_destination);
sm.push_stop_button();
EXPECT_EQ(landing, sm.get_state());
}
TEST(StateManagerTransition, 10_DetectInvalidSensoryMeasurement) {
StateManager sm = StateManager();
sm.set_state(go_destination);
sm.set_invalid_sensory_measurement_flag(true);
sm.check_sensory_measurement();
EXPECT_EQ(landing, sm.get_state());
}
TEST(StateManagerTransition, 11_ReachTheDestination) {
StateManager sm = StateManager();
sm.set_destination("area_b");
sm.set_state(go_destination);
sm.set_position("area_b");
sm.check_position();
EXPECT_EQ(hovering, sm.get_state());
}
TEST(StateManagerTransition, 12_LandingFinish) {
StateManager sm = StateManager();
sm.set_state(landing);
sm.set_touch_ground_flag(true);
sm.check_touch_ground();
EXPECT_EQ(armed, sm.get_state());
}
テストコードの実行は、以下のコマンドによって行います。
$ g++ -std=c++11 src/state_manager.cpp test/test_state_transition.cpp -o test/test_state_transition -L/usr/local/lib -lgtest -lgtest_main -lpthread $ ./test/test_state_transition
テスト結果は以下のようになり、すべてのテストが pass していることがわかります。
Running main() from gtest_main.cc [==========] Running 12 tests from 1 test case. [----------] Global test environment set-up. [----------] 12 tests from StateManagerTransition [ RUN ] StateManagerTransition.1_Arm [ OK ] StateManagerTransition.1_Arm (0 ms) [ RUN ] StateManagerTransition.2_Disarm [ OK ] StateManagerTransition.2_Disarm (0 ms) [ RUN ] StateManagerTransition.3_PushHoverButton [ OK ] StateManagerTransition.3_PushHoverButton (0 ms) [ RUN ] StateManagerTransition.4_PushGoDestinationButton [ OK ] StateManagerTransition.4_PushGoDestinationButton (0 ms) [ RUN ] StateManagerTransition.5_PushGoDestinationButton [ OK ] StateManagerTransition.5_PushGoDestinationButton (0 ms) [ RUN ] StateManagerTransition.6_PushStopButton [ OK ] StateManagerTransition.6_PushStopButton (0 ms) [ RUN ] StateManagerTransition.7_DetectInvalidSensoryMeasurement [ OK ] StateManagerTransition.7_DetectInvalidSensoryMeasurement (0 ms) [ RUN ] StateManagerTransition.8_PushHoverButton [ OK ] StateManagerTransition.8_PushHoverButton (0 ms) [ RUN ] StateManagerTransition.9_PushStopButton [ OK ] StateManagerTransition.9_PushStopButton (0 ms) [ RUN ] StateManagerTransition.10_DetectInvalidSensoryMeasurement [ OK ] StateManagerTransition.10_DetectInvalidSensoryMeasurement (0 ms) [ RUN ] StateManagerTransition.11_ReachTheDestination [ OK ] StateManagerTransition.11_ReachTheDestination (0 ms) [ RUN ] StateManagerTransition.12_LandingFinish [ OK ] StateManagerTransition.12_LandingFinish (0 ms) [----------] 12 tests from StateManagerTransition (1 ms total) [----------] Global test environment tear-down [==========] 12 tests from 1 test case ran. (1 ms total) [ PASSED ] 12 tests.
まとめ
状態遷移テストについて要約して、例題を作りソースコードを書いてみました。テストをしてソースコードに誤りが無いかを見つけるのも大事ですが、状態遷移図や状態遷移表を作っていく最中に仕様に問題がないかを考えていくことも大事だなと思いました。
コメント