デシジョンテーブルテストについて

Software

前回の記事で同値クラステスト / 境界値テストを紹介しましたが、今回の記事ではデシジョンテーブルテストについて解説します。

今回も以下の本をベースに解説していきます。

ソフトウェアテストには、以下の種類のテストがあります。

概要

デシジョンテーブルとは、ソフトウェアの動作と対応する条件を一覧化した表のことで、この表を用いて行うテストをデシジョンテーブルテストといいます。

同値クラステストや境界値テストが単一の条件に着目したテスト技法であるのに対して、デシジョンテーブルテストは複数の条件に着目したテスト技法です。

例題

同値クラステスト / 境界値テストの例では、赤外線センサでロボット – 壁間の距離を計測をして、この情報のみを元にロボットの状態を判定しました。しかし、実際にはロボットには複数のセンサが搭載されていて、それぞれのセンサの情報を元に状態管理を行うことが多いです。

ここでは、以下のセンサを用いて、それぞれの条件に応じて最大速度を変更するプログラムを考えます。

  • 赤外線センサ
  • 人感センサ
  • 光量センサ

複数の条件を同時に満たす場合、最大速度はもっとも小さいものを適用します。たとえば、条件 No.1 と 条件 No.2 が満たされる場合は、No.2 が適用され、最大速度は 0.0 m/s となります。

Noセンサ範囲・条件動作(アクション)最大速度
1赤外線センサ0.3 m 以内スピードを落とす0.3 m/s
2人感センサ0.5 m 以内ロボットを停止させる0.0 m/s
3光量センサ10 lx 以下
ref1, ref2
スピードを落とす
(周囲が暗いときはゆっくり動く)
0.5 m/s
4上記条件以外通常速度にする5.0 m/s

これをデシジョンテーブルとして表すと、以下のようになります。たとえば、ルールの3は条件1が yes、条件2が no、条件3が yes のときに、最大速度は 0.3 m/s となることを表しています。

条件・アクション \ ルール12345678
条件1:赤外線センサ 0.3 m 以内yyyynnnn
条件2:人感センサ 0.5 m 以内yynnyynn
条件3:光量センサ 10 lx 以下ynynynyn
アクション:最大速度 0.0 m/syyyy
アクション:最大速度 0.3 m/syy
アクション:最大速度 0.5 m/sy
アクション:最大速度 5.0 m/sy

デシジョンテーブルの書き方

上記の例のように、すべての条件の組み合わせと対応するアクションを書き表した表がデシジョンテーブルです。

条件の組み合わせを、ルールと呼ぶことにします。

デシジョンテーブルの書き方のポイントは以下です。

  • 条件の数を $n$ としたとき、$2^n$ 個のルールを書く。
  • $i$ 個目のルールは、$\frac{n}{2^i}$ ごとに y と n を入れ替える。
  • アクション欄は、ソフトウェアの仕様から期待される結果を書き込む。

上記の2つ目は少しイメージしにくいかもしれないので、例を示します。たとえば例題だと、1個目のルール(yyyynnnn)は4($= \frac{8}{2^1}$)ごとに y と n を入れ替えています。同様に、2個目のルール(yynnyynn)は2($= \frac{8}{2^2}$)ごとに y と n を入れ替えています。

ソースコード

上記の例題のソースコードを書いてみます。ソースコードは、github にも上げています。

赤外線センサ InfraredSensor・人感センサ HumanSensor・光量センサ LightSensor が距離や光量などの情報から制限速度を計算し、それぞれの制限速度を RobotMultiSensor が統合してロボットの制限速度を割り出します。

robot_multi_sensors.h

#include <bits/stdc++.h>
#include <string>
using namespace std;

class RobotMultiSensors
{
        private:
        public:
                RobotMultiSensors() {}
                ~RobotMultiSensors() {}
                float limit_vel_fusion(float inf_vel, float hum_vel, float lit_vel);
};

