同値クラステスト / 境界値テストについて

Software

ソフトウェアの品質向上のため、ソフトウェアテストの設計についてしっかり勉強したいと思い、「ソフトウェアテストの教科書」という本で勉強しました。Part 2「さまざまなテスト技法」では、様々なテスト技法の解説と注意点が網羅的に書かれていて、非常にわかりやすかったのでおすすめです。

上記の本で学んだことをベースに例題を考え、コード + テストコードを書いてみます。

参考図書では、以下のテスト技法がそれぞれ紹介されています。

  • 同値クラステスト
  • 境界値テスト
  • デシジョンテーブルテスト
  • 状態遷移テスト
  • 組み合わせテスト

全部を解説すると記事が長くなってしまうので、本記事では同値クラステスト / 境界値テストを解説します。

※極論すべての入出力をテストケースとしてテストをすればすべてのバグを発見できるのですが、これは現実的には不可能です。そこで、上記のテスト技法を用いてテストケース数を減らし、効率よくバグを発見するというのがテスト技法を導入する目的です。

同値クラステスト

概念

同値クラスとは、同じ動作をする条件の集まりとして定義されます。各同値クラスから最低限1つの代表値を用いてテストを行うテスト技法を、同値クラステストと呼びます。

代表値として、以下の値を使うケースが多いそうです。

  • 中間値
  • デフォルトの設定値
  • 入力される頻度の高い値

ロボットの赤外線センサで壁との距離を計測して、一定の距離内に入ったらロボットの速度を落とし(slow)、さらに近づくと停止する(stop)例を考えます。この例ではセンサの読み取り値に応じて、ロボットの状態が以下の表のように遷移します。

センサ値に関する条件状態
30 cm 以上normal
10 cm 以上 30 cm 未満slow
0 cm 以上 10 cm 未満stop
0 cm 未満error

センサが 0 cm 未満の値を出力しているときは、センサの読み取りエラー等が発生しているはずなので、エラー状態(error)とします。

ロボットの状態遷移をソースコードで記述すると、以下のようになります。簡単のため、センサ値は整数値を取ると仮定して、dist の型は int 型にしています。

string  RobotDistance::get_robot_state(int dist) {
    string state = "normal";
    if (dist >= 30) state = "normal";
    else if (dist >= 10) state = "slow";
    else if (dist >= 0) state = "stop";
    else state = "error";

    return state;
}

この場合、それぞれの状態が実現される距離(dist)を代表値としてテストケースを作成すると、たとえば以下のような4つのテストケースを作ることができます。

Tab 1: Test cases for equivalence partitioning

センサ値に関する条件状態テストケース
(ロボット – 壁間の距離)
30 cm 以上normal45 cm
10 cm 以上 30 cm 未満slow20 cm
0 cm 以上 10 cm 未満stop5 cm
0 cm 未満error-5 cm

「テストケース」の列がテスト入力に対応し、「状態」の列が期待結果(出力)に対応します。

注意点

仕様書とソフトウェアの内部構造が一致しているとは限らないため、仕様書を元にした同値クラスの分類は不十分である可能性があります。

たとえば、以下のソースコードは上の例題の仕様を満たしたコードですが、分岐が余分に1つあります。このようなコードに対してすべての分岐を通るテストをしたい場合は、余分に 50 cm 以上のテストケースを導入する必要があります。

string  RobotDistance::get_robot_state(int dist) {
    string state = "normal";
    if (dist >= 50) state = "normal";
    else if (dist >= 30) state = "normal";
    else if (dist >= 10) state = "slow";
    else if (dist >= 0) state = "stop";
    else state = "error";

    return state;
}
センサ値に関する条件状態テストケース
(ロボット – 壁間の距離)
50 cm 以上normal65 cm (追加すべきテストケース)
30 cm 以上normal45 cm
10 cm 以上 30 cm 未満slow20 cm
0 cm 以上 10 cm 未満stop5 cm
0 cm 未満error-5 cm

境界値テスト

概念

