WACATE2014夏にてテスト設計のワークショップ実施

少し前の話になりますが、先日WACATE2014夏というソフトウェアテストの合宿勉強会にて、テストに関わるワークショップを担当させていただきました。

ワークショップ解説資料

プログラム概要


テーマは、テスト設計を対象とした、リバースエンジニアリング、追加変更、保守性改善の3つで構成しています。具体的には、問題を抱えるテスト手順書と仕様書を用いて、以下を実施頂きました。

  • テスト設計のリバースエンジニアリングを行い、リバースしたテスト設計に追加変更を加えて再度手順書に展開する。
  • テストの保守性の問題を分析し、その改善策を提案して頂く。


(WACATEではよくありますが)ワークショップでは参加者の方々に想定外の優れたアプローチを構築頂き、モデレータ側としても、学ぶ所が多い有意義な場となりました。

解説資料以外の資料についてはまだオープンにしていませんが、機会があれば公開できればと考えています。
実行委員・参加者の方々には改めてお礼申し上げます。

CRESTの生成値が記録されない問題

CRESTは、デフォルトでは解析対象を実行する際に生成した値を出力しない。
生成値を出力する方法としては、公式サイトにて、run_crest本体のコードに細工する方法が以下で説明されている。
https://github.com/jburnim/crest/wiki/CREST-Frequently-Asked-Questions#does-crest-save-the-test-inputs-it-generates

ただ上記の場合、一回のイテレーションで生成する値がすべて0だった場合、値が保存されないようになっている。0とそれ以外での挙動の差異は、CRESTを使ってテスト設計からテスト実行まで自動化しようとする際に注意がいる。

原因として、コードを見ると、解析処理を行うSearch::RunProgram()にて、生成値を格納した引数inputsのサイズが0だと、生成する値をすべて0に初期化する処理を行っているようだ。そして調べて見る限り、CRESTは解析ですべての生成値が0だった場合、inputsをサイズ0のままにしてSearch::RunProgram()を実行し、生成値を初期化している。


そのため対策だけど、「記録された生成値ファイルが空であれば、生成値はすべて0にする」と判断すれば一応支障はなさそうだ。
一方0以外の時と同じように値を記録する際は、ソースコードでのSearch::WriteInputToFileOrDie()に細工が必要になる。例えば生成対象の変数が1つだけなら、以下の様な細工をすると生成値が0かどうかで場合分けが不要になる。

void Search::WriteInputToFileOrDie(const string& file, const vector<value_t>& input) {
  FILE* f = fopen(file.c_str(), "w");
  if (!f) {
    fprintf(stderr, "Failed to open %s.\n", file.c_str());
    perror("Error: ");
    exit(-1);
  }

  for (size_t i = 0; i < input.size(); i++) {
    fprintf(f, "%lld\n", input[i]);
  }
  if (input.size() == 0) {
    fprintf(f, "0\n");
  }
  fclose(f);
}

ただ、そもそも上記の公式の方法では、生成値の型や変数名といった構文情報が失われる。生成値を記録する際は、マクロのCREST_*()に細工して、解析対象から直接生成値を出力させたほうが都合が良いかもしれない。

Concolic Testingツール:CRESTによるテストケースの自動生成

ちょっと前に、C言語向けConcolic TestingツールであるCRESTを簡易的なサンプルで試したので、結果を簡単にまとめたいと思う。
なおCRESTを使用した雑感は以下の通り:

  • コードから実行パターンを生成するのみ。テストオラクル問題をどうにかしないといけない。
  • 制御パスの分析は大変高性能。複合条件も分析できるので、MC/DC網羅のテストケース生成や、パス数の見積もりが可能。
  • ループや再起など繰り返しの解析は行わない。例えばループ中で特定の関数を何回呼び出したかといったことは解析できない。ループカバレッジにも対応できず。
  • 外部コンポーネントの呼び出しでパス数が爆発しやすい。Test Doubleへの置換の仕組みが必要。

全体として、動的ユニットテストを用途に限ると、リグレッションテスト以外ではCRESTは使いにくい(テストオラクル問題を何とかしなければならない点と、コードベースである点などから)。そこを使えるようにするには独自のツールサポートやDbCの導入などが必要そう。ただ動的解析としては役に立つ用途がいくつかある。これについてはいずれ出せればと思う。

サンプルでの結果

単純な分岐

