単体テストの効率化を考える(2)-スタブの戻り値

2021年1月30日

どもです。

「単体テストの効率化について考える」の2回目です。
(1回目はコチラ:単体テストの効率化を考える(1)-はじめに)

1.振り返り

前回のエントリでは、

  • 単体テストの効率化は、環境構築を効率化することで進めることができる!
  • 環境構築の効率化は、スタブの自動生成から!

という結論になりました。
そして、そのために、

  • スタブで行うべき処理について考える必要がある

となりました。
今回は、この「スタブで行うべき処理」について考えます。

2.スタブとは?

スタブとは

  • 特定のコンポーネント(仮にAと呼ぶ)をテストするため、Aに呼び出される(特定目的のための最小限度の)コンポーネント。

と定義されています。
(出典:ISTQB ソフトウェアテスト標準用語集)
これでは、スタブで行わなければならない具体的な処理を決定できません。
やっぱり自分で考える必要があります。

3.関数のパターン

3.1.最も基本的な関数

まず、「最も基本的な関数」について考えます。
「一番基本的な関数」は、「戻り値なし」/「引数なし」の関数であると、私は考えています。
即ち、以下のような関数です。

void FuncNoReturnNoArg()
{
    //何か処理
}

この関数のスタブは、どのようになるか考えます。

void FuncNoReturnNoArg()
{
    //何もしない
}

しかし、これでは実際にこのスタブが呼び出されたのか、さらに言えば「この関数を呼び出すパスを通過したのか」が判断できません。
具体例を挙げると、下記のような関数の単体テストを行う場合です。

int SampleFuncCaller(int arg)
{
    if (0 < arg) {
        FuncNoReturnNoArg();
    }
    
    return 0;
}

この関数では、argの値でFuncNoReturnNoArg()が呼ばれる/呼ばれないが異なります。
しかし、関数SampleFuncCaller()関数からの戻り値は変わりません。
そのため、戻り値を確認しても、FuncNoReturnNoArg()が呼ばれたか否かを判断できません。
この問題を解決するためには、「呼び出し回数をカウントする」ようにするのがよいと考えます。
即ち、以下のように実装します。

int FuncNoReturnNoArg_called_count = 0; //呼び出し回数
void FuncNoReturnNoArg()
{
    FuncNoReturnNoArg_called_count++;//呼び出し回数のインクリメント
}

「この関数を呼び出すパスを通過したのか」は、カバレッジの測定の際に有用な情報になります。
そのため、この「呼び出し回数のカウント」は戻り値や引数の有無に関わらず保持することがよいかと考えます。

3.2.関数の分類

「最も基本的な関数」として、「戻り値なし」/「引数なし」という関数を挙げました。
そこで、「戻り値」と「引数」に着目して関数を分類して、それぞれの分類ごとにスタブで行うべき処理を考えてみます。

3.3.「戻り値」に着目した分類

関数の戻り値については、基本的に「あり」と「なし」の2つだけです。

3.3.1.「戻り値なし」の場合

「戻り値なし」の場合は、先の「一番基本的な関数」と同様の実装になります。

3.3.2.「戻り値あり」の場合

戻り値を持つ関数については、何か値を返さなければなりません。
この時、「固定値を返す」のか「設定された値を返す」のかで議論が分かれるかと思います。

3.3.2.1.固定値を返す

固定値を返す場合のスタブの実装例を、下記に示します。

int FuncNoReturnNoArg_called_count = 0; //呼び出し回数
int SampleStub(void)
{
    SampleStub_called_count++;
    
    return 0;   //固定値を返す。
}

スタブの実装は大変シンプルです。
しかし、戻り値によって呼び出し元の関数(テスト対象の関数)の処理が変化する場合には、これでは不十分です。

void SampleFunction()
{
    int result_val = 0;
    result_val = SampleStub();
    if (0 == result_val) {
        //何か処理
    } else {
        //何か処理  ←上記スタブでは、ここに到達できない。
    }
}

対応として、「戻り値ごとに異なるスタブを実装する」という方法が考えられます。
しかし、それでは実装量が増大してしまいます。
即ち、テスト対象関数において判定値が追加される度にスタブを新規に追加しなければなりません。
「スタブを自動で作成する」ので、実装の手間は増えませんが(増えないようにしますが)、作成された関数の管理が必要になります。
例えば、「1」と「-1」を返すスタブを実装する場合は、それぞれ

int FuncNoReturnNoArg_called_count = 0; //呼び出し回数
int SampleStub_Return_1(void)
{
    SampleStub_called_count++;
    
    return (1);     //固定値を返す。
}
int SampleStub_Return_Minus1(void)
{
    SampleStub_called_count++;
    
    return (-1);    //固定値を返す。
}

となります。
このような実装にした場合、テストケースごとに適用されるスタブを切り替えるための仕組みが必要になります。
またこの仕組みのメンテナンスも必要になります。
即ち、「固定値を返す」ようなスタブを作成する場合、「戻り値ごとに作成されたスタブ」と「テストケースごとに関数を切り替える仕組み」を管理する必要があります。
管理しなければならないモノが増えるのは、あまりよろしくないです。

3.3.2.2.設定された値を返す

設定された値を返す場合の実装例を、下記に示します。

int FuncNoReturnNoArg_called_count = 0; //呼び出し回数
int SampleStub(void)
{
    SampleStub_called_count++;
    
    return SampleStub_ret_val;
}

この実装であれば、戻り値によってテスト対象の関数の処理が変化するとしても、戻り値ごとに関数を追加する必要はありません。
固定値を返す場合に比べて、管理しなければならない項目が少なくなります。
しかし、この方法では「スタブが複数回呼び出された場合」が問題になります。
即ち、「実行される度に異なる値を返す」という場合です。
これには、「スタブが返す値を配列で持たせる」という方法で対応ができます。
実装例としては、下記になります。

int FuncNoReturnNoArg_called_count = 0; //呼び出し回数
int SampleStub_ret_val[100];            //戻り値のバッファ(サイズは暫定)
int SampleStub(void)
{
    int ret_val = SampleStub_ret_val[SampleStub_called_count];
    SampleStub_called_count++;
    
    return ret_val;
}

4.続く!!

またまた長くなってきたので、今回のエントリはこの辺にしておきます。

今回のエントリでは、自動生成するスタブで行うべき処理について考えました。
結論ですが、

  • スタブでは、呼び出し回数をカウントする。
  • 戻り値がある関数のスタブは、返す値を設定しておくバッファを配列で用意しておく。
  • 戻り値がある関数のスタブは、呼び出される度に配列から値を取得し、それを返す。

とするのがよいかと思います。

次回は、スタブの引数について書きます。

ではっ!

続きはコチラ

単体テストの効率化を考える(3)-スタブの引数
単体テストの効率化を考える(4)-スタブの引数(ダブルポインタ)
単体テストの効率化を考える(5)-スタブの自動生成への入力
単体テストの効率化を考える(6)-スタブの自動生成ツール