境界値テストは、仕様条件の境界の値とその隣の値に対してテストを行うテスト技法です。境界付近にバグが潜んでいる可能性は高いため、境界値テストは重要です。

テストする値は以下の3パターンあります。

  • 境界値
  • 境界値の1つ上(境界値と同値クラスなら省略可)
  • 境界値の1つ下(境界値と同値クラスなら省略可)

境界値の隣の値を考えるときは、最小単位に気をつける必要があります。たとえば、仕様で最小単位が 0.01 cm と定められていれば、境界値 30 cm と隣り合う値は 29.99 cm と 30.01 cm ですし、最小単位が 1 cm と定められていれば隣り合う値は 29 cm と 31 cm です。

最小単位境界値境界値と隣り合う値
0.01 cm30 cm29.9 cm, 30.1 cm
1 cm30 cm29 cm, 31 cm

先と同様の例に対して、境界値テストのテストケースを考えます。注意事項は以下です。

  • 最小単位は 1 cm
  • 境界値と同値クラスのテストケースも省略しない

Tab 2: Test cases for boundary value analysis

センサ値に関する条件同値クラス状態テストケース
(ロボット – 壁間の距離)
30 cm 以上x >= 30normal30 cm
normal31 cm
slow29 cm
10 cm 以上 30 cm 未満10 <= x <= 29slow28 cm
※29 cm と 30 cm はテストケース作成済
slow10 cm
slow11 cm
stop 9 cm
0 cm 以上 10 cm 未満0 <= x <= 9stop8 cm
stop1 cm
stop0 cm
error-1 cm
0 cm 未満x <= -1error-2 cm

「テストケース」の列がテスト入力に対応し、「状態」の列が期待結果(出力)に対応します。

同値クラステストではテストケースは 4 個でしたが、境界値テストでは 12 個あります。

ソースコード

ソースコードは、github に上げています。

上記の例題のソースコードを c++ で書くと、以下のようなコードになります。先ほどは関数 get_robot_state() のみを記述しましたが、今回は全体のソースコードを記載しています。

robot_distance.h

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

class RobotDistance
{
        private:
        public:
                RobotDistance() {}
                string get_robot_state(int dist);
};

robot_distance.cpp

#include "robot_distance.h"

string RobotDistance::get_robot_state(int dist) {
        string state = "normal";
        if (dist >= 30) state = "normal";
        else if (dist >= 10) state = "slow";
        else if (dist >= 0) state = "stop";
        else state = "error";

        return state;
}

テストコード

テスト方法(google test)

google test とは google が提供している、単体テストの自動実行フレームワークのことです。今回はこのフレームワークを使用します。こちらのサイトで導入方法とサンプルコードが書かれていたので、ご参照ください。

上記サイトのコンパイルコマンドをそのまま実行すると、私の環境ではエラーが発生しました。このサイトを参考に最後に -lpthread オプションを追加することで、上記サイトのサンプルコードをコンパイルすることができました。

$ g++ -std=c++11 sample.cc sample_test.cc -o test -L/usr/local/lib -lgtest -lgtest_main -lpthread

同値クラステスト

Tab 1 のテストケースを参考に、テストコードを作成しました。クラス化されたコード用の google test の書き方はこのサイトを参考にしました。

test_class.cpp

#include <gtest/gtest.h>

#include "robot_distance.h"

TEST(RobotDistanceClass, Normal) {
        RobotDistance rd = RobotDistance();
        EXPECT_EQ("normal", rd.get_robot_state(45));
}

TEST(RobotDistanceClass, Slow) {
        RobotDistance rd = RobotDistance();
        EXPECT_EQ("slow", rd.get_robot_state(20));
}

TEST(RobotDistanceClass, Stop) {
        RobotDistance rd = RobotDistance();
        EXPECT_EQ("stop", rd.get_robot_state(5));
}

TEST(RobotDistanceClass, Error) {
        RobotDistance rd = RobotDistance();
        EXPECT_EQ("error", rd.get_robot_state(-5));
}

