jcunit's blog

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

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

さて昨日に引き続き、JCUnitによる有限状態機械のテストの書き方について。

テストメソッドの宣言

これはどうということもなくて

  public FlyingSpaghettiMonster sut = new FlyingSpaghettiMonster();

  @Test
  public void test() throws Throwable {
    FSMUtils.performScenarioSequence(this.main, this.sut, ScenarioSequence.SIMPLE_REPORTER);
  }

こう実装してしまえば終わり。
mainは昨日のエントリで言及したmainフィールドで、テスト内容ほぼそのものといっていいScenarioSequenceのオブジェクトが格納されている。

FSMのテストは、「ある状態で(Given)どんなことをやったら(When)どうなるべきか(Then)」にほかならない。(とJCUnitでは解釈している)
SUTが前回までにおこなった操作によっても挙動が変わり得ることを考えると、このGiven、When、Thenを何個か連ねた場合もテストするべきだろう。
このGiven, When, ThenをまとめたものがScenarioで、Scenarioを連ねたものがScenarioSequenceだ。

このScenarioSequenceをSUT (Software under test - テスト対象ソフトウェア)に対して実行する。
これがテストだ。
this.sutには、SUTが格納されている。
だから、上の一行でテストが実行できる。

ちなみに三番目の引数は、テストの実行状況をレポートするためのオブジェクト。
ひとまずここでは「クリシェ」ということでひとまず説明を割愛する。

そしてやや分かりにくいかもしれないが、大事なのがこれ。

  @Before
  public void before() throws Throwable {
    FSMUtils.performScenarioSequence(this.setUp, this.sut, ScenarioSequence.SILENT_REPORTER);
  }

前日のエントリではテストの「準備」をする、と書いた。
なんで準備が必要かというとこういうことだ。


ある状態機械が3つの状態を遷移するとしよう。

S0 - (a0) -> S1 - (a1) -> S2

最初の状態がS0、それにイベントa0が起こり、S1、さらにそこにa2がおこりS2に遷移する。
このとき、S0やa1に対応する状態やイベントを様々に変えてテストしたい。この「様々に変える」のにペアワイズのテクニックを使っているのが、今回実装したJCUnitによる有限状態機械テストサポートの中身だ。

もちろん、状態機械の設計によっては、存在し得ないようなパターンがたくさん存在する。たとえば、「使用済」状態の「お皿」オブジェクトを「未使用」状態にもどすことはできない、とか、buildメソッドが呼ばれる前にsetFactoryメソッドが呼ばれていないといけない、とか。

これは「制約」と呼ばれるもので表現すればよい。JCUnitのConstraintManagerにあたる。

ところが、状態機械がとり得る状態やそれに与えるイベントを因子や水準とみなすとしてもそれらの間にどのような制約があるかはかなり複雑だ。
複雑である一方、状態機械の仕様がひとたび与えられれば、機械的に決まる。こういものは計算機にやらせるべきことであって人間が手でやるべきことではない。

人間は状態機械の仕様を理解したり設計したりすることに集中するべきで、そこからテストを作ったり実行したりというのは計算機にやらせたいのだ。

閑話休題

上述の状態機械が3つの状態を遷移する場合の話に戻る。

S0が様々な状態をとる。S1も様々な状態をとる。以下、同様。でも、S0に指定された状態が、テスト対象となるソフトウェアの初期状態と違う場合はどうすればよいのだろう?
一つの考え方はS0は、必ず状態機械の初期状態だという制約を設けてしまうことだ。実は当初、そういう方法で実装してみたのだが、うまく行かなかったのだ。うまく行かなかった理由の詳細はいずれ別の機会に述べることもあると思うが、今は先を急ぐ。

結局、JCUnitのFSMサポートでは、S0は状態機械がとり得る状態(水準)が全部割り当てられることにし、そしてS0にたどり着くための方法は別の問題として別に解くことにした。
ではS0にたどり着くための手順はどうすれば生成できるのか?
言うまでもない。与えられたFSMの仕様から、S0に割り当てられた状態にたどりつく経路を探索すればよいのだ。

そうして得られた経路というか手順をたどれば、SUTを所定の状態にもっていくことができるはずだ。
そしてそれが、

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

このフィールドに格納されており、以下のコードで実行できる。

  @Before
  public void before() throws Throwable {
    FSMUtils.performScenarioSequence(this.setUp, this.sut, ScenarioSequence.SILENT_REPORTER);
  }

とまあこういうわけだ。