jcunit's blog

JCUnitの開発日誌(ログ)です。"その時点での"JCUnit作者の理解や考え、開発状況を日本語で書きます。

JCUnitによる有限状態機械のテスト - 有限状態機械テストの方法(その1)

はじめに

かれこれ三カ月くらい頑張って、有限状態機械(以下、FSM)をJCUnitを使ってテストする機能を作ってみた。
今回はひとまず0.4.20としてリリースした。MavenのCoordinateは以下のとおり。

<dependency>
  <groupId>com.github.dakusui</groupId>
  <artifactId>jcunit</artifactId>
  <version>0.4.20</version>
</dependency>

テストは十分ではないので、バグはまだあると思う。
この追加はかなり大きなものだったし、実用性という点でもひとつの区切りになるものだと思う。
デバッグが終わったら、0.5.0としてリリースしなおしたい。

使い方を解説する記事を書こうと思ったのだが、かなりの分量になりそうなので、何回かに分けてここのブログに掲載したい。

大幅な変更は難しいかもしれないが、多少の機能やインタフェースの変更は可能なので、コメントをいただければ幸いである。
また、記事中に分かりにくい点などあれば、これもコメントをいただきたい。

まず、サンプルはこちら。


このテストは、内部に状態をもつクラス、FlyingSpaghettiMonsterを「イイ感じ」にテストする。
どう「イイ感じ」かというと、「状態、状態遷移関数、状態遷移関数に与えられる引数、これらそれぞれの履歴を因子・水準として、ペアワイズでテストを生成・実行」するのだが、ひとまずここでは「イイ感じ」とだけ、しておきたい。
FSMテストの生成方式についてのエントリを書くときに、詳しく述べる。
今回は、「使い方」に集中する。

テスト対象クラス(SUT)

まずは、テスト対象クラスを示す。

package com.github.dakusui.jcunit.examples.fsm;
import com.github.dakusui.jcunit.core.Checks;
public class FlyingSpaghettiMonster {
  private String dish = null;
  public String cook(String pasta, String sauce) {
    this.dish = pasta;
    return String.format("Cooking %s %s", pasta, sauce);
  }
  public String eat() {
    if (dish != null) {
      return String.format("%s is yummy!", this.dish);
    }
    throw new IllegalStateException();
  }
  public boolean isReady() {
    return dish != null;
  }
}

調理(cook)したものを食べる(eat)だけの単純なクラスだ。調理前(初期状態)と調理後の二つの状態があり、一旦調理後の状態に遷移したら調理前にもどることはない。

テストの書き方

次に、テスト(FlyingSpaghettiMonster)を見ていこう。
ストファイルへのリンクを再掲する。


クラス宣言部

まず、クラス宣言部だが、通常のテストと同じく、"JCUnit.class"をRunWithアノテーションに指定する。

@RunWith(JCUnit.class)
public class FlyingSpaghettiMonsterTest {

この部分だが、他のテストと同様、IPO2TupleGeneratorなどを明示的に指定することもできる。

@RunWith(JCUnit.class)
@TupleGeneration(
  generator = @Generator(value = IPO2TupleGenerator.class, params = @Param("3"))
)
public class FlyingSpaghettiMonsterTest {

この部分は通常のテストとなんら変わることはない。

フィールド宣言部

クラスのフィールド宣言で、FSMが使用されていることが判明するとJCUnitは、「イイ感じ」のテスト生成を始める。
テスト内には、FSMのテストを行うためのフィールドが二つある。mainとsetUpというのがそれらだ。
それぞれ役割が違うので一つずつ説明する。

まず、main。

  @FactorField(
    levelsProvider = FSMLevelsProvider.class,
      providerParams = {
        @Param("flyingSpaghettiMonster"),
        @Param("main")
  })
  public ScenarioSequence<FlyingSpaghettiMonster> main;

このフィールドにはその名のとおり、"main"のテスト生成結果が格納される。
一つ目のパラメタ、"flyingSpaghettiMonster"は、テストしようとしている有限状態機械のオブジェクトを返却する静的メソッドの名前。
二つ目のパラメタが"main"になっていれば、フィールド名は任意。

ScenarioSequenceは、FSMを「どうテストするか?」というシナリオが格納されている。(後述)
このシナリオは以下のコードによって実行でき、これがいわばほぼテストそのものである。

    FSMUtils.performScenarioSequence(this.main, this.sut, ScenarioSequence.SIMPLE_REPORTER);

ScenarioSequenceはその名のとおり、Scenarioを連なりでScenarioは、

  • 現在の状態(given - State)
  • それに対する操作(when - Action)
  • その結果期待される出力・状態(then - Expectation)

を合わせたものである。
JCUnitに一旦、あるFSMの仕様を与えるとこのオブジェクトScenarioSequenceが「イイ感じに」自動で生成されるのだ。

で、上にあげたメソッドperformScenarioSequenceを実行すると、現在の状態にたいして、whenで与えられた操作を行い、その結果得られる出力や、送出される例外、実行後の状態を検査する。
これをScenarioSequence内の各要素にたいして繰り返す。
ということで「イイ感じ」のテストが実施できるというわけ。

これがJCUnitによるFSMのテストの核心。

が、これで実用的にテストが行えるかというと、もう一つ問題がある。
mainに格納されるテストは、かならずしも初期状態ではじまっているとは限らない。
どういうことかというと、JCUnitにとっては、mainの最初の状態と、2個目の状態、これらは独立した因子でしかない。で、JCUnitは各々の因子の水準の組み合わせが所定の網羅率を持っているかしか気にしていない。なので、mainの最初にある状態をどうすれば作れるか?そんなことは考えていないのだ。

じゃ、mainに格納されたScenarioSequenceの最初の状態を作るのはユーザに任せればいいか?
それもいささかだせえ。

ということでサンプル内のもう一つのScenarioSequenceフィールド、setUpの出番だ。

  @FactorField(
  levelsProvider = FSMLevelsProvider.class,
    providerParams = {
      @Param("flyingSpaghettiMonster"),
      @Param("setUp")
  })
  public ScenarioSequence<FlyingSpaghettiMonster> setUp;

最初のパラメタ、flyingSpaghettiMonsterはmainのときと同じく、FSMオブジェクトを返す静的メソッドの名前。
二つ目のパラメタが"setUp"になっていると、同じメソッドによって生成されたmainのScenarioSequenceの最初の状態にたどり着くためのScenarioSequenceが生成されて、このフィールドに格納されるのだ。

なので、

    // 準備
    FSMUtils.performScenarioSequence(this.setUp, this.sut, ScenarioSequence.SIMPLE_REPORTER);
    // 実行
    FSMUtils.performScenarioSequence(this.main, this.sut, ScenarioSequence.SIMPLE_REPORTER);

とすると、テストの準備と実行ができることになる。

すでにずいぶん長くなってしまったので、以降は次回(明朝の予定)に。