例えば1つ目のテストは、大項目を RobotDistanceClass、小項目を Normal と設定しています(これらは単なる名前なので自由に決められます)。このテストでは、ロボット – 壁間の距離が 45 cm であるときに、状態が “normal” となることを確かめています。

ビルド方法

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

実行結果

$ ./test_class
 Running main() from /usr/local/src/googletest-release-1.8.1/googletest/src/gtest_main.cc
 [==========] Running 4 tests from 1 test case.
 [----------] Global test environment set-up.
 [----------] 4 tests from RobotDistanceClass
 [ RUN      ] RobotDistanceClass.Normal
 [       OK ] RobotDistanceClass.Normal (0 ms)
 [ RUN      ] RobotDistanceClass.Slow
 [       OK ] RobotDistanceClass.Slow (0 ms)
 [ RUN      ] RobotDistanceClass.Stop
 [       OK ] RobotDistanceClass.Stop (0 ms)
 [ RUN      ] RobotDistanceClass.Error
 [       OK ] RobotDistanceClass.Error (0 ms)
 [----------] 4 tests from RobotDistanceClass (0 ms total)
 [----------] Global test environment tear-down
 [==========] 4 tests from 1 test case ran. (0 ms total)
 [  PASSED  ] 4 tests.

上記実行結果から、すべてのテストがパスしていることがわかります。

境界値テスト

Tab 2 のテストケースを参考に、テストコードを作成しました。

test_boundary.cpp

#include <gtest/gtest.h>

#include "robot_distance.h"

TEST(RobotDistanceBoundary, NormalLower) {
	RobotDistance rd = RobotDistance();
	EXPECT_EQ("normal", rd.get_robot_state(30));
}

TEST(RobotDistanceBoundary, NormalLowerPlusOne) {
	RobotDistance rd = RobotDistance();
	EXPECT_EQ("normal", rd.get_robot_state(31));
}

TEST(RobotDistanceBoundary, NormalLowerMinusOne) {
	RobotDistance rd = RobotDistance();
	EXPECT_EQ("slow", rd.get_robot_state(29));
}

TEST(RobotDistanceBoundary, SlowUpperMinusOne) {
	RobotDistance rd = RobotDistance();
	EXPECT_EQ("slow", rd.get_robot_state(28));
}

TEST(RobotDistanceBoundary, SlowLower) {
	RobotDistance rd = RobotDistance();
	EXPECT_EQ("slow", rd.get_robot_state(10));
}

TEST(RobotDistanceBoundary, SlowLowerPlusOne) {
	RobotDistance rd = RobotDistance();
	EXPECT_EQ("slow", rd.get_robot_state(11));
}

TEST(RobotDistanceBoundary, SlowLowerMinusOne) {
	RobotDistance rd = RobotDistance();
	EXPECT_EQ("stop", rd.get_robot_state(9));
}

TEST(RobotDistanceBoundary, StopUpperMinusOne) {
	RobotDistance rd = RobotDistance();
	EXPECT_EQ("stop", rd.get_robot_state(8));
}

TEST(RobotDistanceBoundary, StopLower) {
	RobotDistance rd = RobotDistance();
	EXPECT_EQ("stop", rd.get_robot_state(0));
}

TEST(RobotDistanceBoundary, StopLowerPlusOne) {
	RobotDistance rd = RobotDistance();
	EXPECT_EQ("stop", rd.get_robot_state(1));
}

TEST(RobotDistanceBoundary, StopLowerMinusOne) {
	RobotDistance rd = RobotDistance();
	EXPECT_EQ("error", rd.get_robot_state(-1));
}

TEST(RobotDistanceBoundary, ErrorUpperMinusOne) {
	RobotDistance rd = RobotDistance();
	EXPECT_EQ("error", rd.get_robot_state(-2));
}

ビルド方法

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

実行結果