サンプル
int hoge(int a)
{
        CREST_int(a);

        if (a == 1) {
                return 1;
        }
        return 0;
}
解析結果
Read 2 branches.
Read 11 nodes.
Wrote 0 branch edges.
実行結果
Iteration 0 (0s): covered 0 branches [0 reach funs, 0 reach branches].
Iteration 1 (0s): covered 1 branches [1 reach funs, 2 reach branches].
Iteration 2 (0s): covered 2 branches [1 reach funs, 2 reach branches].
まとめ

単純な例でもあり、パスを網羅している。

switch

サンプル
int hoge(int a)
{
        CREST_int(a);

        switch (a) {
        case 0:
                return 1;
        case 1:
                return 2;
        default:
                return -1;
        }
        return 0;
}
解析結果
Read 6 branches.
Read 22 nodes.
Wrote 3 branch edges.
まとめ

ビルドして解析を行っているので、次のマクロ関数のサンプル含め網羅的にパスをピックアップできている。run_crestでもパスを全網羅している

マクロ関数

サンプル
#define FUGA(a) ((a==1) ? 1 : 0)

int hoge(int a)
{
        CREST_int(a);
        int b = FUGA(a);

        return (FUGA(b));
}
実行結果
Read 4 branches.
Read 13 nodes.
Wrote 4 branch edges.
まとめ

CRESTの仕様上、プリプロセッサを実行したあとのコードを網羅している。

ループ

サンプル
int hoge(int a)
{
        int i;
        CREST_int(a);
        for (i = 0; i < a; i++) {
                if (a < 100) {   
                        return 0;
                }
        }
        return 1;
}
解析結果
Read 2 branches.
Read 12 nodes.
Wrote 1 branch edges.
まとめ

基本的に分岐しか見ていない。分岐を扱うカバレッジについては高機能だが、ループカバレッジなどは扱えない。

再帰

サンプル
int hoge(int a)
{
        int b;
        CREST_int(a);
        if (a == 0) {
                return 1;
        }
        b = hoge(a - 1);
        return a * b;

}
解析結果
Read 4 branches.
Read 19 nodes.
Wrote 4 branch edges.
まとめ

ループと同様。再帰関数内にCREST_int()を記述したがこれは良くないかもしれない。なおこちらはrun_crestでのイテレーション数が20を超えた。

ランダム

サンプル
int hoge(int a)
{
        CREST_int(a);
        srand((unsigned int)time(NULL));

        if ((rand() % a) == 0)
        {
                return 0;
        }
        return 1;
}
解析結果
Read 2 branches.
Read 11 nodes.
Wrote 0 branch edges.
実行結果
Iteration 0 (0s): covered 0 branches [0 reach funs, 0 reach branches].
Floating point exception (core dumped)
Iteration 1 (0s): covered 1 branches [1 reach funs, 2 reach branches].
その他

当然かも知れないがrun_crestの実行結果は不安定。rand()のようなテスト対象外の外部コンポーネントはTest Double化が必要になる。

複合条件

サンプル
int hoge(int a)
{
        CREST_int(a);
        if (a > 200 && a < 400) {
                return 0;
        }
        return 1;
}
解析結果
Read 4 branches.
Read 13 nodes.
Wrote 2 branch edges.
まとめ

解析、実行ともに、複合条件を分解して網羅できていた。複合条件を扱えるユニットテストツールは高価な有償ツールが多いので、この点はCRESTが優秀だと感じる。

AAAにてテスト自動化の品質特性について講演

先日、AAAというテスト自動化のイベントで、25分の短時間枠ですが、テスト自動化に関わる品質モデルや品質特性についてのセッションの機会を頂きました。

http://www.slideshare.net/goyoki/ss-36405244

イベントも大変エモい感じで、楽しい一日でした。
運営の方々、参加者の方々、大変ありがとうございました。

探索的テスト入門

だいぶ前に探索的テストの入門解説を行う機会があったのですが、最近その資料を公開しました。

探索的テスト入門改訂版/Introduction to exploratory testing - Speaker Deck

なお今年読んだExplore It!の内容を一部加筆しています。
一般的な割に意外と探索的テストの日本語資料は少ないようですが、何かしらの一助になれば幸いです。

Classification Tree法(クラシフィケーションツリー法)について

※Classification Tree法のまとまった解説として以下資料を作成しました:

クラシフィケーションツリー法入門/Introduction to Classification Tree Method - Speaker Deck

最新は上記参照ください。以降はバックアップです。

●●●●

 ソフトウェアテストの分野では、日本語圏と英語圏で話題や志向が違うことが結構ある。
 その違いの代表例の一つに、テスト技法であるClassification Tree法がある。この技法は、海外ではISO/IEC 29119が代表的なテスト技法として挙げているなどそれなりの知名度を持っているそうだが、日本国内では知名度がかなり低い。
 今回はそのClassification Tree法について、簡単に紹介したいと思う。

