jcunit's blog

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

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

考えてみれば、ソフトウェアシステムというのは有限状態機械であり、ソフトウェアシステムの仕様を決めるとはその有限状態機械の仕様を決めることに他ならない。

と、いうことは有限状態機械の仕様からテストを自動的に生成して、それを自動的に実施できるなら、人間はソフトウェアシステム自体の設計と開発に集中してあとのことは計算機にやらせればよい、ということにならないだろうか?

なったらいいなあ。

今回のFSMサポートの目的はこの辺だ。
では、有限状態機械の仕様をどのようにJCUnitに伝えればよいだろうか?
ということでいよいよ、JCUnit上で如何にして有限状態機械を定義するかの解説に移る。

jcunit/FlyingSpaghettiMonsterTest.java at develop · dakusui/jcunit · GitHub

  public static enum Spec implements FSMSpec<FlyingSpaghettiMonster> {
    @StateSpec I {
      @Override public boolean check(FlyingSpaghettiMonster flyingSpaghettiMonster) {
        return flyingSpaghettiMonster.isReady();
      }

      @Override public Expectation<FlyingSpaghettiMonster> cook(FSM<FlyingSpaghettiMonster> fsm, String dish, String sauce) {
        return FSMUtils.valid(fsm, COOKED, CoreMatchers.startsWith("Cooking"));
      }
    },
    @StateSpec COOKED {
      @Override public boolean check(FlyingSpaghettiMonster flyingSpaghettiMonster) {
        return flyingSpaghettiMonster.isReady();
      }

      @Override public Expectation<FlyingSpaghettiMonster> eat(FSM<FlyingSpaghettiMonster> fsm) {
        return FSMUtils.valid(fsm, COOKED, CoreMatchers.containsString("yummy"));
      }

      @Override public Expectation<FlyingSpaghettiMonster> cook(FSM<FlyingSpaghettiMonster> fsm, String dish, String sauce) {
        return FSMUtils.valid(fsm, COOKED, CoreMatchers.startsWith("Cooking"));
      }
    },;


    @ActionSpec public Expectation<FlyingSpaghettiMonster> cook(FSM<FlyingSpaghettiMonster> fsm, String pasta, String sauce) {
      return FSMUtils.invalid();
    }

    @ParametersSpec public static final Object[][] cook = new Object[][] {
        { "spaghetti", "spaghettini" },
        { "peperoncino", "carbonara", "meat sauce" },
    };

    @ActionSpec public Expectation<FlyingSpaghettiMonster> eat(FSM<FlyingSpaghettiMonster> fsm) {
      return FSMUtils.invalid();
    }
  }

途中をすっとばすならば、上記の内部クラスSpecがJCUnitに渡す有限状態機械の仕様そのものだ。
JCUnitはこのSpecの定義内容を解析し、テストを生成し、実行する。

Specは別に内部クラスでなくてもよいし、enumでなくてもよいが以下の条件を満たす必要がある。

  1. SUTは任意のクラス。
  2. FSMSpecインタフェースを実装する必要がある。
  3. 状態を定義するには、フィールドを定義し、これにStateSpecアノテーションを付す。これらのフィールドは以下の条件を満たす必要がある。
    1. publicであること
    2. staticであること
    3. finalであること
    4. nullではないこと
    5. そのクラスのオブジェクトであること。(上述の例で言うとSpecクラスのオブジェクト。enumの場合には自動的にそうなる)
    6. checkメソッドは引数に与えられるSUTオブジェクトが、それが表す状態に適合しているか検査し、適合するならtrue、しないならfalseを返すこと
    7. "I"という名前のものがあること。初期状態として使われる。
  4. 状態遷移関数を定義するにはメソッドを定義し、これにActionSpecアノテーションを付す。これにより、SUTにある同名のメソッドが必要なタイミングで自動的に呼ばれるようになる。上掲の例で言うと、"cook"メソッドと"eat"メソッド。これらのメソッドは以下の条件を満たす必要がある。
    1. 第一引数はFSMクラスのオブジェクト
    2. 第二引数以降は、SUTの同名のメソッドに渡すべき引数。
    3. 名前が同じメソッドは一つしか使えない。(オーバーロードできない)
    4. 必ずExpectationオブジェクトを返すこと。このオブジェクトは、そのメソッドが実行された時に期待される戻り値や例外、実行後に遷移するべき状態が格納されたオブジェクトである。
  5. 上述の状態遷移関数に渡す引数がある時には、同名のフィールドを定義し、ParamsSpecアノテーションを付す。上掲の例で言うと、"cook"フィールド。このフィールドは以下の条件を満たす必要がある。
    1. publicであること
    2. staticであること
    3. finalであること
    4. nullではないこと
    5. Objectオブジェクトの値を持つこと。
    6. 配列の長さが、SUTの同名のメソッドの引数と同じであること。
    7. 配列の各要素は、nullではないこと。
    8. 配列の各要素は、それぞれSUTの同名のメソッドに与える引数の候補になっていること。この例で言うと、FlyingSpaghttiMonster#cookの第一引数にはspaghettiまたはspaghettini、第二引数にはcarbonara, peperoncino, またはmeat sauceが渡されるようにテストケースが生成される。

うーん。
頑張ってシンプルにしたんだけど、書き下してみると、けっこうややこしいなあ。
なんにしても、有限状態機械を定義するのには必要な情報だし、なれれば難しくはないと思う(多分)

。。続きます。