$ ./test_boundary
 Running main() from /usr/local/src/googletest-release-1.8.1/googletest/src/gtest_main.cc
 [==========] Running 12 tests from 1 test case.
 [----------] Global test environment set-up.
 [----------] 12 tests from RobotDistanceBoundary
 [ RUN      ] RobotDistanceBoundary.NormalLower
 [       OK ] RobotDistanceBoundary.NormalLower (0 ms)
 [ RUN      ] RobotDistanceBoundary.NormalLowerPlusOne
 [       OK ] RobotDistanceBoundary.NormalLowerPlusOne (0 ms)
 [ RUN      ] RobotDistanceBoundary.NormalLowerMinusOne
 [       OK ] RobotDistanceBoundary.NormalLowerMinusOne (0 ms)
 [ RUN      ] RobotDistanceBoundary.SlowUpperMinusOne
 [       OK ] RobotDistanceBoundary.SlowUpperMinusOne (0 ms)
 [ RUN      ] RobotDistanceBoundary.SlowLower
 [       OK ] RobotDistanceBoundary.SlowLower (0 ms)
 [ RUN      ] RobotDistanceBoundary.SlowLowerPlusOne
 [       OK ] RobotDistanceBoundary.SlowLowerPlusOne (0 ms)
 [ RUN      ] RobotDistanceBoundary.SlowLowerMinusOne
 [       OK ] RobotDistanceBoundary.SlowLowerMinusOne (0 ms)
 [ RUN      ] RobotDistanceBoundary.StopUpperMinusOne
 [       OK ] RobotDistanceBoundary.StopUpperMinusOne (0 ms)
 [ RUN      ] RobotDistanceBoundary.StopLower
 [       OK ] RobotDistanceBoundary.StopLower (0 ms)
 [ RUN      ] RobotDistanceBoundary.StopLowerPlusOne
 [       OK ] RobotDistanceBoundary.StopLowerPlusOne (0 ms)
 [ RUN      ] RobotDistanceBoundary.StopLowerMinusOne
 [       OK ] RobotDistanceBoundary.StopLowerMinusOne (0 ms)
 [ RUN      ] RobotDistanceBoundary.ErrorUpperMinusOne
 [       OK ] RobotDistanceBoundary.ErrorUpperMinusOne (0 ms)
 [----------] 12 tests from RobotDistanceBoundary (0 ms total)
 [----------] Global test environment tear-down
 [==========] 12 tests from 1 test case ran. (0 ms total)
 [  PASSED  ] 12 tests.

12 個の境界値テストがすべて成功していることがわかります。

失敗するテストケース

あえて失敗するテストケースも用意してみました。異常系と呼ばれるものです。下記は、戻り値が “normal” になるはずのテストケースで、あえて期待値を “stop” してみた例です。

test_fail.cpp

#include <gtest/gtest.h>

#include "robot_distance.h"

TEST(RobotDistanceFail, Normal) {
        RobotDistance rd = RobotDistance();
        EXPECT_EQ("stop", rd.get_robot_state(45));
}

ビルド方法

g++ -std=c++11 robot_distance.cpp test_fail.cpp -o test_fail -L/usr/local/lib -lgtest -lgtest_main -lpthread

実行結果

$ ./test_fail
 Running main() from /usr/local/src/googletest-release-1.8.1/googletest/src/gtest_main.cc
 [==========] Running 1 test from 1 test case.
 [----------] Global test environment set-up.
 [----------] 1 test from RobotDistanceFail
 [ RUN      ] RobotDistanceFail.Normal
 test_fail.cpp:7: Failure
 Expected equality of these values:
   "stop"
   rd.get_robot_state(45)
     Which is: "normal"
 [  FAILED  ] RobotDistanceFail.Normal (1 ms)
 [----------] 1 test from RobotDistanceFail (1 ms total)
 [----------] Global test environment tear-down
 [==========] 1 test from 1 test case ran. (1 ms total)
 [  PASSED  ] 0 tests.
 [  FAILED  ] 1 test, listed below:
 [  FAILED  ] RobotDistanceFail.Normal
 1 FAILED TEST

テストの実行結果が FAILED となっていることが確認できます。

まとめ

同値クラステストと境界値テストについて例題を通して説明し、最後に google test を用いた単体テストの実装を行いました。google test は思ったより簡単に導入することができました。

コメント