状態遷移テストについて

Software

今回の記事では、状態遷移テストについて紹介します。以下が参考図書です。

同値クラステストやデシジョンテーブルテスト等はこれまでの記事で紹介してきました。

概要

状態遷移テストは「状態遷移図」や「状態遷移表」を用いたテストのことです。これらを用いると、状態遷移の全体像を把握でき、ソフトウェアの動きを網羅的に確認することができます。

状態

状態は以下のようなカテゴリーに分けられます。

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回は通る
  2. すべてのイベントを1回は発生させる
  3. すべての遷移を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 E1event E2event E3
state S1state S2state S3
state S2state 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 S1state S2state S3
state S1event E1event E3
state S2event E2
state S3

「状態 x イベント」の状態遷移表(Tab 2 のような状態遷移表)はイベントの数に応じて表が大きくなってしまうものの、表を作成していくことで状態やイベントの抽出漏れに気づけるという利点があるため、「状態 x イベント」の状態遷移表が推奨されています。

メリット

  • すべての状態とすべてのイベントの組み合わせを一覧表示できるため、仕様が曖昧な箇所を特定できる
  • 状態遷移図の不備を見つけられる

テストケース

状態遷移表をベースとしてテストケースを作る場合は、1つの升目を1つのテストケースとします。たとえば、「Tab 2. Sample state transition table」を元にテストケースを作成すると以下のようになります。

Tab 4. Testcase for sample state transition table

No.遷移前の状態イベント遷移後の状態
1state S1event E1state S2
2state S1event E3state S3
3state S2event E2state S3
4state S1event E2状態遷移しない
5state S2event E1状態遷移しない
6state S2event E3状態遷移しない
7state S3event E1状態遷移しない
8state S3event E2状態遷移しない
9state S3event 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

E1E2E3E4E5E6E7E8
S1S2
S2S1S3S4
S3S4S5S5
S4S3S5S5S3
S5S2

状態遷移図の状態に注目して、矢印が伸びているイベントがある場合は升目を埋めていくようにすると状態遷移表を作りやすいです。(上の表だと、横一列を上から順に埋めていくイメージで状態遷移表を作っていくと作りやすいです。)

テストケース

今回は、状態遷移するケースのみをテストケースとして抽出します。

Tab 4. Testcase for a drone system example

No.previous stateeventtransferred state
1S1: unarmedE1: ArmS2: armed
2S2: armedE2: DisarmS1: unarmed
3S2: armedE3: Push hover buttonS3: hovering
4S2: armedE4: Push go_destination buttonS4: go_destination
5S3: hoveringE4: Push go_destination buttonS4: go_destination
6S3: hoveringE5: Push stop buttonS5: landing
7S3: hoveringE6: Detect invalid sensory measurementS5: landing
8S4: go_destinationE3: Push hover buttonS3: hovering
9S4: go_destinationE5: Push stop buttonS5: landing
10S4: go_destinationE6: Detect invalid sensory measurementS5: landing
11S4: go_destinationE7: Reach the destinationS3: hovering
12S5: landingE8: Landing finishS2: 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 のテストでは、

  1. 初期状態として armed 状態をセットする
  2. 目的地を “area_a” とする
  3. 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.

まとめ

状態遷移テストについて要約して、例題を作りソースコードを書いてみました。テストをしてソースコードに誤りが無いかを見つけるのも大事ですが、状態遷移図や状態遷移表を作っていく最中に仕様に問題がないかを考えていくことも大事だなと思いました。

コメント