class InfraredSensor
{
        private:
        public:
                InfraredSensor() {}
                ~InfraredSensor() {}
                float limit_vel(float distance);
};

class HumanSensor
{
        private:
        public:
                HumanSensor() {}
                ~HumanSensor() {}
                float limit_vel(float distance);
};

class LightSensor
{
        private:
        public:
                LightSensor() {}
                ~LightSensor() {}
                float limit_vel(float brightness);
};

robot_multi_sensors.cpp

#include "robot_multi_sensors.h"

float RobotMultiSensors::limit_vel_fusion(float inf_vel, float hum_vel, float lit_vel)
{
        float limit_vel = min(inf_vel, hum_vel);
        limit_vel = min(limit_vel, lit_vel);
        return limit_vel;
}

float InfraredSensor::limit_vel(float distance)
{
        float limit_vel = (distance >= 0 && distance <= 0.3) ? 0.3 : 5.0;
        return limit_vel;
}

float HumanSensor::limit_vel(float distance)
{
        float limit_vel = (distance >= 0 && distance <= 0.5) ? 0.0 : 5.0;
        return limit_vel;
}

float LightSensor::limit_vel(float brightness)
{
        float limit_vel = (brightness >= 0 && brightness <= 10) ? 0.5 : 5.0;
        return limit_vel;
}

テストコード

google test でのテストコードの書き方については、前回の記事で軽く触れています。

たとえば、rule4 のテストでは、それぞれのセンサが以下の値を出力したときのロボットの制限速度が 0.3 m/s であることを確かめています。

センサ種類出力結果
赤外線センサ0.1 m
人感センサ1.0 m
光量センサ20 lx

