ソフトウェアの品質向上のため、ソフトウェアテストの設計についてしっかり勉強したいと思い、「ソフトウェアテストの教科書」という本で勉強しました。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 以上 | normal | 45 cm |
10 cm 以上 30 cm 未満 | slow | 20 cm |
0 cm 以上 10 cm 未満 | stop | 5 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 以上 | normal | 65 cm (追加すべきテストケース) |
30 cm 以上 | normal | 45 cm |
10 cm 以上 30 cm 未満 | slow | 20 cm |
0 cm 以上 10 cm 未満 | stop | 5 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 cm | 30 cm | 29.9 cm, 30.1 cm |
1 cm | 30 cm | 29 cm, 31 cm |
例
先と同様の例に対して、境界値テストのテストケースを考えます。注意事項は以下です。
- 最小単位は 1 cm
- 境界値と同値クラスのテストケースも省略しない
Tab 2: Test cases for boundary value analysis
センサ値に関する条件 | 同値クラス | 状態 | テストケース (ロボット – 壁間の距離) |
30 cm 以上 | x >= 30 | normal | 30 cm |
normal | 31 cm | ||
slow | 29 cm | ||
10 cm 以上 30 cm 未満 | 10 <= x <= 29 | slow | 28 cm ※29 cm と 30 cm はテストケース作成済 |
slow | 10 cm | ||
slow | 11 cm | ||
stop | 9 cm | ||
0 cm 以上 10 cm 未満 | 0 <= x <= 9 | stop | 8 cm |
stop | 1 cm | ||
stop | 0 cm | ||
error | -1 cm | ||
0 cm 未満 | x <= -1 | error | -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 は思ったより簡単に導入することができました。
コメント