Classification Tree法の概要

 Classification Tree法(Classification Tree Method。クラシフィケーションツリー法。分類木法。略称はCTM)は、テスト観点や同値クラスモデリング手法に属する。
 決定木と同じく、元々Classification Treeは用途をテストに限らない汎用的なモデリング手法として使われてきた。テスト設計ではそれにいくつかのサブセットルールと意味論的な定義を加えて用いることになる。
 用途だけれど、テスト設計でのClassification Tree法は、分類木を使って組み合わせや同値分割を階層構造でモデル化するのに用いられる。それによって、同値分割のズームイン・ズームアウトを容易にする、組み合わせの分布を抽象・具象両面で評価可能にする、テスト観点の分析をやりやすくする、といったメリットを確保する。またツールを使って、テスト網羅度のビジュアライズやテストの自動生成をサポートさせる場合もある。
 なおこの手法と類似している手法として、テスト観点のモデリングを扱うNGTや、テスト設計手法であるFOTで似た図を用いている。違いとしては、前者はテストアーキテクチャ設計を行うためのテスト観点のパッケージングなどが補強されている。後者は原因結果グラフと同じように、組み合わせの制約を図中に記述できるように拡張されている。

Classification Tree法の運用

 Classification Tree法ではモデリングやテスト設計支援でツールを用いるのが一般的になっている。
 ツールによっては、Classification Treeのモデルを作成すると、それに基づいて組み合わせテストを自動生成してくれるものがある。そのためClassification Tree法はモデルベースドテストの実現手段として見られることもあるようだ。
 Classification Tree法では、モデル上の各要素をテストケースにおいてどう組み合わせるかは自由度をもたせている。そこではそれぞれのテストの目的や制約に応じて、テスト設計者やツールが全網羅や2因子間網羅などを選択することになる。

Classification Treeの書き方

 Classification Treeは以下の構成要素でモデルを構成する

  • Aspect : テスト観点。Classficationとも呼ばれる。文献によってはClassの集合と表現されることもある。
  • Domain : テスト入力の集合や同値クラス。Classとも呼ばれる。

 また、最上位のAspectを「Aspect of interest」と呼称し扱いを区別することもある。


 モデルでは上記をツリー上でつなげて記述する。具体的には、以下のようなパターンでツリーを作成する。

  • 幹側の「Aspect」に、それを構成する「Domain」の枝をつなげる
  • 幹側の「Aspect」に、より具体的な「Aspect」の枝をつなげる
  • 幹側の「Domain」に、それを分解するための「Aspect」の枝をつなげる。

 なおモデルの表記法は文献によってばらつきがある。ツールによるアシストが一般的な手法のため、ツールの違いが表記のばらつきにつながっているようだ。表記法の例をいくつか以下に紹介する。

●「Guide to Advanced Software Testing」の場合

 書籍 「Guide to Advanced Software Testing」 では以下のような書式スタイルをとる。

  • Aspect : 四角の枠線で囲って描く
  • Domain : 丸の枠線で囲って描く

例を以下に示す。

●「Test Case Design Using Classification Trees」の場合

 割と著名な資料 「Test Case Design Using Classification Trees」 では以下のようなスタイルをとる。

  • Aspect : 四角の枠線で囲って描く
  • Domain : 枠線なしで描く

例を以下に示す。


ツールによるサポート

 Classification Tree法はツールによって拡張されることが多い。例えばCTEというツールでは、原因結果グラフと同じような制約条件を各構成要素に設定し、テストケースを自動生成できるようにする機能をサポートしている。

キーワード駆動テストの導入

 最近キーワード駆動テストがややバズワード化している傾向を感じている。というのも、キーワード駆動テストの導入で無用な手間を増やしている場面を見るようになっているためだ。
 キーワード駆動テストはフレームワークによっては手間を増やすことがあるので、その導入にあたっては、導入内容が目的に見合っているか多少の注意を向ける必要があると感じる。基本的な事柄であえて言及する必要もない内容かも知れないが、今回はそれについて簡単に触れたい。

キーワード駆動テストの目的

 言及するまでもないかもしれないけれども、何かしらの改善を行う際は、その手段が目的に見合っているか留意する必要がある。ではキーワード駆動テストの目的は何かというと、大雑把にまとめて以下の3つがある。なおこれは排他ではなく、一緒に目指しても良い。