float の値をテストしたい場合、前回のように EXPECT_EQ() は使えないそうです。(参考サイト

以下のコードで、デシジョンテーブルの8通りのテストをすべて記述しています。

test_dec_table.cpp

#include <gtest/gtest.h>

#include "robot_multi_sensors.h"

RobotMultiSensors rms = RobotMultiSensors();
InfraredSensor is = InfraredSensor();
HumanSensor hs = HumanSensor();
LightSensor ls = LightSensor();

TEST(RobotMultiSensorsDecisionTableTest, rule1) {
	float i_dist = 0.1; float h_dist = 0.1; float l_bright = 1;
	float i_lv = is.limit_vel(i_dist);
	float h_lv = hs.limit_vel(h_dist);
	float l_lv = ls.limit_vel(l_bright);

	double expected_lv = 0.0;
	EXPECT_FLOAT_EQ(expected_lv, rms.limit_vel_fusion(i_lv, h_lv, l_lv));
}

TEST(RobotMultiSensorsDecisionTableTest, rule2) {
	float i_dist = 0.1; float h_dist = 0.3; float l_bright = 15;
	float i_lv = is.limit_vel(i_dist);
	float h_lv = hs.limit_vel(h_dist);
	float l_lv = ls.limit_vel(l_bright);

	double expected_lv = 0.0;
	EXPECT_FLOAT_EQ(expected_lv, rms.limit_vel_fusion(i_lv, h_lv, l_lv));
}

TEST(RobotMultiSensorsDecisionTableTest, rule3) {
	float i_dist = 0.08; float h_dist = 1.0; float l_bright = 1;
	float i_lv = is.limit_vel(i_dist);
	float h_lv = hs.limit_vel(h_dist);
	float l_lv = ls.limit_vel(l_bright);

	double expected_lv = 0.3;
	EXPECT_FLOAT_EQ(expected_lv, rms.limit_vel_fusion(i_lv, h_lv, l_lv));
}

TEST(RobotMultiSensorsDecisionTableTest, rule4) {
	float i_dist = 0.1; float h_dist = 1.0; float l_bright = 20;
	float i_lv = is.limit_vel(i_dist);
	float h_lv = hs.limit_vel(h_dist);
	float l_lv = ls.limit_vel(l_bright);

	double expected_lv = 0.3;
	EXPECT_FLOAT_EQ(expected_lv, rms.limit_vel_fusion(i_lv, h_lv, l_lv));
}

TEST(RobotMultiSensorsDecisionTableTest, rule5) {
	float i_dist = 0.5; float h_dist = 0.2; float l_bright = 7;
	float i_lv = is.limit_vel(i_dist);
	float h_lv = hs.limit_vel(h_dist);
	float l_lv = ls.limit_vel(l_bright);

	double expected_lv = 0.0;
	EXPECT_FLOAT_EQ(expected_lv, rms.limit_vel_fusion(i_lv, h_lv, l_lv));
}

TEST(RobotMultiSensorsDecisionTableTest, rule6) {
	float i_dist = 0.5; float h_dist = 0.2; float l_bright = 18;
	float i_lv = is.limit_vel(i_dist);
	float h_lv = hs.limit_vel(h_dist);
	float l_lv = ls.limit_vel(l_bright);

	double expected_lv = 0.0;
	EXPECT_FLOAT_EQ(expected_lv, rms.limit_vel_fusion(i_lv, h_lv, l_lv));
}

TEST(RobotMultiSensorsDecisionTableTest, rule7) {
	float i_dist = 0.5; float h_dist = 1.2; float l_bright = 6;
	float i_lv = is.limit_vel(i_dist);
	float h_lv = hs.limit_vel(h_dist);
	float l_lv = ls.limit_vel(l_bright);

	double expected_lv = 0.5;
	EXPECT_FLOAT_EQ(expected_lv, rms.limit_vel_fusion(i_lv, h_lv, l_lv));
}

TEST(RobotMultiSensorsDecisionTableTest, rule8) {
	float i_dist = 0.7; float h_dist = 0.7; float l_bright = 70;
	float i_lv = is.limit_vel(i_dist);
	float h_lv = hs.limit_vel(h_dist);
	float l_lv = ls.limit_vel(l_bright);

	double expected_lv = 5.0;
	EXPECT_FLOAT_EQ(expected_lv, rms.limit_vel_fusion(i_lv, h_lv, l_lv));
}

ソースコード・テストコードは、以下のコマンドでコンパイル・実行します。

$ g++ -std=c++11 robot_multi_sensors.cpp test_dec_table.cpp -o test_dec_table -L/usr/local/lib -lgtest -lgtest_main -lpthread
$ ./test_dec_table

以下の結果から、すべてテストが成功していることを確認できます。

$ ./test_dec_table 
 Running main() from /usr/local/src/googletest-release-1.8.1/googletest/src/gtest_main.cc
 [==========] Running 8 tests from 1 test case.
 [----------] Global test environment set-up.
 [----------] 8 tests from RobotMultiSensorsDecisionTableTest
 [ RUN      ] RobotMultiSensorsDecisionTableTest.rule1
 [       OK ] RobotMultiSensorsDecisionTableTest.rule1 (0 ms)
 [ RUN      ] RobotMultiSensorsDecisionTableTest.rule2
 [       OK ] RobotMultiSensorsDecisionTableTest.rule2 (0 ms)
 [ RUN      ] RobotMultiSensorsDecisionTableTest.rule3
 [       OK ] RobotMultiSensorsDecisionTableTest.rule3 (0 ms)
 [ RUN      ] RobotMultiSensorsDecisionTableTest.rule4
 [       OK ] RobotMultiSensorsDecisionTableTest.rule4 (0 ms)
 [ RUN      ] RobotMultiSensorsDecisionTableTest.rule5
 [       OK ] RobotMultiSensorsDecisionTableTest.rule5 (0 ms)
 [ RUN      ] RobotMultiSensorsDecisionTableTest.rule6
 [       OK ] RobotMultiSensorsDecisionTableTest.rule6 (0 ms)
 [ RUN      ] RobotMultiSensorsDecisionTableTest.rule7
 [       OK ] RobotMultiSensorsDecisionTableTest.rule7 (0 ms)
 [ RUN      ] RobotMultiSensorsDecisionTableTest.rule8
 [       OK ] RobotMultiSensorsDecisionTableTest.rule8 (0 ms)
 [----------] 8 tests from RobotMultiSensorsDecisionTableTest (0 ms total)
 [----------] Global test environment tear-down
 [==========] 8 tests from 1 test case ran. (0 ms total)
 [  PASSED  ] 8 tests.

ここまでの議論で、デシジョンテーブルテストに関する基礎と実装方法はまとめたので、ここから先はオマケです。

デシジョンテーブルの簡略化

デシジョンテーブルは、条件の数に応じて指数関数的にルールの数が多くなってしまいます(条件数が $n$ のとき、ルールの数は $2^n$)。

そこで、デシジョンテーブルを簡略化するために以下のような手法が提案されています。

  • 矛盾する条件の削除
  • 同じアクションになるルールをまとめる
  • 同値クラス分類による条件 / アクションの簡略化

この説明だけでは何のことかわからないと思うので、先ほどの例題に新たに 1′ の条件を加えた場合のデシジョンテーブルを考え、これを元にデシジョンテーブル簡略化の手法を解説していきます。

Noセンサ範囲・条件動作(アクション)最大速度
1赤外線センサ0.3 m 以内スピードを落とす0.3 m/s
1′赤外線センサ0.1 m 以内ロボットを停止させる0.0 m/s
2人感センサ0.5 m 以内ロボットを停止させる0.0 m/s
3光量センサ10 lx 以下
ref1, ref2
スピードを落とす
(周囲が暗いときはゆっくり動く)
0.5 m/s
4上記条件以外通常速度にする5.0 m/s

先ほどと同様に表を埋めていくと、以下のようになります。

Tab 1: Complicated decision table

条件・アクション \ ルール12345678910111213141516
条件1:赤外線センサ 0.3 m 以内yyyyyyyynnnnnnnn
条件1’:赤外線センサ 0.1 m 以内yyyynnnnyyyynnnn
条件2:人感センサ 0.5 m 以内yynnyynnyynnyynn
条件3:光量センサ 10 lx 以下ynynynynynynynyn
アクション:最大速度 0.0 m/syyyyyyyyyyyy
アクション:最大速度 0.3 m/syy
アクション:最大速度 0.5 m/sy
アクション:最大速度 5.0 m/sy

矛盾する条件の削除

たとえば、赤外線センサに関して以下の二つの条件は矛盾します。

  • 条件1:赤外線センサ 0.3 m 以内:no
  • 条件1’:赤外線センサ 0.1 m 以内:yes

赤外線センサが 0.1 m 以下の出力を出した場合、「条件1:赤外線センサ 0.3 m 以内」も yes になるはずです。しかし、このような矛盾する条件も Tab 1 には含まれてしまっています。

このような場合、矛盾する条件に対応するアクションの欄には n/a と表記し、テストを行わないこととします。今回着目した箇所は青太字で示しています。

条件・アクション \ ルール12345678910111213141516
条件1:赤外線センサ 0.3 m 以内yyyyyyyynnnnnnnn
条件1’:赤外線センサ 0.1 m 以内yyyynnnnyyyynnnn
条件2:人感センサ 0.5 m 以内yynnyynnyynnyynn
条件3:光量センサ 10 lx 以下ynynynynynynynyn
アクション:最大速度 0.0 m/syyyyyyn/an/an/an/ayy
アクション:最大速度 0.3 m/syyn/an/an/an/a
アクション:最大速度 0.5 m/sn/an/an/an/ay
アクション:最大速度 5.0 m/sn/an/an/an/ay

この簡略化手法を適用したデシジョンテーブルでは、確認すべきルールが 16 個から 12 個に減りました。

※ただし、矛盾する条件がちゃんとプログラム側ではじかれるかテストしたい場合もあるので、テストケースとして含めるか含めないかはプロジェクトごとの判断になります。

同じアクションになるルールをまとめる

今回の例だと、以下の条件のいずれかが満たされるとき、他の条件に関わらずアクションは必ず「最大速度 0.0 m/s」となります。

  • 条件1’:赤外線センサ 0.1 m 以内
  • 条件2:人感センサ 0.5 m 以内

これらの条件が成り立つとき、他の条件については考慮する必要がないので、以下のようにデシジョンテーブルを簡略化します。考慮する必要のない条件には「-」を表記しています。

同様に、条件1が y で、条件1′, 2が n のときは条件3に関わらず最大速度は0.3 m/s になります。

条件・アクション \ ルール1571516
条件1:赤外線センサ 0.3 m 以内ynn
条件1’:赤外線センサ 0.1 m 以内ynnn
条件2:人感センサ 0.5 m 以内ynnn
条件3:光量センサ 10 lx 以下yn
アクション:最大速度 0.0 m/syy
アクション:最大速度 0.3 m/sy
アクション:最大速度 0.5 m/sy
アクション:最大速度 5.0 m/sy

この方法で、ルールが 16 個から 5 個に減りました。効果抜群ですね。

同値クラス分類による条件 / アクションの簡略化

条件表記の簡略化

条件1と条件1’の赤外線センサの計測結果 d [m] に関する条件を同値クラスとしてまとめると、以下のようなクラスに分類されます。

  • 0 <= d <= 0.1
  • 0.1 < d <= 0.3
  • d > 0.3

アクション表記の簡略化

これまでのデシジョンテーブルでは、最大速度 0.0 m/s, 0.3 m/s, 0.5 m/s, 5.0 m/s をそれぞれ別々に表記していましたが、数値を直接デシジョンテーブルに記入しても大丈夫です。

上記の二つの観点から、デシジョンテーブルを簡略化すると以下のようになります。

条件・アクション \ ルール123456789101112
条件1:赤外線センサ距離 d [m]0 <= d <= 0.10 <= d <= 0.10 <= d <= 0.10 <= d <= 0.10.1 < d <= 0.30.1 < d <= 0.30.1 < d <= 0.30.1 < d <= 0.3d > 0.3d > 0.3d > 0.3d > 0.3
条件2:人感センサ 0.5 m 以内yynnyynnyynn
条件3:光量センサ 10 lx 以下ynynynynynyn
アクション:最大速度 v [m]0.00.00.00.00.00.00.30.30.00.00.55.0

元々 16 個あったルールが 12 個に削減されています。

注意点

デシジョンテーブルテストは、いろいろな組み合わせのテストケースを網羅できるテストですが、一つのルールに対して一つのテストケースしか作成しないとバグの発見を見逃す可能性もあります。同値クラステストや境界値テスト等と組み合わせて、重要な箇所では一つのルールに対して複数のテストケースを作成するなどの工夫も必要です。

特に、見やすくするためにデシジョンテーブルを簡略化した場合、重要なテストケースまで削ってしまっていないか注意する必要があります。

活用法

また、デシジョンテーブルはバグの潜んでいる箇所を発見するのにも役に立ちます。例えば条件1〜3のうち、条件1と条件2が両方 y のときのみ動作不具合がある場合(下記デシジョンテーブルの赤太字の箇所だけ期待結果と応答が合わない場合)、条件1, 2の両条件を満たす処理にバグが潜んでいる可能性が高いと絞り込みを行うことができます。

条件・アクション \ ルール12345678
条件1:赤外線センサ 0.3 m 以内yyyynnnn
条件2:人感センサ 0.5 m 以内yynnyynn
条件3:光量センサ 10 lx 以下ynynynyn
アクション:最大速度 [m/s]0.00.00.30.30.00.00.55.0

まとめ

今回は、デシジョンテーブルテストに関して記事をまとめました。複数の条件を扱えるテスト技法です。

本記事で参考にした「ソフトウェアテストの教科書」という本に、より詳細にテスト技法について書かれているので、手にとってみることをおすすめします。

コメント