目的(1)テストの保守性改善

 まず目的の一つに、テスト設計やテスト実装物の保守性改善のための構造化手段として、キーワード駆動テストを導入する場合がある。例えば以下のような目的だ。

  • 重複テストコードを共通ロジックに、非重複テストコードをキーワードに展開してテストスクリプトのコピペを削減し、保守のミスを防ぐ。
  • 可変性分析で抽出した流動的要素をキーワードに展開することで、テストケースやテストコードの変更性を高める。

目的(2)非開発者をテスト設計に巻き込む

 次に、非開発者でもテスト設計・実装が行えるようにするのもキーワード駆動テストの主な目的とされる。例えばテスト実装スキルのないユーザやテストエンジニアが、キーワードを組み合わせてテスト設計を行ったり、キーワードの組み合わせでテストをレビューしたりするのを実現する手段として、キーワード駆動テストのフレームワークを導入する。そこでは具体的にはExcelWikiによるテストケースの記述や、非開発者が読めるDSLでのテストケースの記述などが実現される。
 キーワード駆動テスト向けのフレームワークは、基本こちらの目的に対応したものが多い。

目的(3)テストの抽象化

 その他としては、テストの保守性と被る部分もあるが、テストケースの抽象化手段として用いられることがある。例えば、抽象的な手順をアクションワードで表現し具体的な手順はフレームワークが担保することで、以下を実現する。

  • テスト対象が変更されてもテストケースに影響がないようにする(フレームワークが変更部分を吸収する)。
  • 一つのアクションワードを複数の環境や条件間のテスト手順に横断的に展開できるようにする。

 この目的も、キーワード駆動テストの目的として一般的だ。

キーワード駆動テストの導入の注意点:目的を取り違えないこと

目的(2)のツールを導入する場合の注意点

 まず、見ていて注意が必要だと感じているのが、上記の目的(1)目的(3)の達成のために、目的(2)の用途のフレームワークや技術を導入してしまうパターンだ。
 目的(2)のために作られたフレームワークは、非技術者でもテスト設計を担当できるようにする。その代償として、保守性の作りこみに制限をかけたり、キーワードをテストスクリプトに落としこむ手間でテスト実装の効率性を落としたりすることがある。極端な例だと、「テストケース仕様をExcelで書くようにして構成管理コストを悪化させる」「テストケースのフォーマットをキーワード形式に無理に合わせるためにテストの可読性を犠牲にする」といったものがある。
 そのため、目的(2)のフレームワークを導入すると、目的(1)(3)の達成を難しくする場合がある。そして上記でも軽く触れたけれど、商用のキーワード駆動テストのフレームワークは目的(2)の用途を想定したものが多いので注意が必要だと感じる。目的(2)の達成も十分価値があることだけれど、その価値が活かせない環境ならば、手間が増えるだけになってしまうかもしれない。

目的(1)(3)の改善が必要な場合の注意点

 一方、目的(1)目的(3)が要求される状況では、背景としてそもそもテストスクリプトの品質の悪さが根本原因である場合が少なくない点に留意が必要だ。フレームワークを導入してキーワード形式でフォーマットを固定するよりも、愚直にテストスクリプトの設計改善を行ったほうが問題が解決することがある。
 コードやスクリプトの形式をフレームワークで固定して設計品質を向上させるのはDIやバインディングなどであるけれども、それは設計品質の向上を意図して作られているからできるというのを忘れてはいけない。

目的(2)の改善が必要な場合の注意点

 また目的(2)が要求される場合、そもそもスキル的な問題が根本原因にあることがある。そこではフレームワークを導入するより、テストスクリプトを書けるように教育・人員確保を行ったり、スクリプトの保守性を上げて部分的にでも非開発者がメンテナンスできるようにしたほうが、全体の生産性が高まる場合がある。
 あと目的(2)については、キーワード駆動テストにスクリプトを無理に合わせこむことで、ユーザ等にとってかえってテストがわかりにくくなり書きづらくなることがある。今では柔軟なDSLでテストを書けるフレームワークが複数出ているので、キーワード駆動テスト以外の選択肢の必要性も考慮したほうが良いと思う。

まとめ

 とにかく、キーワード駆動テストを導入する際は、新技術だから、流行っているからという印象でフレームワークに飛びすくのではなく、目的とフレームワークの方向性が一致しているか検討するのも大事だと思う。極端に言えば「担当者は皆テストコードが書けるのに、新技術という事であえてテストをコードでなくExcelで書くように変更した」と同じようなことをしてしまっている可能性もあると感じる。